Test iOS image crop coordinates from view bounds to pixels

Test iOS image crop coordinates from view bounds to pixels

Test iOS image crop coordinates from view bounds to pixels

Place a 4000×3000-pixel image in a 300×300-point crop view, select the center, and export it—only to find that the subject has shifted upward. Before changing the gesture code, check the geometry: aspect-fit display leaves 37.5 points of padding at both the top and bottom. Mapping the selection against the full view gives you the wrong origin and height. On a cloud Mac, validate the coordinates in pure Swift before connecting the UI so you can separate math errors from rendering errors.

Establish a contract for the three coordinate spaces

Define three objects: the source image’s pixel rectangle, the rectangle where the image is actually displayed in the view, and the selection the user drags out. The last two must share the same coordinate space. One cannot be relative to a parent view while the other is relative to the image container.

This article covers aspect-fit display only, not aspect-fill cropping, rotation, or perspective. The origin is at the top left, with x increasing to the right and y increasing downward. The input image must already be normalized to its display orientation, and its width and height must come from its actual pixels, not the image object’s size in points.

The screen scale factor is not the image’s scale factor. Multiplying the selection by the screen scale works only when the dimensions happen to match; it is not a general cropping formula.

If the UI uses nested views, first use the coordinate-conversion API to convert the selection into the image container’s coordinate space, then call the function below. If zoom gestures are involved, you must also undo the translation and scale transforms first. Do not feed a transformed rectangle directly into a static formula.

Implement the display rectangle and pixel mapping

Save the following as CropCheck.swift. With the full Xcode toolchain available, run xcrun swift CropCheck.swift in Terminal. The code depends only on the system Foundation framework, makes no network requests, and does not require a simulator.

import Foundation

func cropPixels(
    source: CGSize,
    viewport: CGSize,
    selection: CGRect
) -> CGRect? {
    let values = [
        source.width, source.height,
        viewport.width, viewport.height,
        selection.origin.x, selection.origin.y,
        selection.width, selection.height
    ]
    guard values.allSatisfy({ $0.isFinite }),
          source.width > 0, source.height > 0,
          viewport.width > 0, viewport.height > 0,
          source.width.rounded(.down) == source.width,
          source.height.rounded(.down) == source.height,
          selection.width > 0, selection.height > 0
    else {
        return nil
    }

    let scale = min(
        viewport.width / source.width,
        viewport.height / source.height
    )
    guard scale.isFinite, scale > 0 else {
        return nil
    }

    let display = CGRect(
        x: (viewport.width - source.width * scale) / 2,
        y: (viewport.height - source.height * scale) / 2,
        width: source.width * scale,
        height: source.height * scale
    )
    let clipped = selection.intersection(display)
    guard !clipped.isNull, !clipped.isEmpty else {
        return nil
    }

    let left = max(
        0, floor((clipped.minX - display.minX) / scale)
    )
    let top = max(
        0, floor((clipped.minY - display.minY) / scale)
    )
    let right = min(
        source.width,
        ceil((clipped.maxX - display.minX) / scale)
    )
    let bottom = min(
        source.height,
        ceil((clipped.maxY - display.minY) / scale)
    )
    guard right > left, bottom > top else {
        return nil
    }

    return CGRect(
        x: left, y: top,
        width: right - left, height: bottom - top
    )
}

The order matters: first intersect the selection with the displayed image rectangle, then subtract the display offset, and finally divide by the scale factor. Rounding the view coordinates first amplifies the error by the size ratio between the source image and the view.

Pixel bounds are rounded outward to include every pixel touched by the selection. This does not guarantee that the selection’s mathematical dimensions remain exactly unchanged. If the product requires fixed output dimensions, handle export resizing as a separate, subsequent step.

Catch coordinate offsets with verifiable examples

Append the assertions below to the same file. A successful run produces no output; if any assertion fails, the process exits with a failure status. Do not run this acceptance script with optimization settings that remove assertions.

let source = CGSize(width: 4000, height: 3000)
let viewport = CGSize(width: 300, height: 300)

func mapped(_ rectangle: CGRect) -> CGRect? {
    cropPixels(
        source: source,
        viewport: viewport,
        selection: rectangle
    )
}

assert(
    mapped(CGRect(x: 75, y: 75, width: 150, height: 150))
    == CGRect(x: 1000, y: 500, width: 2000, height: 2000)
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 300, height: 300))
    == CGRect(x: 0, y: 0, width: 4000, height: 3000)
)

assert(
    mapped(CGRect(x: 10, y: 0, width: 20, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 0, y: 0, width: 0, height: 20))
    == nil
)

assert(
    mapped(CGRect(x: 225, y: 225, width: 100, height: 100))
    == CGRect(x: 3000, y: 2500, width: 1000, height: 500)
)

You can check the center-selection example by hand: the display scale is 0.075, and subtracting the top padding leaves the selection’s starting y-coordinate at 37.5 points, corresponding to 500 pixels in the source image. The final example extends past both the right and bottom edges, proving that the function clips the out-of-bounds portion rather than merely constraining the origin.

Add cases for portrait images, non-square containers, and selections with fractional coordinates. For every valid result, verify that the width and height are positive, the bounds stay within the source image, and all four edges have integer coordinates. Negative widths and heights are rejected here; the UI layer should normalize the drag direction beforehand.

Verify the calculation and the actual export separately

Once the math tests pass, prepare a test fixture with fixed pixel dimensions, a different color in each corner, and a cross at the center. Export to a lossless format first so that lossy encoding does not introduce noise into color assertions.

Check three things: the exported width and height match the function’s return value; a bottom-right crop does not include the top-left marker; and a full-image selection does not add transparent borders. Do not compare file bytes alone, because changes in encoder-generated metadata do not necessarily mean the pixels have changed.

If the cropper operates directly on the orientation-normalized pixel image, pass it the pixel rectangle from this article. If it redraws through a graphics context, separately verify the origin direction and output scale setting. A common second-stage error is multiplying by the display scale again in the export code, even though the coordinate function already returns pixels.

Also test asynchronous image loading: if a source image with different dimensions replaces the image after the user has made a selection, recalculate the selection or invalidate the old one. Do not combine the old image’s scale factor with the new image’s pixel dimensions.

Turn failures into reproducible inputs

For each failure, retain the source image’s pixel width and height, the container’s size in points, the selection rectangle, the actual display mode, and the calculated result. Recording these values is enough; there is no need to upload the user’s photo. Reproduce the issue with a test fixture of the same dimensions.

Before merging, cover at least six input categories: landscape images, portrait images, full-image selections, selections entirely within padding, selections extending beyond an edge, and fractional boundaries. After a layout change, read the container dimensions again rather than reusing a snapshot taken before rotation.

Final acceptance has two layers: the pure function verifies the coordinate relationships, while export tests verify pixel orientation and dimensions. Once both pass, check the dragging experience over remote desktop. UI adjustments, source-image replacements, and export implementation changes then each have a clearly identifiable point of failure.

Frequently asked questions

Should crop coordinates be multiplied by the screen scale?

Not directly. Derive the ratio from the source pixel dimensions and the actual image display rectangle. Screen scale does not describe that relationship.

Why round pixel bounds outward?

Flooring the minimum edges and ceiling the maximum edges covers every pixel touched by the continuous selection. Clamp the rounded result to the source bounds.

Run your projects on a dedicated physical Mac

Build, test, and use a remote desktop on a cloud Mac. Choose the configuration and billing period that suit your project. Check the console for real-time availability.

Next, set up your environment

Compare configurations, then choose how to connect. Use SSH for command-line builds and VNC for interactive debugging.