Skip to content

Instantly share code, notes, and snippets.

@stevengoldberg
Last active June 3, 2025 04:05
Show Gist options
  • Save stevengoldberg/857dda61967f9571d9f6251880117f46 to your computer and use it in GitHub Desktop.
Save stevengoldberg/857dda61967f9571d9f6251880117f46 to your computer and use it in GitHub Desktop.
Expo Module wrapper for Mantis iOS cropping library
function CropperExample({
image,
cropperRef,
isCroppingLocked,
setIsCroppingLocked,
setCroppingRatio,
setIsCropResettable,
}) {
return (
<CropperView
uri={image.localUri}
style={{
width: image.width,
height: image.height,
}}
initialCropInfo={initialCropInfo}
ref={cropperRef}
isCroppingLocked={isCroppingLocked}
setIsCroppingLocked={setIsCroppingLocked}
setIsCropResettable={setIsCropResettable}
setCroppingRatio={setCroppingRatio}
/>
)
}
export default CropperExample
/*
A Swift Expo Module wrapping the Mantis functionality. Includes a "headless" cropping function (not yet supported by the library) which crops via an offscreen view.
*/
import ExpoModulesCore
import Mantis
import Photos
import UIKit
public class MantisModule: Module, CropViewControllerDelegate {
// MARK: – Properties for headless crop
private var headlessPromise: Promise?
private var offscreenWindow: UIWindow?
public func definition() -> ModuleDefinition {
Name("MantisModule")
View(MantisView.self) {
Prop("uri") { (view: MantisView, uri: String) in
view.uri = uri
}
Prop("initialCropInfo") { (view: MantisView, initialCropInfo: [String: Any]?) in
if let dict = initialCropInfo,
let t = view.parseTransformation(dict: dict)
{
view.initialTransformation = t
} else {
view.initialTransformation = nil
}
}
AsyncFunction("cropAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.performCrop { result in
switch result {
case .success(let data): promise.resolve(data)
case .failure(let error): promise.reject(error)
}
}
}
}
AsyncFunction("cancelAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.cancelCrop { _ in }
promise.resolve(nil)
}
}
AsyncFunction("rotateCounterClockwiseAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.rotateCounterClockwise()
promise.resolve(nil)
}
}
AsyncFunction("rotateClockwiseAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.rotateClockwise()
promise.resolve(nil)
}
}
AsyncFunction("resetAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.reset()
promise.resolve(nil)
}
}
AsyncFunction("horizontallyFlipAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.horizontallyFlip()
promise.resolve(nil)
}
}
AsyncFunction("verticallyFlipAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.verticallyFlip()
promise.resolve(nil)
}
}
AsyncFunction("setFixedRatioAsync") { (view: MantisView, ratio: Double, promise: Promise) in
Task { @MainActor in
view.setFixedRatio(ratio)
promise.resolve(nil)
}
}
AsyncFunction("setFreeRatioAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.setFreeRatio()
promise.resolve(nil)
}
}
AsyncFunction("alterCropper90DegreeAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
view.alterCropper90Degree()
promise.resolve(nil)
}
}
AsyncFunction("getCurrentAspectRatioAsync") { (view: MantisView, promise: Promise) in
Task { @MainActor in
promise.resolve(view.getCurrentAspectRatio())
}
}
AsyncFunction("autoAdjustAsync") { (view: MantisView, isActive: Bool, promise: Promise) in
Task { @MainActor in
view.autoAdjust(isActive: isActive)
promise.resolve(nil)
}
}
Events("onResizeStart", "onBecomeResettable")
}
// headless, module‐level crop function —
AsyncFunction("cropWithTransformAsync") { (
uri: String,
transformDict: [String: Any],
promise: Promise
) in
Task { @MainActor in
// 1) parse the saved transform
guard let transform = self.parseTransformation(dict: transformDict) else {
return promise.reject("E_INVALID_TRANSFORM", "Malformed transform")
}
// 2) load the full-res UIImage
guard let url = URL(string: uri),
let data = try? Data(contentsOf: url),
let image = UIImage(data: data)
else {
return promise.reject("E_LOAD_IMAGE", "Could not load image at \(uri)")
}
// 3) configure a CropViewController with your transform
var config = Config()
config.cropViewConfig.presetTransformationType = .presetInfo(info: transform)
let ratio = transform.maskFrame.width / transform.maskFrame.height
config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: ratio)
let cropVC = Mantis.cropViewController(image: image, config: config)
cropVC.delegate = self
// 4) stick it into an offscreen window so layout actually runs
let window = UIWindow(frame: UIScreen.main.bounds)
window.isHidden = false
let host = UIViewController()
window.rootViewController = host
host.view.addSubview(cropVC.view)
cropVC.view.frame = host.view.bounds
window.layoutIfNeeded()
self.offscreenWindow = window
// 5) stash the promise and fire‐and‐forget the crop call
self.headlessPromise = promise
cropVC.crop()
}
}
}
// MARK: – CropViewControllerDelegate
public func cropViewControllerDidCrop(
_ cropViewController: CropViewController,
cropped: UIImage,
transformation: Transformation,
cropInfo: CropInfo
) {
guard let promise = headlessPromise else { return }
headlessPromise = nil
// tear down our offscreen window
offscreenWindow?.isHidden = true
offscreenWindow = nil
// encode & save the JPEG
guard let data = cropped.jpegData(compressionQuality: 1.0) else {
return promise.reject("E_ENCODE_FAILED", "Failed to encode crop")
}
let docs = FileManager.default
.urls(for: .documentDirectory, in: .userDomainMask)[0]
let outURL = docs.appendingPathComponent(UUID().uuidString + ".jpg")
do {
try data.write(to: outURL)
let r = cropInfo.cropRegion
promise.resolve([
"uri": outURL.absoluteString,
"width": Int(cropped.size.width),
"height": Int(cropped.size.height),
"transformation": buildTransformationDict(transformation),
"cropRegion": [
"topLeft": ["x": r.topLeft.x, "y": r.topLeft.y],
"topRight": ["x": r.topRight.x, "y": r.topRight.y],
"bottomLeft": ["x": r.bottomLeft.x, "y": r.bottomLeft.y],
"bottomRight": ["x": r.bottomRight.x,"y": r.bottomRight.y]
]
])
} catch {
promise.reject("E_SAVE_FAILED", "Failed to save cropped image")
}
}
public func cropViewControllerDidFailToCrop(
_ cropViewController: CropViewController,
original: UIImage
) {
if let promise = headlessPromise {
headlessPromise = nil
offscreenWindow?.isHidden = true
offscreenWindow = nil
promise.reject("E_CROP_FAILED", "Mantis failed to crop")
}
}
public func cropViewControllerDidCancel(
_ cropViewController: CropViewController,
original: UIImage
) {
if let promise = headlessPromise {
headlessPromise = nil
offscreenWindow?.isHidden = true
offscreenWindow = nil
promise.reject("E_USER_CANCELLED", "User cancelled crop")
}
}
// MARK: – Transformation helpers
func parseTransformation(dict: [String: Any]) -> Transformation? {
guard
let offsetX = dict["offsetX"] as? CGFloat,
let offsetY = dict["offsetY"] as? CGFloat,
let rotation = dict["rotation"] as? CGFloat,
let scale = dict["scale"] as? CGFloat,
let isManual = dict["isManuallyZoomed"] as? Bool,
let imf = dict["initialMaskFrame"] as? [String: CGFloat],
let mf = dict["maskFrame"] as? [String: CGFloat],
let cb = dict["cropWorkbenchViewBounds"] as? [String: CGFloat],
let hf = dict["horizontallyFlipped"] as? Bool,
let vf = dict["verticallyFlipped"] as? Bool
else {
return nil
}
return Transformation(
offset: CGPoint(x: offsetX, y: offsetY),
rotation: rotation,
scale: scale,
isManuallyZoomed: isManual,
initialMaskFrame: rectFromDict(imf),
maskFrame: rectFromDict(mf),
cropWorkbenchViewBounds: rectFromDict(cb),
horizontallyFlipped: hf,
verticallyFlipped: vf
)
}
func buildTransformationDict(_ t: Transformation) -> [String: Any] {
return [
"offsetX": t.offset.x,
"offsetY": t.offset.y,
"rotation": t.rotation,
"scale": t.scale,
"isManuallyZoomed": t.isManuallyZoomed,
"initialMaskFrame": rectToDict(t.initialMaskFrame),
"maskFrame": rectToDict(t.maskFrame),
"cropWorkbenchViewBounds": rectToDict(t.cropWorkbenchViewBounds),
"horizontallyFlipped": t.horizontallyFlipped,
"verticallyFlipped": t.verticallyFlipped
]
}
private func rectFromDict(_ d: [String: CGFloat]) -> CGRect {
return CGRect(
x: d["x"] ?? 0,
y: d["y"] ?? 0,
width: d["width"] ?? 0,
height: d["height"] ?? 0
)
}
private func rectToDict(_ r: CGRect) -> [String: CGFloat] {
return [
"x": r.origin.x,
"y": r.origin.y,
"width": r.size.width,
"height": r.size.height
]
}
}
/* TypeScript module exposing the Mantis functions */
import { requireNativeModule } from 'expo-modules-core'
import * as MantisTypes from './Mantis.types'
const MantisModule = requireNativeModule('MantisModule')
export async function cropAsync(viewTag: number) {
return await MantisModule.cropAsync(viewTag)
}
export async function cancelAsync(viewTag: number) {
return await MantisModule.cancelAsync(viewTag)
}
export function rotateCounterClockwise(viewTag: number) {
return MantisModule.rotateCounterClockwise(viewTag)
}
export function rotateClockwise(viewTag: number) {
return MantisModule.rotateClockwise(viewTag)
}
export function reset(viewTag: number) {
return MantisModule.reset(viewTag)
}
export function horizontallyFlip(viewTag: number) {
return MantisModule.horizontallyFlip(viewTag)
}
export function verticallyFlip(viewTag: number) {
return MantisModule.verticallyFlip(viewTag)
}
export function setFixedRatio(viewTag: number, ratio: number) {
return MantisModule.setFixedRatio(viewTag, ratio)
}
export function setFreeRatio(viewTag: number) {
return MantisModule.setFreeRatio(viewTag)
}
export function alterCropper90Degree(viewTag: number) {
return MantisModule.alterCropper90Degree(viewTag)
}
export function getCurrentAspectRatio(viewTag: number): number {
return MantisModule.getCurrentAspectRatio(viewTag)
}
export async function autoAdjustAsync(viewTag: number, isActive: boolean) {
return await MantisModule.autoAdjustAsync(viewTag, isActive)
}
export async function cropWithTransformAsync(
uri: string,
transformation: MantisModule.Transformation
) {
return await MantisModule.cropWithTransformAsync(uri, transformation)
}
/* React Native view wrapping the Mantis view */
import { requireNativeViewManager } from 'expo-modules-core'
import { forwardRef, useImperativeHandle, useRef } from 'react'
import { View, TouchableOpacity } from 'react-native'
import LockIcon from 'components/icons/lock'
import { useTheme } from 'helpers/hooks'
const NativeCropperView = requireNativeViewManager('MantisModule')
export const CropperView = forwardRef(
(
{
isCroppingLocked,
setIsCroppingLocked,
uri,
setIsCropResettable,
setCroppingRatio,
...props
},
ref
) => {
const nativeRef = useRef(null)
const { colors } = useTheme()
const handleLock = async () => {
if (isCroppingLocked) {
setIsCroppingLocked(false)
await nativeRef.current.setFreeRatioAsync()
} else {
const currentRatio =
await nativeRef.current.getCurrentAspectRatioAsync()
await nativeRef.current.setFixedRatioAsync(currentRatio)
setIsCroppingLocked(true)
}
}
const onResizeStart = () => {
if (!isCroppingLocked) {
setCroppingRatio(null)
}
setIsCropResettable(true)
}
useImperativeHandle(ref, () => ({
crop: async () => {
try {
if (!nativeRef.current) {
console.error('No nativeRef.current available')
return
}
return await nativeRef.current.cropAsync()
} catch (error) {
console.error('Crop error:', error)
throw error
}
},
cancel: async () => {
if (!nativeRef.current) return null
return await nativeRef.current.cancelAsync()
},
rotateCounterClockwise: async () => {
if (!nativeRef.current) return
setIsCropResettable(true)
return await nativeRef.current.rotateCounterClockwiseAsync()
},
rotateClockwise: async () => {
if (!nativeRef.current) return
setIsCropResettable(true)
return await nativeRef.current.rotateClockwiseAsync()
},
reset: async () => {
if (!nativeRef.current) return
setIsCroppingLocked(true)
setIsCropResettable(false)
await nativeRef.current.resetAsync()
const currentRatio =
await nativeRef.current.getCurrentAspectRatioAsync()
const roundedRatio = Math.round(currentRatio * 100) / 100
setCroppingRatio(roundedRatio)
},
horizontallyFlip: async () => {
if (!nativeRef.current) return
setIsCropResettable(true)
return await nativeRef.current.horizontallyFlipAsync()
},
verticallyFlip: async () => {
if (!nativeRef.current) return
setIsCropResettable(true)
return await nativeRef.current.verticallyFlipAsync()
},
setFixedRatio: async (ratio) => {
if (!nativeRef.current) return
setIsCroppingLocked(true)
setIsCropResettable(true)
return await nativeRef.current.setFixedRatioAsync(ratio)
},
setFreeRatio: async () => {
if (!nativeRef.current) return
setIsCroppingLocked(false)
return await nativeRef.current.setFreeRatioAsync()
},
alterCropper90Degree: async () => {
if (!nativeRef.current) return
setIsCropResettable(true)
return await nativeRef.current.alterCropper90DegreeAsync()
},
getCurrentAspectRatio: async () => {
if (!nativeRef.current) return 1.0
return await nativeRef.current.getCurrentAspectRatioAsync()
},
autoAdjust: async (isActive) => {
if (!nativeRef.current) return
return await nativeRef.current.autoAdjustAsync(isActive)
},
}))
return (
<View style={{ flex: 1 }}>
<NativeCropperView
uri={uri}
{...props}
ref={nativeRef}
onResizeStart={onResizeStart}
/>
<TouchableOpacity
onPress={() => handleLock()}
type="transparent"
style={{
position: 'absolute',
top: 6,
right: 6,
borderRadius: 8,
backgroundColor: colors.cream['50'],
padding: 6,
borderWidth: 1,
borderColor: colors.offBlack['100'],
justifyContent: 'center',
alignItems: 'center',
opacity: 0.6,
}}
>
<LockIcon
isLocked={isCroppingLocked}
size={20}
color={
isCroppingLocked
? colors.button.dark
: colors.button.light
}
/>
</TouchableOpacity>
</View>
)
}
)
CropperView.displayName = 'CropperView'
/* Swift Expo View used for embedding the Mantis cropper into an RN app */
import UIKit
import Mantis
import ExpoModulesCore
import Photos
public class MantisView: ExpoView, CropViewControllerDelegate {
private var cropViewController: CropViewController?
private var originalImage: UIImage?
private var pendingCropCompletion: ((Result<[String: Any], Error>) -> Void)?
private var pendingImage: UIImage?
private var currentCropInfo: CropInfo?
private var originalAspectRatio: CGFloat = 0
public var initialTransformation: Transformation? {
didSet {
if let img = pendingImage {
setupCropper(with: img)
}
}
}
@objc
var uri: String = "" {
didSet {
loadImage()
}
}
@objc
var initialCropInfo: [String: Any]? {
didSet {
if let dict = initialCropInfo {
initialTransformation = parseTransformation(dict: dict)
}
}
}
public let onResizeStart = EventDispatcher()
public let onBecomeResettable = EventDispatcher()
private func loadImage() {
guard let url = URL(string: uri) else {
return
}
DispatchQueue.global(qos: .userInitiated).async {
if let data = try? Data(contentsOf: url),
let image = UIImage(data: data) {
DispatchQueue.main.async { [weak self] in
self?.originalImage = image
self?.setupCropper(with: image)
}
}
}
}
private func setupCropper(with img: UIImage) {
// Convert to 8-bit if necessary
var imageToUse = img
if let cgImage = img.cgImage, cgImage.bitsPerComponent != 8 {
if let converted = convertTo8Bit(image: img) {
imageToUse = converted
}
}
// Remove existing crop view controller if any
cropViewController?.view.removeFromSuperview()
cropViewController = nil
let buttonColorLight = UIColor(
red: 33 / 255,
green: 185 / 255,
blue: 255 / 255,
alpha: 1
)
let buttonColorStandard = UIColor(
red: 0.0 / 255.0,
green: 148.0 / 255.0,
blue: 217.0 / 255.0,
alpha: 0.75
)
let cream50 = UIColor(
red: 251.0 / 255.0,
green: 251.0 / 255.0,
blue: 249.0 / 255.0,
alpha: 1.0
)
let cream100 = UIColor(
red: 234 / 255,
green: 232 / 255,
blue: 227 / 255,
alpha: 0.7
)
var config = Config()
var rotationDialConfig = RotationDialConfig()
rotationDialConfig.smallScaleColor = cream50
rotationDialConfig.bigScaleColor = buttonColorStandard
rotationDialConfig.numberColor = buttonColorLight
config.cropViewConfig.builtInRotationControlViewType = .rotationDial(config: rotationDialConfig)
config.cropToolbarConfig.mode = .embedded
config.showAttachedCropToolbar = false
config.cropViewConfig.cropAuxiliaryIndicatorConfig.borderNormalColor = cream100
config.cropViewConfig.cropAuxiliaryIndicatorConfig.borderHintColor = buttonColorLight
config.cropViewConfig.cropAuxiliaryIndicatorConfig.cornerHandleColor = buttonColorStandard
config.cropViewConfig.cropAuxiliaryIndicatorConfig.gridMainColor = cream100
// Calculate the aspect ratio to use
let aspectRatioToUse: CGFloat
if let transform = initialTransformation {
config.cropViewConfig.presetTransformationType = .presetInfo(info: transform)
// Use the aspect ratio from the initial transformation's mask frame
aspectRatioToUse = transform.maskFrame.width / transform.maskFrame.height
} else {
config.cropViewConfig.presetTransformationType = .none
// Use the original image's aspect ratio
aspectRatioToUse = img.size.width / img.size.height
}
// Store the aspect ratio we're using
self.originalAspectRatio = aspectRatioToUse
// Lock the cropper to this aspect ratio
config.presetFixedRatioType = .alwaysUsingOnePresetFixedRatio(ratio: aspectRatioToUse)
let cropVC = Mantis.cropViewController(
image: imageToUse,
config: config
)
cropVC.delegate = self
self.cropViewController = cropVC
// Ensure we're on the main thread
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// Add the crop view controller's view
self.addSubview(cropVC.view)
cropVC.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cropVC.view.topAnchor.constraint(equalTo: self.topAnchor),
cropVC.view.bottomAnchor.constraint(equalTo: self.bottomAnchor),
cropVC.view.leadingAnchor.constraint(equalTo: self.leadingAnchor),
cropVC.view.trailingAnchor.constraint(equalTo: self.trailingAnchor)
])
// Force layout update
self.layoutIfNeeded()
}
}
@MainActor
public func performCrop(completion: @escaping (Result<[String: Any], Error>) -> Void) {
guard let cropVC = cropViewController else {
completion(.failure(CropError.noCropViewController))
return
}
pendingCropCompletion = completion
// Before calling crop
if let image = self.originalImage {
print("Original image size: \(image.size.width) x \(image.size.height)")
print("Image orientation: \(image.imageOrientation.rawValue)")
}
// If you have access to the crop rect or transformation:
print("Initial transformation: \(String(describing: self.initialTransformation))")
cropVC.crop()
}
@MainActor
public func cancelCrop(completion: @escaping (Result<[String: Any], Error>) -> Void) {
pendingCropCompletion = completion
cropViewController?.didSelectCancel()
}
// MARK: - CropViewControllerDelegate
public func cropViewControllerDidCrop(
_ cropViewController: CropViewController,
cropped: UIImage,
transformation: Transformation,
cropInfo: CropInfo
) {
// 1. Convert the cropped image to JPEG
guard let data = cropped.jpegData(compressionQuality: 1.0) else {
pendingCropCompletion?(.failure(CropError.failedToEncode))
pendingCropCompletion = nil
return
}
// 2. Save to documents directory (not temp directory)
let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let filePath = docsDir.appendingPathComponent(UUID().uuidString + ".jpg")
do {
try data.write(to: filePath)
let cropRegion = cropInfo.cropRegion
// 3. Build result object with the same structure as the old module
let result: [String: Any] = [
"uri": filePath.absoluteString,
"width": Int(cropped.size.width),
"height": Int(cropped.size.height),
"transformation": buildTransformationDict(transformation),
"cropRegion": [
"topLeft": ["x": cropRegion.topLeft.x, "y": cropRegion.topLeft.y],
"topRight": ["x": cropRegion.topRight.x, "y": cropRegion.topRight.y],
"bottomLeft": ["x": cropRegion.bottomLeft.x, "y": cropRegion.bottomLeft.y],
"bottomRight": ["x": cropRegion.bottomRight.x, "y": cropRegion.bottomRight.y],
]
]
pendingCropCompletion?(.success(result))
} catch {
pendingCropCompletion?(.failure(CropError.failedToSave))
}
pendingCropCompletion = nil
}
public func cropViewControllerDidCancel(
_ cropViewController: CropViewController,
original: UIImage
) {
print("cropViewControllerDidCancel called")
pendingCropCompletion?(.failure(CropError.userCancelled))
pendingCropCompletion = nil
}
public func cropViewControllerDidFailToCrop(
_ cropViewController: CropViewController,
original: UIImage
) {
print("cropViewControllerDidFailToCrop called")
pendingCropCompletion?(.failure(CropError.failedToCrop))
pendingCropCompletion = nil
}
public func cropViewControllerDidEndResize(_ cropViewController: CropViewController, original: UIImage, cropInfo: CropInfo) {
currentCropInfo = cropInfo
}
public func cropViewControllerDidBeginResize(_ cropViewController: CropViewController) {
print("Swift: resize start")
onResizeStart([:])
}
public func cropViewController(_ cropViewController: CropViewController, didBecomeResettable resettable: Bool) {
onBecomeResettable(["resettable": resettable])
}
// MARK: - Transformation Helpers
public func parseTransformation(dict: [String: Any]) -> Transformation? {
guard
let offsetX = dict["offsetX"] as? CGFloat,
let offsetY = dict["offsetY"] as? CGFloat,
let rotation = dict["rotation"] as? CGFloat,
let scale = dict["scale"] as? CGFloat,
let isManual = dict["isManuallyZoomed"] as? Bool,
let initialMaskFrameDict = dict["initialMaskFrame"] as? [String: CGFloat],
let maskFrameDict = dict["maskFrame"] as? [String: CGFloat],
let workbenchDict = dict["cropWorkbenchViewBounds"] as? [String: CGFloat],
let horizontallyFlipped = dict["horizontallyFlipped"] as? Bool,
let verticallyFlipped = dict["verticallyFlipped"] as? Bool
else {
return nil
}
let offset = CGPoint(x: offsetX, y: offsetY)
let initialFrame = rectFromDict(initialMaskFrameDict)
let maskFrame = rectFromDict(maskFrameDict)
let workbenchFrame = rectFromDict(workbenchDict)
return Transformation(
offset: offset,
rotation: rotation,
scale: scale,
isManuallyZoomed: isManual,
initialMaskFrame: initialFrame,
maskFrame: maskFrame,
cropWorkbenchViewBounds: workbenchFrame,
horizontallyFlipped: horizontallyFlipped,
verticallyFlipped: verticallyFlipped
)
}
private func rectFromDict(_ dict: [String: CGFloat]) -> CGRect {
let x = dict["x"] ?? 0
let y = dict["y"] ?? 0
let w = dict["width"] ?? 0
let h = dict["height"] ?? 0
return CGRect(x: x, y: y, width: w, height: h)
}
private func buildTransformationDict(_ t: Transformation) -> [String: Any] {
return [
"offsetX": t.offset.x,
"offsetY": t.offset.y,
"rotation": t.rotation,
"scale": t.scale,
"isManuallyZoomed": t.isManuallyZoomed,
"initialMaskFrame": rectToDict(t.initialMaskFrame),
"maskFrame": rectToDict(t.maskFrame),
"cropWorkbenchViewBounds": rectToDict(t.cropWorkbenchViewBounds),
"horizontallyFlipped": t.horizontallyFlipped,
"verticallyFlipped": t.verticallyFlipped
]
}
private func rectToDict(_ rect: CGRect) -> [String: CGFloat] {
return [
"x": rect.origin.x,
"y": rect.origin.y,
"width": rect.size.width,
"height": rect.size.height
]
}
@MainActor
public func rotateCounterClockwise() {
cropViewController?.didSelectCounterClockwiseRotate()
}
@MainActor
public func rotateClockwise() {
cropViewController?.didSelectClockwiseRotate()
}
@MainActor
public func reset() {
// First restore the original aspect ratio
cropViewController?.didSelectRatio(ratio: originalAspectRatio)
// Then perform the reset
cropViewController?.didSelectReset()
}
@MainActor
public func horizontallyFlip() {
cropViewController?.didSelectHorizontallyFlip()
}
@MainActor
public func verticallyFlip() {
cropViewController?.didSelectVerticallyFlip()
}
@MainActor
public func setFixedRatio(_ ratio: Double) {
cropViewController?.didSelectRatio(ratio: ratio)
}
@MainActor
public func setFreeRatio() {
cropViewController?.didSelectFreeRatio()
}
@MainActor
public func alterCropper90Degree() {
cropViewController?.didSelectAlterCropper90Degree()
}
@MainActor
public func getCurrentAspectRatio() -> Double {
guard let cropInfo = currentCropInfo else { return 1.0 }
return Double(cropInfo.cropSize.width / cropInfo.cropSize.height)
}
@MainActor
public func autoAdjust(isActive: Bool) {
cropViewController?.didSelectAutoAdjust(nil, isActive: isActive)
}
func convertTo8Bit(image: UIImage) -> UIImage? {
guard let cgImage = image.cgImage else { return nil }
let width = cgImage.width
let height = cgImage.height
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue
let context = CGContext(
data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: 0,
space: colorSpace,
bitmapInfo: bitmapInfo
)
guard let ctx = context else { return nil }
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
guard let newCGImage = ctx.makeImage() else { return nil }
return UIImage(cgImage: newCGImage)
}
}
enum CropError: Error {
case failedToEncode
case failedToSave
case userCancelled
case noCropViewController
case failedToCrop
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment