Skip to content

Commit

Permalink
Preview rotation (#226)
Browse files Browse the repository at this point in the history
  • Loading branch information
muukii committed Mar 18, 2024
1 parent 508b9fd commit ca398c0
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 214 deletions.
7 changes: 7 additions & 0 deletions Dev/Sources/SwiftUIDemo/ContentView.swift
Expand Up @@ -15,6 +15,13 @@ struct ContentView: View {
VStack {

Form {

NavigationLink("ImagePreviewView") {
DemoCropView2(editingStack: {
horizontalStack
})
}

NavigationLink("Isolated", destination: IsolatedEditinView())

if #available(iOS 16, *) {
Expand Down
16 changes: 0 additions & 16 deletions Sources/BrightroomEngine/Core/EditingCrop.swift
Expand Up @@ -352,19 +352,3 @@ public struct EditingCrop: Equatable {
}
*/
}

extension CIImage {
func cropped(to _cropRect: EditingCrop) -> CIImage {

let targetImage = self
var cropRect = _cropRect.cropExtent

cropRect.origin.y = targetImage.extent.height - cropRect.minY - cropRect.height

let croppedImage =
targetImage
.cropped(to: cropRect)

return croppedImage
}
}
214 changes: 35 additions & 179 deletions Sources/BrightroomEngine/Core/EditingStack.swift
Expand Up @@ -29,41 +29,6 @@ public enum EditingStackError: Error {
case unableToCreateRendererInLoading
}

private enum MTLImageCreationError: Error {
case imageTooBig
}

extension MTLDevice {
fileprivate func supportsImage(size: CGSize) -> Bool {
#if DEBUG
switch MTLGPUFamily.apple1 {
case .apple1,
.apple2,
.apple3,
.apple4,
.apple5,
.apple6,
.apple7,
.apple8,
.apple9,
.common1,
.common2,
.common3,
.mac1,
.mac2,
.macCatalyst1,
.macCatalyst2,
.metal3:
break
@unknown default: //If a warning is triggered here, please check https://developer.apple.com/metal/Metal-Feature-Set-Tables.pdf for a possibly new value in the Maximum 2D texture width and height table.
break
}
#endif
let maxSideSize: CGFloat = self.supportsFamily(.apple3) ? 16384 : 8192
return size.width <= maxSideSize && size.height <= maxSideSize
}
}

/// A stateful object that manages current editing status from original image.
/// And supports rendering a result image.
///
Expand Down Expand Up @@ -223,19 +188,6 @@ open class EditingStack: Hashable, StoreComponentType {
currentEdit = history.popLast() ?? initialEditing
}

/**
Returns a CIImage applied cropping in current editing.
For previewing image
*/
public func makeCroppedImage() -> CIImage {
editingSourceImage.cropped(
to: currentEdit.crop.scaledWithPixelPerfect(
maxPixelSize: max(editingSourceImage.extent.width, editingSourceImage.extent.height)
)
)
}

}

public fileprivate(set) var hasStartedEditing = false
Expand All @@ -258,6 +210,8 @@ open class EditingStack: Hashable, StoreComponentType {

public let options: Options

private let mtlDevice = MTLCreateSystemDefaultDevice()

public let imageProvider: ImageProvider

private let filterPresets: [FilterPreset]
Expand Down Expand Up @@ -380,20 +334,16 @@ open class EditingStack: Hashable, StoreComponentType {

assert(editingSourceCGImage.colorSpace != nil)

let device = MTLCreateSystemDefaultDevice()

/// resized
let _editingSourceCIImage: CIImage = _makeCIImage(
source: editingSourceCGImage,
let _editingSourceCIImage: CIImage = editingSourceCGImage._makeCIImage(
orientation: metadata.orientation,
device: device,
device: self.mtlDevice,
usesMTLTexture: self.options.usesMTLTextureForEditingImage
)

let _thumbnailImage: CIImage = _makeCIImage(
source: thumbnailCGImage,
let _thumbnailImage: CIImage = thumbnailCGImage._makeCIImage(
orientation: metadata.orientation,
device: device,
device: self.mtlDevice,
usesMTLTexture: self.options.usesMTLTextureForEditingImage
)

Expand Down Expand Up @@ -454,6 +404,35 @@ open class EditingStack: Hashable, StoreComponentType {
}
}

/**
Returns a CIImage applied cropping in current editing.
For previewing image
*/
public func makeCroppedCIImage(loadedState: State.Loaded) -> CIImage {

do {
let crop = loadedState.currentEdit.crop
let image = loadedState.editingSourceCGImage
let imageSize = image.size

let scaledCrop = crop.scaledWithPixelPerfect(
maxPixelSize: max(imageSize.width, imageSize.height)
)

return try image
.croppedWithColorspace(
to: scaledCrop.cropExtent, adjustmentAngleRadians: scaledCrop.aggregatedRotation.radians)
._makeCIImage(
orientation: loadedState.metadata.orientation,
device: mtlDevice,
usesMTLTexture: options.usesMTLTextureForEditingImage
)
} catch {
return .init(color: .gray)
}
}

deinit {
EngineLog.debug("[EditingStack] deinit")
}
Expand Down Expand Up @@ -676,126 +655,3 @@ open class EditingStack: Hashable, StoreComponentType {

}

/// TODO: As possible, creates CIImage from MTLTexture
/// 16bits image can't be MTLTexture with MTKTextureLoader.
/// https://stackoverflow.com/questions/54710592/cant-load-large-jpeg-into-a-mtltexture-with-mtktextureloader
private func makeMTLTexture(from cgImage: CGImage, device: MTLDevice) throws -> MTLTexture {
guard device.supportsImage(size: cgImage.size) else {
throw MTLImageCreationError.imageTooBig
}

#if true
let loader = MTKTextureLoader(device: device)
let texture = try loader.newTexture(cgImage: cgImage, options: [:])
return texture
#else

// Here does not work well.

let textureDescriptor = MTLTextureDescriptor()

textureDescriptor.pixelFormat = .rgba16Uint
textureDescriptor.width = cgImage.width
textureDescriptor.height = cgImage.height

let texture = try device.makeTexture(descriptor: textureDescriptor).unwrap(
orThrow: "Failed to create MTLTexture"
)

let context = try CGContext.makeContext(for: cgImage)
.perform { context in
let flip = CGAffineTransform(a: 1, b: 0, c: 0, d: -1, tx: 0, ty: CGFloat(cgImage.height))
context.concatenate(flip)
context.draw(
cgImage,
in: CGRect(x: 0, y: 0, width: CGFloat(cgImage.width), height: CGFloat(cgImage.height))
)
}

let data = try context.data.unwrap()

texture.replace(
region: MTLRegionMake2D(0, 0, cgImage.width, cgImage.height),
mipmapLevel: 0,
withBytes: data,
bytesPerRow: 8 * cgImage.width
)

return texture
#endif

}

private func _makeCIImage(
source cgImage: CGImage,
orientation: CGImagePropertyOrientation,
device: MTLDevice?,
usesMTLTexture: Bool
) -> CIImage {

let colorSpace = cgImage.colorSpace ?? CGColorSpaceCreateDeviceRGB()

func createFromCGImage() -> CIImage {
return CIImage(
cgImage: cgImage
)
.oriented(orientation)
}

func createFromMTLTexture(device: MTLDevice) throws -> CIImage {
let thumbnailTexture = try makeMTLTexture(
from: cgImage,
device: device
)

let ciImage = try CIImage(
mtlTexture: thumbnailTexture,
options: [.colorSpace: colorSpace]
)
.map {
$0.transformed(by: .init(scaleX: 1, y: -1))
}.map {
$0.transformed(by: .init(translationX: 0, y: $0.extent.height))
}
.map {
$0.oriented(orientation)
}
.unwrap()

EngineLog.debug(.stack, "Load MTLTexture")

return ciImage
}

if usesMTLTexture {
assert(device != nil)
}

if usesMTLTexture, let device = device {

do {
// TODO: As possible, creates CIImage from MTLTexture
// 16bits image can't be MTLTexture with MTKTextureLoader.
// https://stackoverflow.com/questions/54710592/cant-load-large-jpeg-into-a-mtltexture-with-mtktextureloader
return try createFromMTLTexture(device: device)
} catch {
EngineLog.debug(
.stack,
"Unable to create MTLTexutre, fallback to CIImage from CGImage.\n\(cgImage)"
)

return createFromCGImage()
}
} else {

if usesMTLTexture, device == nil {
EngineLog.error(
.stack,
"MTLDevice not found, fallback to using CGImage to create CIImage."
)
}

return createFromCGImage()
}

}
10 changes: 0 additions & 10 deletions Sources/BrightroomEngine/Engine/BrightRoomImageRenderer.swift
Expand Up @@ -238,16 +238,6 @@ public final class BrightRoomImageRenderer {
resizedImage = try croppedImage.resized(maxPixelSize: maxPixelSize)
}

/*
===
===
===
*/
EngineLog.debug(.renderer, "Rotation")

// // TODO: should be better that combines crop and rotation into single operation.
// let rotatedImage = try resizedImage.rotated(rotation: crop.rotation)

return .init(cgImage: resizedImage, options: options, engine: .coreGraphics)
}

Expand Down

0 comments on commit ca398c0

Please sign in to comment.