Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CodeScanner/CodeScanner/Code.mm
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ + (nonnull NSData *)decode:(nonnull NSData *)data {
}

+ (nullable NSData *)scan:(nonnull NSData *)data width:(NSInteger)width height:(NSInteger)height {
[self scan:data width:width height:height quality:KikCodesScanQualityHigh];
return [self scan:data width:width height:height quality:KikCodesScanQualityHigh];
}

+ (nullable NSData *)scan:(nonnull NSData *)data width:(NSInteger)width height:(NSInteger)height quality:(KikCodesScanQuality)quality {
Expand Down
101 changes: 76 additions & 25 deletions Flipcash/Core/Screens/Main/Bill/Extraction/CodeExtractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,15 @@ class CodeExtractor: CameraSessionExtractor {
required init() {}

func extract(output: AVCaptureOutput, sampleBuffer: CMSampleBuffer, connection: AVCaptureConnection) -> ScannedCode? {
let sample = extractSample(from: sampleBuffer)

guard let sample = sample else {
return nil
// The scan runs inside withLuminanceSample so the sample's zero-copy view of the plane
// stays valid -- the base address is only guaranteed while the pixel buffer is locked.
withLuminanceSample(from: sampleBuffer) { sample in
Self.processSample(
sample: sample,
quality: .best,
container: &container
)
}

let payload = Self.processSample(
sample: sample,
quality: .best,
container: &container
)

return payload
}

private static func processSample(sample: Sample, quality: KikCodesScanQuality) -> (Data, ScannedCode)? {
Expand Down Expand Up @@ -61,31 +57,86 @@ class CodeExtractor: CameraSessionExtractor {
return nil
}

private func extractSample(from sampleBuffer: CMSampleBuffer) -> Sample? {
/// Vends the frame's luminance (Y) plane as a `Sample`, tightly packed the way `kikCodeScan`
/// expects, and calls `body` with it.
///
/// Internal rather than private so `CodeScanSweepTests` can drive it with synthesized frames.
///
/// The sample is only valid for the duration of `body`: when the plane is already tightly
/// packed its `data` is a no-copy view of the locked pixel buffer, which CoreVideo only
/// guarantees between lock and unlock.
func withLuminanceSample<T>(
from sampleBuffer: CMSampleBuffer,
_ body: (Sample) -> T?
) -> T? {
guard let buffer = CMSampleBufferGetImageBuffer(sampleBuffer) else {
return nil
}
CVPixelBufferLockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))

CVPixelBufferLockBaseAddress(buffer, .readOnly)
defer {
CVPixelBufferUnlockBaseAddress(buffer, CVPixelBufferLockFlags(rawValue: 0))
CVPixelBufferUnlockBaseAddress(buffer, .readOnly)
}

guard let base = CVPixelBufferGetBaseAddressOfPlane(buffer, 0) else {
return nil
}

let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
let width = CVPixelBufferGetWidth(buffer)
let height = CVPixelBufferGetHeight(buffer)


// The capture format is planar 420, so these must be read per-plane.
// CVPixelBufferGetBytesPerRow reports a whole-buffer value for planar formats -- at
// 1920x1080 it returns 2884 rather than the plane's actual 1920 -- which both over-claims
// the buffer's length and hides the row padding below.
let width = CVPixelBufferGetWidthOfPlane(buffer, 0)
let height = CVPixelBufferGetHeightOfPlane(buffer, 0)
let rowStride = CVPixelBufferGetBytesPerRowOfPlane(buffer, 0)

let sample = Sample(
width: width,
height: height,
data: Data(bytesNoCopy: base, count: bytesPerRow * height, deallocator: .none)
data: Self.luminanceData(base: base, width: width, height: height, rowStride: rowStride)
)

return sample

return body(sample)
}

/// Produces the tightly packed `width * height` buffer `kikCodeScan` reads.
///
/// CoreVideo aligns plane rows to 64 bytes, so a capture width that is not a multiple of 64
/// arrives padded -- 1440 wide comes back with a 1472-byte stride. Handing that straight to the
/// scanner shears the image by a growing offset per row. Widths that are already 64-aligned
/// (1920 among them, which is why `.hd1920x1080` has always worked) need no copy at all.
///
/// This mirrors `LuminancePlane` in the shared Kotlin module, which Android applies to the same
/// decision; there is no pixel-stride term because plane 0 of a 420 buffer is always one byte
/// per pixel.
static func luminanceData(
base: UnsafeRawPointer,
width: Int,
height: Int,
rowStride: Int
) -> Data {
let scannedByteCount = width * height

guard rowStride != width else {
return Data(
bytesNoCopy: UnsafeMutableRawPointer(mutating: base),
count: scannedByteCount,
deallocator: .none
)
}

var data = Data(count: scannedByteCount)
data.withUnsafeMutableBytes { destination in
guard let destination = destination.baseAddress else { return }
for row in 0..<height {
memcpy(
destination.advanced(by: row * width),
base.advanced(by: row * rowStride),
width
)
}
}
return data
}
}

Expand Down
Loading