照片选择页显示的是竖图,上传后的缩略图却横了过来。接手这类问题时,先别改裁剪比例:JPEG 可能保存着横向排列的像素,同时用 EXIF 方向标记要求查看器旋转显示。iOS 的 UIImage 可以把它画对,但另一段导出代码或接收端未必以同样方式处理。下面在云端 Mac 的 iOS 测试目标中造出固定样本,把“显示正确”和“文件正确”分开验收。
先区分方向标记与像素
记录三项值:文件的 EXIF 方向、解码后 UIImage.imageOrientation,以及底层 CGImage 的像素宽高。EXIF 方向值 6 表示显示时需要顺时针旋转;它不等于像素已经转过。若底层像素宽 120、高 80,正确显示时应是宽 80、高 120。
不要仅凭预览截图判定修复完成。预览组件可能替文件应用了方向标记,而后续处理链路仍在传递原始像素。
先确定责任边界:原文件需要保留,就把它与归一化产物分别保存;只需要统一方向的上传图,就在导出前重新绘制,并对导出文件再做一次解码检查。不要直接把原文件的方向值改成 1,那样不会移动任何像素。
在 iOS 测试中生成固定样本
把以下测试放进启用 UIKit 的 iOS 单元测试目标。它在运行时生成一张 120×80 像素的 JPEG,并写入方向值 6;不依赖开发者电脑上的照片,也不把真实素材或用户信息提交到仓库。代码同时检查 ImageIO 读出的标记与 UIKit 解码后的方向。
import XCTest
import UIKit
import ImageIO
import UniformTypeIdentifiers
final class ImageOrientationTests: XCTestCase {
func testOrientationSurvivesExport() throws {
let format = UIGraphicsImageRendererFormat()
format.scale = 1
format.opaque = true
let source = UIGraphicsImageRenderer(
size: CGSize(width: 120, height: 80),
format: format
).image { _ in
UIColor.red.setFill()
UIRectFill(CGRect(x: 0, y: 0, width: 60, height: 80))
UIColor.blue.setFill()
UIRectFill(CGRect(x: 60, y: 0, width: 60, height: 80))
}
let jpeg = NSMutableData()
let destination = try XCTUnwrap(
CGImageDestinationCreateWithData(
jpeg, UTType.jpeg.identifier as CFString, 1, nil
)
)
CGImageDestinationAddImage(
destination,
try XCTUnwrap(source.cgImage),
[kCGImagePropertyOrientation: 6] as CFDictionary
)
XCTAssertTrue(CGImageDestinationFinalize(destination))
let input = jpeg as Data
let imageSource = try XCTUnwrap(
CGImageSourceCreateWithData(input as CFData, nil)
)
let properties = try XCTUnwrap(
CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil)
as? [CFString: Any]
)
XCTAssertEqual(
(properties[kCGImagePropertyOrientation] as? NSNumber)?.intValue,
6
)
let decoded = try XCTUnwrap(UIImage(data: input))
XCTAssertEqual(decoded.imageOrientation, .right)
XCTAssertEqual(decoded.cgImage?.width, 120)
XCTAssertEqual(decoded.cgImage?.height, 80)
let upright = UIGraphicsImageRenderer(
size: CGSize(width: 80, height: 120),
format: format
).image { _ in
decoded.draw(in: CGRect(x: 0, y: 0, width: 80, height: 120))
}
XCTAssertEqual(upright.imageOrientation, .up)
let exported = try XCTUnwrap(upright.pngData())
let reopened = try XCTUnwrap(UIImage(data: exported))
XCTAssertEqual(reopened.imageOrientation, .up)
XCTAssertEqual(reopened.cgImage?.width, 80)
XCTAssertEqual(reopened.cgImage?.height, 120)
}
}
运行前确认测试目标已包含这份 Swift 文件,并选用项目支持的 iOS 模拟器。命令行执行时,为 -workspace、-scheme 和 -destination 填入项目中的实际名称;不要把示例设备名称当作所有机器都具备的运行目标。
把验收接到真实导出路径
上面的测试证明了归一化方法可行,还没有证明业务代码真的调用了它。下一步把“解码、按目标尺寸绘制、编码”放进项目已有的图片导出函数,让测试调用同一个函数。测试输入仍可在运行时生成,断言则应落在重新打开的导出文件上,而不是落在函数返回前的 UIImage 上。
给业务函数留下清晰的尺寸契约
先约定函数接受原始文件数据,输出已确定显示方向的数据。方向为 6 的这张样本,输出像素应由 120×80 变成 80×120。若业务还会缩放,改为检查输出宽高比及缩放后的精确目标尺寸,并单独测试缩放规则;不要把旋转、压缩和裁剪的失败混成一个断言。
这里用左右两色,是为了人工查看方向时能发现内容被镜像。自动验收若要检查颜色落点,应使用带容差的像素比较:JPEG 有损编码会改变边界附近的颜色,不能要求每个通道逐位相等。
排查通过预览却失败的路径
先找图片在哪一步换了表示方式:文件数据、UIImage、CGImage、绘图上下文,还是重新编码后的文件。对每一次转换分别记录方向和像素尺寸,通常能定位到“取出 cgImage 后直接编码”这一类分叉:cgImage 给出的是像素,不能把 UIImage 绘制时的方向处理视作已经写进像素。
还要检查导出目标。重新绘制会产生新的像素和文件数据,也可能改变编码格式、元数据与文件大小。如果工作流需要保留原文件的其他元数据,应逐项列出必须保留的字段并为它们写测试,而不是假定重绘会原样复制。上传链路最好在发送前对最终文件再解码一次;查看器里正确的中间对象不是交付结果。
合并前检查清单
至少覆盖方向为 1 的普通图、方向为 6 的旋转图,以及业务实际允许的镜像方向。对每种样本分别断言输入标记、归一化后的像素宽高和再次解码的显示方向。若生产代码允许裁剪,再补一张非正方形样本:正方形即使旋转错误,宽高断言也看不出来。
最后在与交付流程相同的 iOS 测试目标中运行用例,保存失败时的输入方向、输出尺寸和处理阶段即可;不要把用户原图写进公开日志。这样一次回归失败指向的是文件处理步骤,而不只是“某张照片看起来不对”。
常见问题
UIImage 显示正确,为什么上传后的图片仍可能转向?
UIImage 绘制时可以依据方向元数据旋转画面,但接收方未必按同一规则读取元数据。导出前应检查实际像素方向,并对导出的文件重新解码验收。
归一化图片时只把方向标记改为 1 可以吗?
不可以。若像素仍按原始方向排列,仅修改标记会改变显示结果。应按原方向重新绘制像素,再确认输出方向为正向、宽高符合预期。
把下一次构建交给独享的云端 Mac。
核对 ViewMini 的机型、区域和租期,再配置适合自己工作流的物理节点。