Skip to content

Commit 8a8e4f4

Browse files
committed
Reduce image processing overhead
Signed-off-by: Ethan Dye <mrtops03@gmail.com>
1 parent 2513e9f commit 8a8e4f4

2 files changed

Lines changed: 43 additions & 158 deletions

File tree

Sources/macSubtitleOCR/Subtitles/Subtitle.swift

Lines changed: 43 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -49,34 +49,56 @@ class Subtitle: @unchecked Sendable {
4949

5050
// Converts the RGBA data to a CGImage
5151
func createImage(_ invert: Bool) -> CGImage? {
52-
// Convert the image data to RGBA format using the palette
53-
let rgbaData = imageDataToRGBA()
52+
var rgbaData = imageDataToRGBA()
5453

54+
var minX = imageWidth!, maxX = 0, minY = imageHeight!, maxY = 0
55+
for y in 0 ..< imageHeight! {
56+
for x in 0 ..< imageWidth! {
57+
let pixelIndex = (y * imageWidth! + x) * 4
58+
let alpha = rgbaData[pixelIndex + 3]
59+
if alpha > 0 {
60+
minX = min(minX, x)
61+
maxX = max(maxX, x)
62+
minY = min(minY, y)
63+
maxY = max(maxY, y)
64+
if !invert {
65+
rgbaData[pixelIndex] = 255 - rgbaData[pixelIndex]
66+
rgbaData[pixelIndex + 1] = 255 - rgbaData[pixelIndex + 1]
67+
rgbaData[pixelIndex + 2] = 255 - rgbaData[pixelIndex + 2]
68+
}
69+
} else {
70+
// Set transparent pixels to white
71+
rgbaData[pixelIndex] = 255
72+
rgbaData[pixelIndex + 1] = 255
73+
rgbaData[pixelIndex + 2] = 255
74+
rgbaData[pixelIndex + 3] = 255
75+
}
76+
}
77+
}
78+
79+
guard let provider = CGDataProvider(data: rgbaData as CFData) else { return nil }
5580
let bitmapInfo = CGBitmapInfo.byteOrder32Big
5681
.union(CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue))
5782
let colorSpace = CGColorSpaceCreateDeviceRGB()
5883

59-
guard let provider = CGDataProvider(data: rgbaData as CFData) else {
60-
return nil
61-
}
62-
63-
let image = CGImage(width: imageWidth!,
64-
height: imageHeight!,
65-
bitsPerComponent: 8,
66-
bitsPerPixel: 32,
67-
bytesPerRow: imageWidth! * 4, // 4 bytes per pixel (RGBA)
68-
space: colorSpace,
69-
bitmapInfo: bitmapInfo,
70-
provider: provider,
71-
decode: nil,
72-
shouldInterpolate: false,
73-
intent: .defaultIntent)
84+
let image = CGImage(
85+
width: imageWidth!,
86+
height: imageHeight!,
87+
bitsPerComponent: 8,
88+
bitsPerPixel: 32,
89+
bytesPerRow: imageWidth! * 4,
90+
space: colorSpace,
91+
bitmapInfo: bitmapInfo,
92+
provider: provider,
93+
decode: nil,
94+
shouldInterpolate: false,
95+
intent: .defaultIntent)
7496

75-
guard var croppedImage = cropImageToVisibleArea(image!) else { return nil }
76-
if invert {
77-
croppedImage = invertColors(of: croppedImage) ?? croppedImage
97+
if minX == imageWidth! || maxX == 0 || minY == imageHeight! || maxY == 0 {
98+
return nil
7899
}
79-
return changeTransparency(for: croppedImage, to: CGColor.white)
100+
let croppedRect = CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)
101+
return image?.cropping(to: croppedRect)
80102
}
81103

