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 sharedPhotoFileNameKey = "sharedPhotoFileName"
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
private var appGroupContainerURL: URL? {
@@ -71,68 +71,31 @@ class ShareViewController: UIViewController {
}
for attachment in attachments {
if attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
// First, try to load as UIImage directly (best for HEIF and other formats)
// This uses the system's built-in image decoding which handles HEIF automatically
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 {
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
// Skip non-image attachments
guard attachment.hasItemConformingToTypeIdentifier(UTType.image.identifier) else {
continue
}
// Try to load raw data first to preserve original format
// This preserves the original image format without conversion
attachment.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (data, error) in
guard let self = self else {
completion(false)
return
}
if let error = error {
if error != nil {
completion(false)
return
}
// Handle different image data types
// loadItem(forTypeIdentifier:) typically returns URL or Data, not UIImage
var imageData: Data?
var fileName: String = "shared-image.jpg"
var fileName: String = "shared-image"
if let url = data as? URL {
// Image provided as file URL
// For security-scoped resources, we need to access them properly
// Most common case: Image provided as file URL - read raw data to preserve format
let accessing = url.startAccessingSecurityScopedResource()
defer {
if accessing {
@@ -140,32 +103,19 @@ class ShareViewController: UIViewController {
}
}
// Try loading as UIImage first (handles HEIF, RAW, etc.)
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)
fileName = url.lastPathComponent
// Read raw data directly to preserve original format
imageData = try? Data(contentsOf: url)
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 {
// Image provided as raw Data
// 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
}
fileName = "shared-image.jpg"
// Less common: Image provided as raw Data - use directly to preserve format
imageData = data
fileName = attachment.suggestedName ?? "shared-image"
}
guard let finalImageData = imageData else {
@@ -181,7 +131,6 @@ class ShareViewController: UIViewController {
}
}
return // Process only the first image
}
}
}
@@ -189,6 +138,14 @@ class ShareViewController: UIViewController {
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
/// All images are stored as files regardless of size for consistency and simplicity
/// Returns true if successful, false otherwise
@@ -197,8 +154,10 @@ class ShareViewController: UIViewController {
return false
}
// Create file URL in the container
let fileURL = containerURL.appendingPathComponent(sharedImageFileName)
// Create file URL in the container using the actual filename
// 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
try? FileManager.default.removeItem(at: fileURL)
@@ -216,7 +175,7 @@ class ShareViewController: UIViewController {
}
// Store relative path and filename
userDefaults.set(sharedImageFileName, forKey: sharedPhotoFilePathKey)
userDefaults.set(actualFileName, forKey: sharedPhotoFilePathKey)
userDefaults.set(fileName, forKey: sharedPhotoFileNameKey)
// Clean up any old base64 data that might exist