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 {
completion(false)
return
} }
if let error = error { // Try to load raw data first to preserve original format
completion(false) // This preserves the original image format without conversion
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 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) {
// Convert to JPEG for web compatibility
imageData = image.jpegData(compressionQuality: 0.9)
// Preserve original filename but change extension to .jpg
let originalName = url.lastPathComponent
if let nameWithoutExt = originalName.components(separatedBy: ".").first {
fileName = "\(nameWithoutExt).jpg"
} else {
fileName = "shared-image.jpg"
}
} else {
// Fallback: try reading raw data (for already web-compatible formats)
imageData = try? Data(contentsOf: url) imageData = try? Data(contentsOf: url)
fileName = url.lastPathComponent fileName = url.lastPathComponent
// Fallback: if raw data read fails, try UIImage and convert to PNG (lossless)
if imageData == nil, let image = UIImage(contentsOfFile: url.path) {
imageData = image.pngData()
fileName = self.getFileNameWithExtension(url.lastPathComponent, newExtension: "png")
} }
} 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
if let image = UIImage(data: data) {
imageData = image.jpegData(compressionQuality: 0.9)
} else {
// Fallback: use raw data (assumes already web-compatible)
imageData = data imageData = data
} fileName = attachment.suggestedName ?? "shared-image"
fileName = "shared-image.jpg"
} }
guard let finalImageData = imageData else { guard let finalImageData = imageData else {
@@ -183,12 +133,19 @@ class ShareViewController: UIViewController {
return // Process only the first image return // Process only the first image
} }
} }
}
// No image found // No image found
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