82104
// MARK: - Methods
@@ -108,138 +130,4 @@ class Subtitle: @unchecked Sendable {
108130

109131
return rgbaData
110132
}
111-
112-
/// Crops the image to the visible (non-transparent) area and adds a buffer around it.
113-
/// - Parameter image: The original CGImage to crop.
114-
/// - Returns: A cropped CGImage, or nil if the image is fully transparent.
115-
private func cropImageToVisibleArea(_ image: CGImage) -> CGImage? {
116-
let buffer = 10 // Buffer size around the non-transparent area
117-
let width = image.width
118-
let height = image.height
119-
let newWidth = width + buffer * 2
120-
let newHeight = height + buffer * 2
121-
122-
// Create a new image context with extended dimensions
123-
guard let context = CGContext(data: nil,
124-
width: newWidth,
125-
height: newHeight,
126-
bitsPerComponent: image.bitsPerComponent,
127-
bytesPerRow: 0,
128-
space: image.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
129-
bitmapInfo: image.bitmapInfo.rawValue) else {
130-
return nil
131-
}
132-
133-
// Clear the context (set a transparent background)
134-
context.clear(CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
135-
136-
// Draw the original image onto the new context at the center
137-
context.draw(image, in: CGRect(x: buffer, y: buffer, width: width, height: height))
138-
139-
guard let extendedImage = context.makeImage(),
140-
let dataProvider = extendedImage.dataProvider,
141-
let data = dataProvider.data,
142-
let pixelData = CFDataGetBytePtr(data) else {
143-
return nil
144-
}
145-
146-
let bytesPerPixel = extendedImage.bitsPerPixel / 8
147-
let bytesPerRow = extendedImage.bytesPerRow
148-
149-
// Initialize variables to track the non-transparent bounds
150-
var minX = newWidth, maxX = 0, minY = newHeight, maxY = 0
151-
152-
// Scan the image to find the non-transparent pixels
153-
for y in 0 ..< newHeight {
154-
for x in 0 ..< newWidth {
155-
let pixelIndex = y * bytesPerRow + x * bytesPerPixel
156-
let alpha = pixelData[pixelIndex + 3] // Assuming RGBA format
157-
158-
if alpha > 0 { // Non-transparent pixel found
159-
minX = min(minX, x)
160-
maxX = max(maxX, x)
161-
minY = min(minY, y)
162-
maxY = max(maxY, y)
163-
}
164-
}
165-
}
166-
167-
// If the image is fully transparent, return nil
168-
if minX == newWidth || maxX == 0 || minY == newHeight || maxY == 0 {
169-
return nil
170-
}
171-
172-
// Apply buffer to the bounding box, ensuring it's within image bounds
173-
minX = max(0, minX - buffer)
174-
maxX = min(newWidth - 1, maxX + buffer)
175-
minY = max(0, minY - buffer)
176-
maxY = min(newHeight - 1, maxY + buffer)
177-
178-
// Crop the image to the visible (non-transparent) area with the buffer
179-
let croppedRect = CGRect(x: minX, y: minY, width: maxX - minX + 1, height: maxY - minY + 1)
180-
return extendedImage.cropping(to: croppedRect)
181-
}
182-
183-
private func invertColors(of image: CGImage) -> CGImage? {
184-
let colorSpace = CGColorSpaceCreateDeviceRGB()
185-
186-
// Create a context with the same dimensions as the image
187-
guard let context = CGContext(
188-
data: nil,
189-
width: image.width,
190-
height: image.height,
191-
bitsPerComponent: 8,
192-
bytesPerRow: image.width * 4,
193-
space: colorSpace,
194-
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
195-
return nil
196-
}
197-
198-
// Draw the image into the context
199-
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
200-
201-
// Get the pixel data from the context
202-
guard let pixelBuffer = context.data else {
203-
return nil
204-
}
205-
206-
let pixelData = pixelBuffer.bindMemory(to: UInt8.self, capacity: image.width * image.height * 4)
207-
208-
// Iterate through the pixel data and invert the colors
209-
for y in 0 ..< image.height {
210-
for x in 0 ..< image.width {
211-
let pixelIndex = (y * image.width + x) * 4
212-
pixelData[pixelIndex] = 255 - pixelData[pixelIndex] // Red
213-
pixelData[pixelIndex + 1] = 255 - pixelData[pixelIndex + 1] // Green
214-
pixelData[pixelIndex + 2] = 255 - pixelData[pixelIndex + 2] // Blue
215-
}
216-
}
217-
218-
// Create a new CGImage from the modified pixel data
219-
return context.makeImage()
220-
}
221-
222-
func changeTransparency(for image: CGImage, to color: CGColor) -> CGImage? {
223-
let colorSpace = CGColorSpaceCreateDeviceRGB()
224-
225-
guard let context = CGContext(
226-
data: nil,
227-
width: image.width,
228-
height: image.height,
229-
bitsPerComponent: image.bitsPerComponent,
230-
bytesPerRow: image.bytesPerRow,
231-
space: colorSpace,
232-
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
233-
return nil
234-
}
235-
236-
// Fill the context with the specified color
237-
context.setFillColor(color)
238-
context.fill(CGRect(x: 0, y: 0, width: image.width, height: image.height))
239-
240-
// Draw the image on top of the colored background
241-
context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
242-
243-
return context.makeImage()
244-
}
245133
}

Sources/macSubtitleOCR/macSubtitleOCR.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ struct macSubtitleOCR: AsyncParsableCommand {
6868
var results: [macSubtitleOCRResult] = []
6969

7070
if input.hasSuffix(".sub") || input.hasSuffix(".idx") {
71-
options.invert.toggle() // Invert the image if the input is a VobSub file
7271
let sub = try VobSub(
7372
URL(fileURLWithPath: input.replacingOccurrences(of: ".idx", with: ".sub")),
7473
URL(fileURLWithPath: input.replacingOccurrences(of: ".sub", with: ".idx")))
@@ -100,14 +99,12 @@ struct macSubtitleOCR: AsyncParsableCommand {
10099
let result = try await processSubtitle(pgs.subtitles, trackNumber: track.trackNumber)
101100
results.append(result)
102101
} else if track.codecID == "S_VOBSUB" {
103-
options.invert.toggle() // Invert the image if the input is VobSub
104102
let vobSub: VobSub = try track.trackData
105103
.withUnsafeBytes { (buffer: UnsafeRawBufferPointer) in
106104
try VobSub(buffer, track.idxData ?? "")
107105
}
108106
let result = try await processSubtitle(vobSub.subtitles, trackNumber: track.trackNumber)
109107
results.append(result)
110-
options.invert.toggle() // Reset the invert flag
111108
}
112109
}
113110
} else if input.hasSuffix(".sup") {

0 commit comments

Comments
 (0)