refactor(ios): preserve original image formats in share extension

Remove JPEG conversion and preserve original image formats and filenames
to test support for all image file types. Refactor image processing logic
for clarity and maintainability.

Changes:
- Remove forced JPEG conversion (was using 0.9 compression quality)
- Preserve original filename extensions instead of forcing .jpg
- Refactor from 3 branches to 2 (removed unlikely UIImage branch)
- Replace if statement with guard for non-image attachment filtering
- Simplify error check from optional binding to nil check
- Extract filename extension helper method to reduce duplication
- Update default filename from "shared-image.jpg" to "shared-image"

The only conversion that may occur is to PNG (lossless) when raw data
cannot be read from a file URL, preserving image quality while ensuring
compatibility.
This commit is contained in:
Jose Olarte III
2025-12-10 19:34:46 +08:00
parent a672c977a8
commit bb92e3ac4f

View File

@@ -13,7 +13,7 @@ class ShareViewController: UIViewController {
private let appGroupIdentifier = "group.app.timesafari" private let appGroupIdentifier = "group.app.timesafari"
private let sharedPhotoFileNameKey = "sharedPhotoFileName" private let sharedPhotoFileNameKey = "sharedPhotoFileName"
private let sharedPhotoFilePathKey = "sharedPhotoFilePath" private let sharedPhotoFilePathKey = "sharedPhotoFilePath"
private let sharedImageFileName = "shared-image.jpg" private let sharedImageFileName = "shared-image"
/// Get the App Group container URL for storing shared files /// Get the App Group container URL for storing shared files
private var appGroupContainerURL: URL? { private var appGroupContainerURL: URL? {
@@ -71,68 +71,31 @@ class ShareViewController: UIViewController {
} }
for attachment in attachments { for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) { // Skip non-image attachments
// First, try to load as UIImage directly (best for HEIF and other formats) guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
// This uses the system's built-in image decoding which handles HEIF automatically continue
if attachment.canLoadObject(ofClass: UIImage.self) { }
attachment.loadObject(ofClass: UIImage.self) { [weak self] (image, error) in
guard let self = self else { // Try to load raw data first to preserve original format
completion(false) // This preserves the original image format without conversion
return attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
}
if let error = error {
completion(false)
return
}
guard let image = image as? UIImage else {
completion(false)
return
}
// Convert to JPEG for web compatibility
guard let imageData = image.jpegData(compressionQuality: 0.9) else {
completion(false)
return
}
// Get filename from attachment if available
var fileName = "shared-image.jpg"
if let identifier = attachment.suggestedName {
let nameWithoutExt = (identifier as NSString).deletingPathExtension
fileName = "\(nameWithoutExt).jpg"
}
// Store image as file in App Group container
if self.storeImageData(imageData, fileName: fileName) {
completion(true)
} else {
completion(false)
}
}
return // Process only the first image
}
// Fallback: load as data/URL (for formats that don't support UIImage loading)
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
guard let self = self else { guard let self = self else {
completion(false) completion(false)
return return
} }
if let error = error { if error != nil {
completion(false) completion(false)
return return
} }
// Handle different image data types // Handle different image data types
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
var imageData: Data? var imageData: Data?
var fileName: String = "shared-image.jpg" var fileName: String = "shared-image"
if let url = data as? URL { if let url = data as? URL {
// Image provided as file URL // Most common case: Image provided as file URL - read raw data to preserve format
// For security-scoped resources, we need to access them properly
let accessing = url.startAccessingSecurityScopedResource() let accessing = url.startAccessingSecurityScopedResource()
defer { defer {
if accessing { if accessing {
@@ -140,32 +103,19 @@ class ShareViewController: UIViewController {
} }
} }
// Try loading as UIImage first (handles HEIF, RAW, etc.) // Read raw data directly to preserve original format
if let image = UIImage(contentsOfFile: url.path) { imageData = try? Data(contentsOf: url)
// Convert to JPEG for web compatibility fileName = url.lastPathComponent
imageData = image.jpegData(compressionQuality: 0.9)
// Preserve original filename but change extension to .jpg // Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
let originalName = url.lastPathComponent if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
if let nameWithoutExt = originalName.components(separatedBy: ".").first { imageData = image.pngData()
fileName = "\(nameWithoutExt).jpg" fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
} else {
fileName = "shared-image.jpg"
}
} else {
// Fallback: try reading raw data (for already web-compatible formats)
imageData = try? Data(contentsOf: url)
fileName = url.lastPathComponent
} }
} else if let data = data as? Data { } else if let data = data as? Data {
// Image provided as raw Data // Less common: Image provided as raw Data - use directly to preserve format
// Try to load as UIImage first to handle HEIF/RAW formats imageData = data
if let image = UIImage(data: data) { fileName = attachment.suggestedName ?? "shared-image"
imageData = image.jpegData(compressionQuality: 0.9)
} else {
// Fallback: use raw data (assumes already web-compatible)
imageData = data
}
fileName = "shared-image.jpg"
} }
guard let finalImageData = imageData else { guard let finalImageData = imageData else {
@@ -181,7 +131,6 @@ class ShareViewController: UIViewController {
} }
} }
return // Process only the first image return // Process only the first image
}
} }
} }
@@ -189,6 +138,14 @@ class ShareViewController: UIViewController {
completion(false) completion(false)
} }
/// Helper to get filename with a new extension, preserving base name
private func getFileNameWithExtension(_ originalName: String, newExtension: String) -> String {
if let nameWithoutExt = originalName.components(separatedBy: ".").first, !nameWithoutExt.isEmpty {
return "\(nameWithoutExt).\(newExtension)"
}
return "shared-image.\(newExtension)"
}
/// Store image data as a file in the App Group container /// Store image data as a file in the App Group container
/// All images are stored as files regardless of size for consistency and simplicity /// All images are stored as files regardless of size for consistency and simplicity
/// Returns true if successful, false otherwise /// Returns true if successful, false otherwise
@@ -197,8 +154,10 @@ class ShareViewController: UIViewController {
return false return false
} }
// Create file URL in the container // Create file URL in the container using the actual filename
let fileURL = containerURL.appendingPathComponent(sharedImageFileName) // Extract extension from fileName if present, otherwise use sharedImageFileName
let actualFileName = fileName.isEmpty ? sharedImageFileName : fileName
let fileURL = containerURL.appendingPathComponent(actualFileName)
// Remove old file if it exists // Remove old file if it exists
try? FileManager.default.removeItem(at: fileURL) try? FileManager.default.removeItem(at: fileURL)
@@ -216,7 +175,7 @@ class ShareViewController: UIViewController {
} }
// Store relative path and filename // Store relative path and filename
userDefaults.set(sharedImageFileName, forKey: sharedPhotoFilePathKey) userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey) userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
// Clean up any old base64 data that might exist // Clean up any old base64 data that might exist