Path: blob/main/Signal/QRCodes/Bitmaps/Bitmaps+Image.swift
1 views
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import CoreGraphics
import Foundation
import SignalServiceKit
extension Bitmaps {
/// A bitmap representation of an image.
///
/// Bitmaps have origin at the bottom-left, matching the default
/// CoreGraphics context orientation.
struct Image {
struct Pixel: Equatable {
let r: UInt8
let g: UInt8
let b: UInt8
let a: UInt8
}
/// Width of the image, in pixels.
let width: Int
/// Height of the image, in pixels.
let height: Int
/// Number of bytes per row in the image. Note that a row may contain
/// padding as well as pixel data, in order to maintain byte alignment.
/// - SeeAlso
/// https://stackoverflow.com/a/25706554/10901655
/// - SeeAlso
/// https://stackoverflow.com/questions/31212402/getting-pixel-data-from-cgimageref-contains-extra-bytes
let bytesPerRow: Int
/// The number of bytes used to represent each pixel - one each for the
/// `{R,G,B,A}` tuple.
private let bytesPerPixel: Int = 4
/// The raw bytes of the image. Pixels are represented as `{R,G,B,A}` byte
/// tuples. Note that these bytes may include alignment padding as well as
/// pixel bytes.
private let bytes: [UInt8]
#if TESTABLE_BUILD
init(width: Int, height: Int, rawBytes: [UInt8]) {
self.width = width
self.height = height
self.bytesPerRow = 4 * width
self.bytes = rawBytes
}
#endif
/// Create a bitmap of the given image.
///
/// Bitmaps, like default CoreGraphics contexts, have their origin in
/// the bottom-left. However, most images (including those coming from
/// UIKit or CoreImage) have their origin in the upper-left.
///
/// This method assumes the given image has its origin in the upper-left
/// and inverts it accordingly when creating the bitmap.
init?(cgImage: CGImage) {
guard cgImage.width > 0, cgImage.height > 0 else {
owsFailDebug("Invalid image size! \(cgImage.width), \(cgImage.height)")
return nil
}
guard cgImage.bytesPerRow > 0 else {
owsFailDebug("Invalid image bytes per row! \(cgImage.bytesPerRow)")
return nil
}
self.width = cgImage.width
self.height = cgImage.height
self.bytesPerRow = cgImage.bytesPerRow
var imageBytes = [UInt8](repeating: 0, count: height * bytesPerRow)
guard
let cgContext = CGContext(
data: &imageBytes,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue,
)
else {
owsFailDebug("Failed to create CGContext!")
return nil
}
// Flip the context so as to match the image orientation.
cgContext.scaleBy(x: 1, y: -1)
cgContext.translateBy(x: 0, y: -CGFloat(height))
cgContext.draw(cgImage, in: cgImage.boundingRect)
self.bytes = imageBytes
}
/// Whether there is a pixel at the given point with non-zero alpha. Returns
/// `false` if the point is out of bounds.
func hasVisiblePixel(at point: Point) -> Bool {
guard let pixel = pixel(at: point) else {
return false
}
return pixel.a > 0
}
/// Get the pixel at the given point.
/// - Returns
/// The pixel, or `nil` if the point is out of bounds.
func pixel(at point: Point) -> Pixel? {
guard point.x < width, point.y < height else {
return nil
}
let pixelRowStartOffset = point.y * bytesPerRow
let pixelOffsetInRow = point.x * bytesPerPixel
let pixelStartOffset = pixelRowStartOffset + pixelOffsetInRow
guard pixelStartOffset + 3 < bytes.count else {
return nil
}
return Pixel(
r: bytes[pixelStartOffset],
g: bytes[pixelStartOffset + 1],
b: bytes[pixelStartOffset + 2],
a: bytes[pixelStartOffset + 3],
)
}
}
}
private extension CGImage {
/// Represents the bounds of the full image.
var boundingRect: CGRect {
return CGRect(x: 0, y: 0, width: width, height: height)
}
}