Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preview rotation #226

Merged
merged 3 commits into from Mar 18, 2024
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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