照片選取畫面顯示的是直向圖片,上傳後的縮圖卻變成橫向。接手這類問題時,先別調整裁切比例: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 的機型、地區與租期,再設定符合工作流程的獨享實體節點。