Skip to content

Commit e5782bb

Browse files
committed
Add .jpg compression, tweak docs, add error type
1 parent 0754018 commit e5782bb

File tree

1 file changed

+57
-5
lines changed

1 file changed

+57
-5
lines changed

Sources/ATProtoKit/Utilities/ATImageProcessing.swift

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ protocol ImageProtocol {
3030
func loadImageData(from imagePath: String) -> Self?
3131
}
3232

33+
/// An error type related to ``ATImageProcessable``..
34+
enum ATImageProcessingError: Error {
35+
/// The image's file size can't be lowered any further to fit the target file size.
36+
case unableToResizeImage
37+
}
38+
3339

3440
/// Provides a standardized approach to processing images for uploading to servers that interact with the AT Protocol.
3541
///
@@ -52,7 +58,7 @@ protocol ImageProtocol {
5258
/// Below is a sample implementation showcasing how to conform to `ATImageProcessable` for a custom image type:
5359
/// ```swift
5460
/// class CustomImageProcessor: ATImageProcessable {
55-
/// func convertToImageQuery(image: ATImage, altText: String, targetFileSize: Int = 1_000_000) -> ImageQuery? {
61+
/// func convertToImageQuery(imagePath: String, altText: String, targetFileSize: Int = 1_000_000) -> ImageQuery? {
5662
/// let atImage = stripMetadata(from: image)
5763
/// guard let customImage = atImage as? CustomImageType else {
5864
/// return nil
@@ -91,14 +97,14 @@ public protocol ATImageProcessable {
9197
///
9298
/// It's required for the image to be less than 1MB (1,000,000 bytes). This method will shrink the file size to the largest possible size that doesn't exceed the limit. Note that image quality may be affected.
9399
/// - Parameters:
94-
/// - image: The image that needs to be prepared.
100+
/// - image: The file path of the image that needs to be prepared.
95101
/// - altText: The alt text used to help blind and low-vision users know what's contained in the text. Optional.
96102
/// - targetFileSize: The size (in bytes) the file needs to be.
97103
/// - Returns: An ``ImageQuery``, which combines the image itself in a `Data` format and the alt text.
98-
func convertToImageQuery(image: ATImage, altText: String?, targetFileSize: Int) -> ImageQuery?
104+
func convertToImageQuery(imagePath: String, altText: String?, targetFileSize: Int) -> ImageQuery?
99105
/// Removes all EXIF and GPS metadata from the image.
100106
///
101-
/// This method should ideally be before ``convertToImageQuery(image:altText:)`` does anything to the image, as it will help with the process of maintaining more of the image quality.
107+
/// This method should ideally be before ``convertToImageQuery(imagePath:altText:targetFileSize:)-2fma7`` does anything to the image, as it will help with the process of maintaining more of the image quality.
102108
func stripMetadata(from image: ATImage) -> ATImage?
103109
}
104110

@@ -115,6 +121,13 @@ extension ATImageProcessable {
115121
return nil
116122
}
117123

124+
// Aspect ratio.
125+
let imageHeight = atImage.size.height
126+
let imageWidth = atImage.size.width
127+
128+
// Calculate the aspect ratio. Make sure it's not dividing by zero.
129+
let aspectRatio: CGFloat = imageHeight > 0 ? imageWidth / imageHeight : 0
130+
118131
// Determine the image format from the file extension.
119132
let filename = URL(fileURLWithPath: imagePath).lastPathComponent
120133

@@ -125,7 +138,15 @@ extension ATImageProcessable {
125138
case "png":
126139
imageData = bitmapImage.representation(using: .png, properties: [:])
127140
case "jpg", "jpeg":
128-
imageData = bitmapImage.representation(using: .jpeg, properties: [:])
141+
do {
142+
try decreaseJPGSize(tiffData, bitmapImage: bitmapImage, targetFileSize: targetFileSize)
143+
} catch ATImageProcessingError.unableToResizeImage {
144+
return nil
145+
} catch {
146+
// Handle any other errors
147+
print("An unexpected error occurred: \(error.localizedDescription)")
148+
return nil
149+
}
129150
case "gif":
130151
imageData = bitmapImage.representation(using: .gif, properties: [:])
131152
case "webp":
@@ -142,6 +163,37 @@ extension ATImageProcessable {
142163
return nil
143164
}
144165

166+
internal func decreaseJPGSize(_ image: Data, bitmapImage: NSBitmapImageRep, targetFileSize: Int) throws -> Data {
167+
var imageData = image
168+
var imageBitmap = bitmapImage
169+
// Check if the file size is already lower than the targetFileSize.
170+
if imageData.count <= targetFileSize {
171+
return image
172+
}
173+
174+
// Loops while the size of the image exceeds the target image size.
175+
var compressionFloat = 1.0
176+
177+
while compressionFloat >= 0.5 {
178+
var imageInstance = bitmapImage
179+
180+
// Lower the compression factor by 5%.
181+
compressionFloat -= 0.05
182+
183+
guard let finalImage = imageInstance.representation(using: .jpeg, properties: [.compressionFactor : compressionFloat]) else {
184+
break
185+
}
186+
187+
// If the resulting compression makes the file size lower, set the value and break the loop.
188+
if finalImage.count <= targetFileSize {
189+
imageBitmap = imageInstance
190+
break
191+
}
192+
}
193+
194+
throw ATImageProcessingError.unableToResizeImage
195+
}
196+
145197
internal func stripMetadata(from image: ATImage) -> ATImage? {
146198
guard let tiffRepresentation = image.tiffRepresentation,
147199
let source = CGImageSourceCreateWithData(tiffRepresentation as CFData, nil) else {

0 commit comments

Comments
 (0)