云端 Mac 上验收 iOS 图片裁剪坐标:从视图选区到像素矩形

云端 Mac 上验收 iOS 图片裁剪坐标:从视图选区到像素矩形

把一张 4000×3000 像素的图片放进 300×300 点的裁剪视图,选择中央区域,导出后却发现人物向上偏移。先别改手势代码:等比适应显示会在上下留下各 37.5 点空白。如果直接按整个视图换算,选区起点和高度都会出错。在云端 Mac 上先用纯 Swift 验证坐标,再接界面,可以把数学错误与渲染错误分开。

固定三套坐标的契约

明确三个对象:原图像素矩形、图片在视图中的实际显示矩形、用户拖出的选区。后两个对象必须位于同一坐标空间,不能一个相对父视图,另一个相对图片容器。

本文只处理等比适应显示,不处理填充裁切、旋转或透视。约定原点在左上角,横轴向右、纵轴向下;输入图片已经按显示方向规范化,宽高来自实际像素,而不是图片对象的点尺寸。

屏幕缩放倍率不是图片缩放比例。用屏幕倍率乘选区,只在偶然匹配的尺寸下正确,不能作为通用裁剪公式。

若界面使用嵌套视图,先通过坐标转换接口把选区转换到图片容器,再调用下面的函数。缩放手势存在时,还要先消除平移与缩放变换,不能把变换后的矩形直接套进静态公式。

实现显示矩形与像素换算

将以下内容保存为 CropCheck.swift。完整 Xcode 工具链可用时,在终端执行 xcrun swift CropCheck.swift。代码仅依赖系统 Foundation,不访问网络,也不需要模拟器。

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
    )
}

顺序不能交换:先求选区与显示区域的交集,再减去显示偏移,最后除以缩放比例。若先取整视图坐标,误差会被原图与视图的尺寸比例放大。

像素边界采用向外取整,目的是覆盖选区触及的像素。它不保证数学上的选区尺寸完全不变;如果产品要求固定输出尺寸,应把缩放导出作为后续独立步骤。

用可核算的样例阻断错位

继续把下面的断言追加到同一文件。成功执行时没有输出;任意断言失败,进程会以失败状态结束。不要使用会移除断言的优化方式运行这份验收脚本。

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)
)

中央样例可以手算:显示比例为 0.075,选区起点扣除顶部留白后是 37.5 点,对应原图纵坐标 500 像素。最后一个样例同时越过右边和底边,用来证明函数确实裁掉越界部分,而非只限制起点。

再补充竖图、非正方形容器和小数选区。对任意有效结果,都检查宽高大于零、边界不超出原图、四条边均为整数。负宽高输入在这里直接拒绝;拖拽方向应由界面层提前规范化。

分开检查计算结果与实际导出

数学测试通过后,准备一张四角颜色不同、中心带十字的固定像素夹具。先用无损格式导出,避免有损编码让颜色断言出现噪声。

检查三项结果:导出宽高等于函数返回值;裁右下角时没有混入左上角标记;整图选择没有多出透明边。不要只比较文件字节,因为编码器附带的元数据变化不一定代表像素变化。

裁剪器若直接使用规范化后的像素图,传入本文的像素矩形;若通过图形上下文重新绘制,则另行确认原点方向和输出缩放参数。最常见的二次错误,是坐标函数已经返回像素,导出代码又乘了一次显示倍率。

图片异步加载时也要验收:用户选区之后若换入另一尺寸的原图,必须重新计算或使旧选区失效。不要把旧图的比例与新图的像素尺寸混用。

把失败记录变成可复现输入

每次失败保留原图像素宽高、容器点尺寸、选区矩形、实际显示模式和计算结果。记录数值即可,不必上传用户照片;复现时用相同尺寸的测试夹具替代。

合并前至少覆盖横图、竖图、全图选择、纯留白选择、边缘越界和小数边界六类输入。布局变化后必须重新读取容器尺寸,不能沿用旋转前的快照。

最终验收分两层:纯函数负责证明坐标关系,导出测试负责证明像素方向与尺寸。两层都通过,再用远程桌面检查拖拽体验。这样界面调整、原图替换和导出实现变更,都有各自明确的失败位置。

常见问题

裁剪坐标要不要乘屏幕缩放倍率?

不能直接乘。先根据原图像素尺寸和实际显示矩形计算比例;屏幕缩放倍率描述显示设备,不代表原图与视图之间的比例。

为什么像素边界采用向外取整?

向下取整最小边界、向上取整最大边界,可以覆盖连续选区涉及的像素。它最多向每侧扩展不足一个像素,取整后仍须限制在原图范围内。

在独享物理 Mac 上运行你的工程

使用云端 Mac 完成构建、测试与远程桌面操作。按工程需要选择配置和计费周期,实际可用状态以控制台实时返回为准。

下一步,确认你的运行环境

先比较配置,再选择接入方式。SSH 命令行与 VNC 桌面可分别用于构建和交互调试。