From 96ee654aa9b92c43f35699ae84e43ae9d5f0181f Mon Sep 17 00:00:00 2001 From: Muukii Date: Wed, 31 Mar 2021 00:25:15 +0900 Subject: [PATCH] Uses CoreGraphics only when editing contains only cropping. (#83) * Patch * Use CoreGraphics when crop only * Update * Fixing colorspace issue * Fix orientation --- Brightroom.xcodeproj/project.pbxproj | 140 ++++---- .../{IMG_5529.HEIC => orientation_down.HEIC} | Bin ...33.HEIC => orientation_down_mirrored.HEIC} | Bin .../{IMG_5530.HEIC => orientation_left.HEIC} | Bin ...32.HEIC => orientation_left_mirrored.HEIC} | Bin .../{IMG_5528.HEIC => orientation_right.HEIC} | Bin ...4.HEIC => orientation_right_mirrored.HEIC} | Bin .../{IMG_5531.HEIC => orientation_up.HEIC} | Bin ...5535.HEIC => orientation_up_mirrored.HEIC} | Bin .../ColorCube/ColorCubeHelper.swift | 2 +- .../DataSource/ImageSource.swift | 61 ++-- .../Engine/CoreGraphics+.swift | 189 ++++++++++ .../Engine/ImageRenderer.swift | 333 +++++++++--------- .../BrightroomEngine/Engine/ImageTool.swift | 76 ++-- .../BrightroomEngine/Library/Optional+.swift | 63 ++++ Sources/Demo/Contents/Components.swift | 34 +- .../DemoBuiltInEditorViewController.swift | 2 +- .../Contents/DemoCropMenuViewController.swift | 2 +- .../Contents/DemoMaskingViewController.swift | 2 +- .../ImitationTinderViewController.swift | 2 +- Sources/SwiftUIDemo/ContentView.swift | 18 +- Sources/SwiftUIDemo/DemoCropView.swift | 2 +- .../BrightroomEngineTests/LoadingTests.swift | 16 +- .../RendererOrientationTests.swift | 106 ++++++ .../BrightroomEngineTests/RendererTests.swift | 18 +- 25 files changed, 740 insertions(+), 326 deletions(-) rename Bundle/Images/{IMG_5529.HEIC => orientation_down.HEIC} (100%) rename Bundle/Images/{IMG_5533.HEIC => orientation_down_mirrored.HEIC} (100%) rename Bundle/Images/{IMG_5530.HEIC => orientation_left.HEIC} (100%) rename Bundle/Images/{IMG_5532.HEIC => orientation_left_mirrored.HEIC} (100%) rename Bundle/Images/{IMG_5528.HEIC => orientation_right.HEIC} (100%) rename Bundle/Images/{IMG_5534.HEIC => orientation_right_mirrored.HEIC} (100%) rename Bundle/Images/{IMG_5531.HEIC => orientation_up.HEIC} (100%) rename Bundle/Images/{IMG_5535.HEIC => orientation_up_mirrored.HEIC} (100%) create mode 100644 Sources/BrightroomEngine/Engine/CoreGraphics+.swift create mode 100644 Sources/BrightroomEngine/Library/Optional+.swift create mode 100644 Tests/BrightroomEngineTests/RendererOrientationTests.swift diff --git a/Brightroom.xcodeproj/project.pbxproj b/Brightroom.xcodeproj/project.pbxproj index ae41d467..6fbf02fa 100644 --- a/Brightroom.xcodeproj/project.pbxproj +++ b/Brightroom.xcodeproj/project.pbxproj @@ -19,6 +19,9 @@ 4B0BDCA1217D663D001098B9 /* ClassicImageEditClarityControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0BDCA0217D663D001098B9 /* ClassicImageEditClarityControl.swift */; }; 4B0BDCA3217D676D001098B9 /* ClassicImageEditSharpenControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0BDCA2217D676D001098B9 /* ClassicImageEditSharpenControl.swift */; }; 4B112F15216C9B5400EF8E25 /* ImageTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B112F14216C9B5400EF8E25 /* ImageTool.swift */; }; + 4B135F6E26136B5D003B5152 /* RendererOrientationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B135F6D26136B5D003B5152 /* RendererOrientationTests.swift */; }; + 4B135F762613767E003B5152 /* CoreGraphics+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B135F752613767E003B5152 /* CoreGraphics+.swift */; }; + 4B135F7B2613769C003B5152 /* Optional+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B135F7A2613769C003B5152 /* Optional+.swift */; }; 4B1E94B12175FB5800E9DFB8 /* ClassicImageEditStepSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9D456E216DEE95003A240A /* ClassicImageEditStepSlider.swift */; }; 4B1E951C21764DE700E9DFB8 /* ClassicImageEditGaussianBlurControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E951B21764DE700E9DFB8 /* ClassicImageEditGaussianBlurControl.swift */; }; 4B1E951E21764E1800E9DFB8 /* FilterGaussianBlur.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E951D21764E1800E9DFB8 /* FilterGaussianBlur.swift */; }; @@ -60,14 +63,14 @@ 4B4C7C7A260A69C50082240A /* LUT_64_X_2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4B4C7C79260A69C50082240A /* LUT_64_X_2.jpg */; }; 4B4C7C7B260A69C50082240A /* LUT_64_X_2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4B4C7C79260A69C50082240A /* LUT_64_X_2.jpg */; }; 4B58DF662607100600D85F97 /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B58DF652607100600D85F97 /* CanvasView.swift */; }; - 4B58E82A260F0027004A834F /* IMG_5532.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* IMG_5532.HEIC */; }; - 4B58E82B260F0027004A834F /* IMG_5533.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* IMG_5533.HEIC */; }; - 4B58E82C260F0027004A834F /* IMG_5534.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* IMG_5534.HEIC */; }; - 4B58E82D260F0027004A834F /* IMG_5528.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* IMG_5528.HEIC */; }; - 4B58E82E260F0027004A834F /* IMG_5529.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* IMG_5529.HEIC */; }; - 4B58E82F260F0027004A834F /* IMG_5530.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* IMG_5530.HEIC */; }; - 4B58E830260F0027004A834F /* IMG_5535.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* IMG_5535.HEIC */; }; - 4B58E831260F0027004A834F /* IMG_5531.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* IMG_5531.HEIC */; }; + 4B58E82A260F0027004A834F /* orientation_left_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* orientation_left_mirrored.HEIC */; }; + 4B58E82B260F0027004A834F /* orientation_down_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* orientation_down_mirrored.HEIC */; }; + 4B58E82C260F0027004A834F /* orientation_right_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* orientation_right_mirrored.HEIC */; }; + 4B58E82D260F0027004A834F /* orientation_right.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* orientation_right.HEIC */; }; + 4B58E82E260F0027004A834F /* orientation_down.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* orientation_down.HEIC */; }; + 4B58E82F260F0027004A834F /* orientation_left.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* orientation_left.HEIC */; }; + 4B58E830260F0027004A834F /* orientation_up_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* orientation_up_mirrored.HEIC */; }; + 4B58E831260F0027004A834F /* orientation_up.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* orientation_up.HEIC */; }; 4B58E889260F0DEA004A834F /* DemoPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B58E888260F0DEA004A834F /* DemoPreviewViewController.swift */; }; 4B600B02216B7A94001E1456 /* BrightroomEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B600B00216B7A94001E1456 /* BrightroomEngine.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4B600B19216B7BB9001E1456 /* ImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B600B18216B7BB9001E1456 /* ImageRenderer.swift */; }; @@ -130,22 +133,22 @@ 4B9D4580216EE1BC003A240A /* ClassicImageEditControlViewBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9D457F216EE1BC003A240A /* ClassicImageEditControlViewBase.swift */; }; 4B9E8E5E2180B6990071DE91 /* FilterBrightness.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9E8E5D2180B6990071DE91 /* FilterBrightness.swift */; }; 4BA0B1532180218C00CDA871 /* ColorCubeStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E95322176F80600E9DFB8 /* ColorCubeStorage.swift */; }; - 4BA41C82260CE817005E6FA7 /* IMG_5535.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* IMG_5535.HEIC */; }; - 4BA41C83260CE817005E6FA7 /* IMG_5534.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* IMG_5534.HEIC */; }; - 4BA41C84260CE817005E6FA7 /* IMG_5532.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* IMG_5532.HEIC */; }; - 4BA41C85260CE817005E6FA7 /* IMG_5530.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* IMG_5530.HEIC */; }; - 4BA41C86260CE817005E6FA7 /* IMG_5529.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* IMG_5529.HEIC */; }; - 4BA41C87260CE817005E6FA7 /* IMG_5528.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* IMG_5528.HEIC */; }; - 4BA41C88260CE817005E6FA7 /* IMG_5531.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* IMG_5531.HEIC */; }; - 4BA41C89260CE817005E6FA7 /* IMG_5533.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* IMG_5533.HEIC */; }; - 4BA41C8D260CE817005E6FA7 /* IMG_5535.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* IMG_5535.HEIC */; }; - 4BA41C8E260CE817005E6FA7 /* IMG_5534.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* IMG_5534.HEIC */; }; - 4BA41C8F260CE817005E6FA7 /* IMG_5532.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* IMG_5532.HEIC */; }; - 4BA41C90260CE817005E6FA7 /* IMG_5530.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* IMG_5530.HEIC */; }; - 4BA41C91260CE817005E6FA7 /* IMG_5529.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* IMG_5529.HEIC */; }; - 4BA41C92260CE817005E6FA7 /* IMG_5528.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* IMG_5528.HEIC */; }; - 4BA41C93260CE817005E6FA7 /* IMG_5531.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* IMG_5531.HEIC */; }; - 4BA41C94260CE817005E6FA7 /* IMG_5533.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* IMG_5533.HEIC */; }; + 4BA41C82260CE817005E6FA7 /* orientation_up_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* orientation_up_mirrored.HEIC */; }; + 4BA41C83260CE817005E6FA7 /* orientation_right_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* orientation_right_mirrored.HEIC */; }; + 4BA41C84260CE817005E6FA7 /* orientation_left_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* orientation_left_mirrored.HEIC */; }; + 4BA41C85260CE817005E6FA7 /* orientation_left.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* orientation_left.HEIC */; }; + 4BA41C86260CE817005E6FA7 /* orientation_down.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* orientation_down.HEIC */; }; + 4BA41C87260CE817005E6FA7 /* orientation_right.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* orientation_right.HEIC */; }; + 4BA41C88260CE817005E6FA7 /* orientation_up.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* orientation_up.HEIC */; }; + 4BA41C89260CE817005E6FA7 /* orientation_down_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* orientation_down_mirrored.HEIC */; }; + 4BA41C8D260CE817005E6FA7 /* orientation_up_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEB260BB32600F77E9A /* orientation_up_mirrored.HEIC */; }; + 4BA41C8E260CE817005E6FA7 /* orientation_right_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE7260BB32600F77E9A /* orientation_right_mirrored.HEIC */; }; + 4BA41C8F260CE817005E6FA7 /* orientation_left_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE6260BB32600F77E9A /* orientation_left_mirrored.HEIC */; }; + 4BA41C90260CE817005E6FA7 /* orientation_left.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE8260BB32600F77E9A /* orientation_left.HEIC */; }; + 4BA41C91260CE817005E6FA7 /* orientation_down.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEC260BB32600F77E9A /* orientation_down.HEIC */; }; + 4BA41C92260CE817005E6FA7 /* orientation_right.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FEA260BB32600F77E9A /* orientation_right.HEIC */; }; + 4BA41C93260CE817005E6FA7 /* orientation_up.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FED260BB32600F77E9A /* orientation_up.HEIC */; }; + 4BA41C94260CE817005E6FA7 /* orientation_down_mirrored.HEIC in Resources */ = {isa = PBXBuildFile; fileRef = 4B254FE9260BB32600F77E9A /* orientation_down_mirrored.HEIC */; }; 4BA41C99260CF035005E6FA7 /* DemoMaskingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA41C98260CF035005E6FA7 /* DemoMaskingViewController.swift */; }; 4BA41C9E260CF04D005E6FA7 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA41C9D260CF04D005E6FA7 /* Utilities.swift */; }; 4BA84F7E25EE94B700BAB430 /* CropView._InteractiveCropGuideView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BA84F7D25EE94B700BAB430 /* CropView._InteractiveCropGuideView.swift */; }; @@ -271,6 +274,9 @@ 4B0BDCA0217D663D001098B9 /* ClassicImageEditClarityControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicImageEditClarityControl.swift; sourceTree = ""; }; 4B0BDCA2217D676D001098B9 /* ClassicImageEditSharpenControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicImageEditSharpenControl.swift; sourceTree = ""; }; 4B112F14216C9B5400EF8E25 /* ImageTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTool.swift; sourceTree = ""; }; + 4B135F6D26136B5D003B5152 /* RendererOrientationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RendererOrientationTests.swift; sourceTree = ""; }; + 4B135F752613767E003B5152 /* CoreGraphics+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreGraphics+.swift"; sourceTree = ""; }; + 4B135F7A2613769C003B5152 /* Optional+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+.swift"; sourceTree = ""; }; 4B1E94E8217634A800E9DFB8 /* FilterHue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterHue.swift; sourceTree = ""; }; 4B1E94ED217634A800E9DFB8 /* FilterSaturation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterSaturation.swift; sourceTree = ""; }; 4B1E94EE217634A800E9DFB8 /* FilterVignette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilterVignette.swift; sourceTree = ""; }; @@ -292,14 +298,14 @@ 4B1E95322176F80600E9DFB8 /* ColorCubeStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorCubeStorage.swift; sourceTree = ""; }; 4B1E9534217714A800E9DFB8 /* ClassicImageEditColorCubeControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicImageEditColorCubeControlView.swift; sourceTree = ""; }; 4B1E9536217714D500E9DFB8 /* ClassicImageEditEditMenuControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicImageEditEditMenuControlView.swift; sourceTree = ""; }; - 4B254FE6260BB32600F77E9A /* IMG_5532.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5532.HEIC; sourceTree = ""; }; - 4B254FE7260BB32600F77E9A /* IMG_5534.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5534.HEIC; sourceTree = ""; }; - 4B254FE8260BB32600F77E9A /* IMG_5530.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5530.HEIC; sourceTree = ""; }; - 4B254FE9260BB32600F77E9A /* IMG_5533.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5533.HEIC; sourceTree = ""; }; - 4B254FEA260BB32600F77E9A /* IMG_5528.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5528.HEIC; sourceTree = ""; }; - 4B254FEB260BB32600F77E9A /* IMG_5535.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5535.HEIC; sourceTree = ""; }; - 4B254FEC260BB32600F77E9A /* IMG_5529.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5529.HEIC; sourceTree = ""; }; - 4B254FED260BB32600F77E9A /* IMG_5531.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = IMG_5531.HEIC; sourceTree = ""; }; + 4B254FE6260BB32600F77E9A /* orientation_left_mirrored.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_left_mirrored.HEIC; sourceTree = ""; }; + 4B254FE7260BB32600F77E9A /* orientation_right_mirrored.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_right_mirrored.HEIC; sourceTree = ""; }; + 4B254FE8260BB32600F77E9A /* orientation_left.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_left.HEIC; sourceTree = ""; }; + 4B254FE9260BB32600F77E9A /* orientation_down_mirrored.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_down_mirrored.HEIC; sourceTree = ""; }; + 4B254FEA260BB32600F77E9A /* orientation_right.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_right.HEIC; sourceTree = ""; }; + 4B254FEB260BB32600F77E9A /* orientation_up_mirrored.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_up_mirrored.HEIC; sourceTree = ""; }; + 4B254FEC260BB32600F77E9A /* orientation_down.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_down.HEIC; sourceTree = ""; }; + 4B254FED260BB32600F77E9A /* orientation_up.HEIC */ = {isa = PBXFileReference; lastKnownFileType = file; path = orientation_up.HEIC; sourceTree = ""; }; 4B3075912605F27400A1E7CB /* TopMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopMenuViewController.swift; sourceTree = ""; }; 4B3075962605F27C00A1E7CB /* StackScrollNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackScrollNode.swift; sourceTree = ""; }; 4B34578A217502F900716DA0 /* ClassicImageEditStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClassicImageEditStyle.swift; sourceTree = ""; }; @@ -612,6 +618,7 @@ 4B0433AA217B962C0039293E /* InternalUtils.swift */, 4B422DFC260B91260011500C /* Orientation.swift */, 4BF6872E2610D6D80037D5F4 /* CIImage+.swift */, + 4B135F7A2613769C003B5152 /* Optional+.swift */, ); path = Library; sourceTree = ""; @@ -831,6 +838,7 @@ isa = PBXGroup; children = ( 4B600B18216B7BB9001E1456 /* ImageRenderer.swift */, + 4B135F752613767E003B5152 /* CoreGraphics+.swift */, 4B9B02EB216BBDA8001593B9 /* Layer.swift */, 4B112F14216C9B5400EF8E25 /* ImageTool.swift */, ); @@ -880,6 +888,7 @@ 4BE9B3D9260BA778000A3D09 /* Bundle.swift */, 4BDEE09C25F8CF5800FA22CB /* Info.plist */, 4B36194B26105BEB00877B21 /* RendererTests.swift */, + 4B135F6D26136B5D003B5152 /* RendererOrientationTests.swift */, ); path = BrightroomEngineTests; sourceTree = ""; @@ -907,14 +916,14 @@ children = ( 4BF23C312603BCE0007019CB /* gaku.jpeg */, 4B9369E725F940E600B18571 /* nasa.jpg */, - 4B254FEA260BB32600F77E9A /* IMG_5528.HEIC */, - 4B254FEC260BB32600F77E9A /* IMG_5529.HEIC */, - 4B254FE8260BB32600F77E9A /* IMG_5530.HEIC */, - 4B254FED260BB32600F77E9A /* IMG_5531.HEIC */, - 4B254FE6260BB32600F77E9A /* IMG_5532.HEIC */, - 4B254FE9260BB32600F77E9A /* IMG_5533.HEIC */, - 4B254FE7260BB32600F77E9A /* IMG_5534.HEIC */, - 4B254FEB260BB32600F77E9A /* IMG_5535.HEIC */, + 4B254FEA260BB32600F77E9A /* orientation_right.HEIC */, + 4B254FEC260BB32600F77E9A /* orientation_down.HEIC */, + 4B254FE8260BB32600F77E9A /* orientation_left.HEIC */, + 4B254FED260BB32600F77E9A /* orientation_up.HEIC */, + 4B254FE6260BB32600F77E9A /* orientation_left_mirrored.HEIC */, + 4B254FE9260BB32600F77E9A /* orientation_down_mirrored.HEIC */, + 4B254FE7260BB32600F77E9A /* orientation_right_mirrored.HEIC */, + 4B254FEB260BB32600F77E9A /* orientation_up_mirrored.HEIC */, ); path = Images; sourceTree = ""; @@ -1166,19 +1175,19 @@ files = ( 4BB4B105260F8589007617D7 /* LUT_64_Neutral.png in Resources */, 4B600B2D216B7C9E001E1456 /* LaunchScreen.storyboard in Resources */, - 4BA41C89260CE817005E6FA7 /* IMG_5533.HEIC in Resources */, - 4BA41C86260CE817005E6FA7 /* IMG_5529.HEIC in Resources */, - 4BA41C85260CE817005E6FA7 /* IMG_5530.HEIC in Resources */, - 4BA41C84260CE817005E6FA7 /* IMG_5532.HEIC in Resources */, + 4BA41C89260CE817005E6FA7 /* orientation_down_mirrored.HEIC in Resources */, + 4BA41C86260CE817005E6FA7 /* orientation_down.HEIC in Resources */, + 4BA41C85260CE817005E6FA7 /* orientation_left.HEIC in Resources */, + 4BA41C84260CE817005E6FA7 /* orientation_left_mirrored.HEIC in Resources */, 4B4C7C7A260A69C50082240A /* LUT_64_X_2.jpg in Resources */, - 4BA41C82260CE817005E6FA7 /* IMG_5535.HEIC in Resources */, + 4BA41C82260CE817005E6FA7 /* orientation_up_mirrored.HEIC in Resources */, 4B600B2A216B7C9E001E1456 /* Assets.xcassets in Resources */, - 4BA41C83260CE817005E6FA7 /* IMG_5534.HEIC in Resources */, + 4BA41C83260CE817005E6FA7 /* orientation_right_mirrored.HEIC in Resources */, 4BF23C322603BCE0007019CB /* gaku.jpeg in Resources */, 4BD275FF260A48FF0044B3D8 /* LUT_64_MMM.png in Resources */, - 4BA41C88260CE817005E6FA7 /* IMG_5531.HEIC in Resources */, + 4BA41C88260CE817005E6FA7 /* orientation_up.HEIC in Resources */, 4B9369E825F940E600B18571 /* nasa.jpg in Resources */, - 4BA41C87260CE817005E6FA7 /* IMG_5528.HEIC in Resources */, + 4BA41C87260CE817005E6FA7 /* orientation_right.HEIC in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1187,17 +1196,17 @@ buildActionMask = 2147483647; files = ( 4B98CF5325F0122000E4F61F /* Assets.xcassets in Resources */, - 4BA41C90260CE817005E6FA7 /* IMG_5530.HEIC in Resources */, - 4BA41C8E260CE817005E6FA7 /* IMG_5534.HEIC in Resources */, + 4BA41C90260CE817005E6FA7 /* orientation_left.HEIC in Resources */, + 4BA41C8E260CE817005E6FA7 /* orientation_right_mirrored.HEIC in Resources */, 4B4C7C7B260A69C50082240A /* LUT_64_X_2.jpg in Resources */, - 4BA41C92260CE817005E6FA7 /* IMG_5528.HEIC in Resources */, - 4BA41C93260CE817005E6FA7 /* IMG_5531.HEIC in Resources */, - 4BA41C94260CE817005E6FA7 /* IMG_5533.HEIC in Resources */, - 4BA41C8D260CE817005E6FA7 /* IMG_5535.HEIC in Resources */, - 4BA41C8F260CE817005E6FA7 /* IMG_5532.HEIC in Resources */, + 4BA41C92260CE817005E6FA7 /* orientation_right.HEIC in Resources */, + 4BA41C93260CE817005E6FA7 /* orientation_up.HEIC in Resources */, + 4BA41C94260CE817005E6FA7 /* orientation_down_mirrored.HEIC in Resources */, + 4BA41C8D260CE817005E6FA7 /* orientation_up_mirrored.HEIC in Resources */, + 4BA41C8F260CE817005E6FA7 /* orientation_left_mirrored.HEIC in Resources */, 4BB9180C25F5460000C446B8 /* Launch Screen.storyboard in Resources */, 4B98CCC325EFF31400E4F61F /* Preview Assets.xcassets in Resources */, - 4BA41C91260CE817005E6FA7 /* IMG_5529.HEIC in Resources */, + 4BA41C91260CE817005E6FA7 /* orientation_down.HEIC in Resources */, 4BF23C332603BCE0007019CB /* gaku.jpeg in Resources */, 4B9369E925F940E600B18571 /* nasa.jpg in Resources */, ); @@ -1208,16 +1217,16 @@ buildActionMask = 2147483647; files = ( 4BE9B3D2260BA769000A3D09 /* gaku.jpeg in Resources */, - 4B58E82E260F0027004A834F /* IMG_5529.HEIC in Resources */, - 4B58E830260F0027004A834F /* IMG_5535.HEIC in Resources */, - 4B58E82D260F0027004A834F /* IMG_5528.HEIC in Resources */, + 4B58E82E260F0027004A834F /* orientation_down.HEIC in Resources */, + 4B58E830260F0027004A834F /* orientation_up_mirrored.HEIC in Resources */, + 4B58E82D260F0027004A834F /* orientation_right.HEIC in Resources */, 4B36195126105E3900877B21 /* Assets.xcassets in Resources */, - 4B58E831260F0027004A834F /* IMG_5531.HEIC in Resources */, - 4B58E82C260F0027004A834F /* IMG_5534.HEIC in Resources */, + 4B58E831260F0027004A834F /* orientation_up.HEIC in Resources */, + 4B58E82C260F0027004A834F /* orientation_right_mirrored.HEIC in Resources */, 4B36195626107ADB00877B21 /* path-data in Resources */, - 4B58E82B260F0027004A834F /* IMG_5533.HEIC in Resources */, - 4B58E82F260F0027004A834F /* IMG_5530.HEIC in Resources */, - 4B58E82A260F0027004A834F /* IMG_5532.HEIC in Resources */, + 4B58E82B260F0027004A834F /* orientation_down_mirrored.HEIC in Resources */, + 4B58E82F260F0027004A834F /* orientation_left.HEIC in Resources */, + 4B58E82A260F0027004A834F /* orientation_left_mirrored.HEIC in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1382,6 +1391,7 @@ 4B1E95242176D06800E9DFB8 /* FilterHighlights.swift in Sources */, 4BEE650D217479C600F3578E /* DrawnPath.swift in Sources */, 4B49C1AA260EE8B30099BFBD /* ColorCubeHelper.swift in Sources */, + 4B135F7B2613769C003B5152 /* Optional+.swift in Sources */, 4B66C9722171D764008F2A54 /* BrightroomEngine.swift in Sources */, 4BEE650C217479C600F3578E /* OvalBrush.swift in Sources */, 4B9B02EC216BBDA8001593B9 /* Layer.swift in Sources */, @@ -1396,6 +1406,7 @@ 4B1E95282176D85900E9DFB8 /* FilterColorCube.swift in Sources */, 4B1E951E21764E1800E9DFB8 /* FilterGaussianBlur.swift in Sources */, 4B0433AB217B962C0039293E /* InternalUtils.swift in Sources */, + 4B135F762613767E003B5152 /* CoreGraphics+.swift in Sources */, 4B66C9702171D6F8008F2A54 /* EditingStack.swift in Sources */, 4B04B3EE2177B0C70032356A /* FilterSaturation.swift in Sources */, 4B600B19216B7BB9001E1456 /* ImageRenderer.swift in Sources */, @@ -1454,6 +1465,7 @@ buildActionMask = 2147483647; files = ( 4B36195026105E3900877B21 /* XCAssets+Generated.swift in Sources */, + 4B135F6E26136B5D003B5152 /* RendererOrientationTests.swift in Sources */, 4B36194C26105BEB00877B21 /* RendererTests.swift in Sources */, 4BE9B3CC260BA69F000A3D09 /* LoadingTests.swift in Sources */, 4BE9B3E1260BA77C000A3D09 /* Bundle.swift in Sources */, diff --git a/Bundle/Images/IMG_5529.HEIC b/Bundle/Images/orientation_down.HEIC similarity index 100% rename from Bundle/Images/IMG_5529.HEIC rename to Bundle/Images/orientation_down.HEIC diff --git a/Bundle/Images/IMG_5533.HEIC b/Bundle/Images/orientation_down_mirrored.HEIC similarity index 100% rename from Bundle/Images/IMG_5533.HEIC rename to Bundle/Images/orientation_down_mirrored.HEIC diff --git a/Bundle/Images/IMG_5530.HEIC b/Bundle/Images/orientation_left.HEIC similarity index 100% rename from Bundle/Images/IMG_5530.HEIC rename to Bundle/Images/orientation_left.HEIC diff --git a/Bundle/Images/IMG_5532.HEIC b/Bundle/Images/orientation_left_mirrored.HEIC similarity index 100% rename from Bundle/Images/IMG_5532.HEIC rename to Bundle/Images/orientation_left_mirrored.HEIC diff --git a/Bundle/Images/IMG_5528.HEIC b/Bundle/Images/orientation_right.HEIC similarity index 100% rename from Bundle/Images/IMG_5528.HEIC rename to Bundle/Images/orientation_right.HEIC diff --git a/Bundle/Images/IMG_5534.HEIC b/Bundle/Images/orientation_right_mirrored.HEIC similarity index 100% rename from Bundle/Images/IMG_5534.HEIC rename to Bundle/Images/orientation_right_mirrored.HEIC diff --git a/Bundle/Images/IMG_5531.HEIC b/Bundle/Images/orientation_up.HEIC similarity index 100% rename from Bundle/Images/IMG_5531.HEIC rename to Bundle/Images/orientation_up.HEIC diff --git a/Bundle/Images/IMG_5535.HEIC b/Bundle/Images/orientation_up_mirrored.HEIC similarity index 100% rename from Bundle/Images/IMG_5535.HEIC rename to Bundle/Images/orientation_up_mirrored.HEIC diff --git a/Sources/BrightroomEngine/ColorCube/ColorCubeHelper.swift b/Sources/BrightroomEngine/ColorCube/ColorCubeHelper.swift index 1d1aab03..af32d123 100644 --- a/Sources/BrightroomEngine/ColorCube/ColorCubeHelper.swift +++ b/Sources/BrightroomEngine/ColorCube/ColorCubeHelper.swift @@ -26,7 +26,7 @@ public class ColorCubeHelper { throw ColorCubeHelperError.failedToCreateCGImageSource } - let cgImage = ImageTool.loadOriginalCGImage(from: imageSource)! + let cgImage = ImageTool.loadOriginalCGImage(from: imageSource, fixesOrientation: false)! let pixels = cgImage.width * cgImage.height let channels = 4 diff --git a/Sources/BrightroomEngine/DataSource/ImageSource.swift b/Sources/BrightroomEngine/DataSource/ImageSource.swift index 1d45f42b..91dbe90d 100644 --- a/Sources/BrightroomEngine/DataSource/ImageSource.swift +++ b/Sources/BrightroomEngine/DataSource/ImageSource.swift @@ -19,40 +19,37 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import UIKit - import CoreImage +import UIKit import Verge #if canImport(UIKit) -import UIKit + import UIKit #endif #if canImport(Photos) -import Photos + import Photos #endif -/** - An object that provides an image-data from multiple backing storage. - */ +/// An object that provides an image-data from multiple backing storage. public final class ImageSource: Equatable { - + private struct Closures { let readImageSize: () -> CGSize let loadOriginalCGImage: () -> CGImage let loadThumbnailCGImage: (CGFloat) -> CGImage let makeCIImage: () -> CIImage } - + public static func == (lhs: ImageSource, rhs: ImageSource) -> Bool { lhs === rhs } - + private let closures: Closures - + public init(image: UIImage) { - + self.closures = .init( readImageSize: { image.size.applying(.init(scaleX: image.scale, y: image.scale)) @@ -68,43 +65,61 @@ public final class ImageSource: Equatable { } ) } - + public init(cgImageSource: CGImageSource) { self.closures = .init( readImageSize: { ImageTool.readImageSize(from: cgImageSource)! }, loadOriginalCGImage: { - ImageTool.loadOriginalCGImage(from: cgImageSource)! + ImageTool.loadOriginalCGImage(from: cgImageSource, fixesOrientation: false)! }, loadThumbnailCGImage: { (maxPixelSize) -> CGImage in - ImageTool.loadThumbnailCGImage(from: cgImageSource, maxPixelSize: maxPixelSize)! + ImageTool.loadThumbnailCGImage( + from: cgImageSource, + maxPixelSize: maxPixelSize, + fixesOrientation: false + )! }, makeCIImage: { if #available(iOS 13.0, *) { return CIImage(cgImageSource: cgImageSource, index: 0, options: [:]) } else { - return CIImage(cgImage: ImageTool.loadOriginalCGImage(from: cgImageSource)!) + return CIImage(cgImage: ImageTool.loadOriginalCGImage(from: cgImageSource, fixesOrientation: false)!) } } ) } - + public func readImageSize() -> CGSize { closures.readImageSize() } - + + /** + Creates an instance of CGImage full-resolution. + + - Attention: The image is not orientated. + */ public func loadOriginalCGImage() -> CGImage { closures.loadOriginalCGImage() } - + + /** + Creates an instance of CGImage resized to maximum pixel size. + + - Attention: The image is not orientated. + */ public func loadThumbnailCGImage(maxPixelSize: CGFloat) -> CGImage { closures.loadThumbnailCGImage(maxPixelSize) } - - public func makeCIImage() -> CIImage { + + /** + Creates an instance of CIImage that backed full-resolution image. + + - Attention: The image is not orientated. + */ + public func makeOriginalCIImage() -> CIImage { closures.makeCIImage() } - -} +} diff --git a/Sources/BrightroomEngine/Engine/CoreGraphics+.swift b/Sources/BrightroomEngine/Engine/CoreGraphics+.swift new file mode 100644 index 00000000..063cedaa --- /dev/null +++ b/Sources/BrightroomEngine/Engine/CoreGraphics+.swift @@ -0,0 +1,189 @@ +// +// Copyright (c) 2021 Hiroshi Kimura(Muukii) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreGraphics +import ImageIO + +extension CGContext { + + @discardableResult + func perform(_ drawing: (CGContext) -> Void) -> CGContext { + drawing(self) + return self + } + + static func makeContext(for image: CGImage, size: CGSize? = nil) throws -> CGContext { + let context = CGContext.init( + data: nil, + width: size.map { Int($0.width) } ?? image.width, + height: size.map { Int($0.height) } ?? image.height, + bitsPerComponent: image.bitsPerComponent, + bytesPerRow: 0, + space: try image.colorSpace.unwrap(), + bitmapInfo: image.bitmapInfo.rawValue + ) + + return try context.unwrap() + } + + fileprivate func detached(_ perform: () -> Void) { + saveGState() + perform() + restoreGState() + } +} + +extension CGImage { + + var size: CGSize { + return .init(width: width, height: height) + } + + func croppedWithColorspace(to cropRect: CGRect) throws -> CGImage { + + let cgImage = try autoreleasepool { () -> CGImage? in + + let context = try CGContext.makeContext(for: self, size: cropRect.size) + .perform { c in + + c.draw( + self, + in: CGRect( + origin: .init( + x: -cropRect.origin.x, + y: -(size.height - cropRect.maxY) + ), + size: size + ) + ) + + } + return context.makeImage() + } + + return try cgImage.unwrap() + + } + + func resized(maxPixelSize: CGFloat) throws -> CGImage { + + let cgImage = try autoreleasepool { () -> CGImage? in + + let targetSize = Geometry.sizeThatAspectFit( + size: size, + maxPixelSize: maxPixelSize + ) + + let context = try CGContext.makeContext(for: self, size: targetSize) + .perform { c in + c.interpolationQuality = .high + c.draw(self, in: c.boundingBoxOfClipPath) + } + return context.makeImage() + } + + return try cgImage.unwrap() + } + + enum Flipping { + case vertically + case horizontally + } + + func rotated(angle: CGFloat, flipping: Flipping? = nil) throws -> CGImage { + guard angle != 0 else { + return self + } + + var rotatedSize: CGSize = + size + .applying(.init(rotationAngle: angle)) + + rotatedSize.width = abs(rotatedSize.width) + rotatedSize.height = abs(rotatedSize.height) + + let cgImage = try autoreleasepool { () -> CGImage? in + let rotatingContext = try CGContext.makeContext(for: self, size: rotatedSize) + .perform { c in + + if let flipping = flipping { + switch flipping { + case .vertically: + c.translateBy(x: 0, y: rotatedSize.height) + c.scaleBy(x: 1, y: -1) + case .horizontally: + c.translateBy(x: rotatedSize.width, y: 0) + c.scaleBy(x: -1, y: 1) + } + } + + c.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) + c.rotate(by: angle) + c.translateBy( + x: -size.width / 2, + y: -size.height / 2 + ) + + c.draw(self, in: .init(origin: .zero, size: self.size)) + } + + return rotatingContext.makeImage() + } + + return try cgImage.unwrap() + } + + func oriented(_ orientation: CGImagePropertyOrientation) throws -> CGImage { + + let angle: CGFloat + + switch orientation { + case .down, .downMirrored: + angle = CGFloat.pi + case .left, .leftMirrored: + angle = CGFloat.pi / 2.0 + case .right, .rightMirrored: + angle = CGFloat.pi / -2.0 + case .up, .upMirrored: + angle = 0 + } + + let flipping: Flipping? + switch orientation { + case .upMirrored, .downMirrored: + flipping = .horizontally + case .leftMirrored, .rightMirrored: + flipping = .vertically + case .up, .down, .left, .right: + flipping = nil + } + + let result = try rotated(angle: angle, flipping: flipping) + + return result + } + + func rotated(rotation: EditingCrop.Rotation, flipping: Flipping? = nil) + throws -> CGImage + { + try rotated(angle: -rotation.angle, flipping: flipping) + } +} diff --git a/Sources/BrightroomEngine/Engine/ImageRenderer.swift b/Sources/BrightroomEngine/Engine/ImageRenderer.swift index 9a75e373..80aaa77c 100644 --- a/Sources/BrightroomEngine/Engine/ImageRenderer.swift +++ b/Sources/BrightroomEngine/Engine/ImageRenderer.swift @@ -20,67 +20,87 @@ // THE SOFTWARE. import CoreImage -import UIKit import SwiftUI +import UIKit public final class ImageRenderer { - + public struct Options { - - public var resolution: Resolution = .full - public var workingFormat: CIFormat = .ARGB8 - - public init(resolution: ImageRenderer.Resolution = .full, workingFormat: CIFormat = .ARGB8) { + + public var resolution: Resolution + public var workingFormat: CIFormat + public var workingColorSpace: CGColorSpace + + public init( + resolution: ImageRenderer.Resolution = .full, + workingFormat: CIFormat = .ARGB8, + workingColorSpace: CGColorSpace = CGColorSpace(name: CGColorSpace.sRGB)! + ) { self.resolution = resolution self.workingFormat = workingFormat + self.workingColorSpace = workingColorSpace } - + } - + /** A result of rendering. */ public struct Rendered { - + + public enum Engine { + case coreGraphics + case combined + } + public enum DataType { case jpeg(quality: CGFloat) case png } - - /// A rendered image that working on DisplayP3 - public let cgImageDisplayP3: CGImage - - public var uiImageDisplayP3: UIImage { - .init(cgImage: cgImageDisplayP3) + + /// A type of engine how rendered by + public let engine: Engine + + /// An Options instance that used in redering. + public let options: Options + + /// A rendered image that working on specified color-space. + /// Orientation fixed. + public let cgImage: CGImage + + public var uiImage: UIImage { + UIImage.init(cgImage: cgImage, scale: 1, orientation: .up) } - + @available(iOS 13.0, *) - public var swiftUIImageDisplayP3: SwiftUI.Image { - .init(decorative: cgImageDisplayP3, scale: 1, orientation: .up) + public var swiftUIImage: SwiftUI.Image { + .init(decorative: cgImage, scale: 1, orientation: .up) } - - init(cgImageDisplayP3: CGImage) { - assert(cgImageDisplayP3.colorSpace == CGColorSpace.init(name: CGColorSpace.displayP3)) - self.cgImageDisplayP3 = cgImageDisplayP3 + + init(cgImage: CGImage, options: Options, engine: Engine) { + assert(cgImage.colorSpace == options.workingColorSpace) + self.cgImage = cgImage + self.options = options + self.engine = engine } - + /** Makes a data of the image that optimized for sharing. - + Since the rendered image is working on DisplayP3 profile, that data might display wrong color on other platform devices. To avoid those issues, use this method to create data to send instead of creating data from `cgImageDisplayP3`. */ public func makeOptimizedForSharingData(dataType: DataType) -> Data { switch dataType { case .jpeg(let quality): - return ImageTool.makeImageForJPEGOptimizedSharing(image: cgImageDisplayP3, quality: quality) + return ImageTool.makeImageForJPEGOptimizedSharing(image: cgImage, quality: quality) case .png: - return ImageTool.makeImageForPNGOptimizedSharing(image: cgImageDisplayP3) + return ImageTool.makeImageForPNGOptimizedSharing(image: cgImage) } } - + } - + private static let queue = DispatchQueue.init(label: "app.muukii.Pixel.renderer") public enum Resolution { @@ -107,8 +127,10 @@ public final class ImageRenderer { public func render( options: Options = .init(), - completion: @escaping (Result - ) -> Void) { + completion: @escaping ( + Result + ) -> Void + ) { type(of: self).queue.async { do { let rendered = try self.render() @@ -129,23 +151,111 @@ public final class ImageRenderer { - Attension: This operation can be run background-thread. */ public func render(options: Options = .init()) throws -> Rendered { - try renderRevison2(options: options) + if edit.drawer.isEmpty, edit.modifiers.isEmpty { + return try renderOnlyCropping(options: options) + } else { + return try renderRevison2(options: options) + } + } + + /** + Render for only cropping using CoreGraphics + */ + private func renderOnlyCropping(options: Options = .init()) throws -> Rendered { + + EngineLog.debug(.renderer, "Start render in using CoreGraphics") + + /* + === + === + === + */ + EngineLog.debug(.renderer, "Load full resolution CGImage from ImageSource.") + + let sourceCGImage: CGImage = source.loadOriginalCGImage() + + /* + === + === + === + */ + EngineLog.debug( + .renderer, + "Fixing colorspace \(sourceCGImage.colorSpace?.name as NSString? ?? "") -> \(options.workingColorSpace.name as NSString? ?? "")" + ) + + let fixedColorspace: CGImage = try sourceCGImage.copy(colorSpace: options.workingColorSpace) + .unwrap(orThrow: "Failed to copy with fixing colorspace.") + + assert(fixedColorspace.colorSpace == options.workingColorSpace) + /* + === + === + === + */ + EngineLog.debug(.renderer, "Fix orientation") + + let orientedImage = try fixedColorspace.oriented(orientation) + + /* + === + === + === + */ + // TODO: Better management of orientation + let crop = edit.croppingRect ?? .init(imageSize: source.readImageSize().applying(cgOrientation: orientation)) + EngineLog.debug(.renderer, "Crop CGImage with extent \(crop)") + + let croppedImage = try orientedImage.croppedWithColorspace(to: crop.cropExtent) + + /* + === + === + === + */ + EngineLog.debug(.renderer, "Resize if needed") + + + let resizedImage: CGImage + + switch options.resolution { + case .full: + resizedImage = croppedImage + case .resize(let maxPixelSize): + resizedImage = try croppedImage.resized(maxPixelSize: maxPixelSize) + } + + /* + === + === + === + */ + EngineLog.debug(.renderer, "Rotation") + + let rotatedImage = try resizedImage.rotated(rotation: crop.rotation) + + return .init(cgImage: rotatedImage, options: options, engine: .coreGraphics) } + /** + Render for full features using CoreImage and CoreGraphics + */ private func renderRevison2( options: Options = .init(), debug: @escaping (CIImage) -> Void = { _ in } ) throws -> Rendered { - + let ciContext = CIContext( options: [ .workingFormat: options.workingFormat, .highQualityDownsample: true, - // .useSoftwareRenderer: true, + .useSoftwareRenderer: false, .cacheIntermediates: false, ] ) + let startTime = CACurrentMediaTime() + EngineLog.debug(.renderer, "Start render in v2 using CIContext => \(ciContext)") /* @@ -155,7 +265,7 @@ public final class ImageRenderer { */ EngineLog.debug(.renderer, "Take full resolution CIImage from ImageSource.") - let sourceCIImage: CIImage = source.makeCIImage().oriented(orientation) + let sourceCIImage: CIImage = source.makeOriginalCIImage().oriented(orientation) EngineLog.debug(.renderer, "Input oriented CIImage => \(sourceCIImage)") @@ -163,7 +273,8 @@ public final class ImageRenderer { { guard let crop = edit.croppingRect else { return true } return crop.imageSize == CGSize(image: sourceCIImage) - }()) + }() + ) /* === @@ -183,7 +294,8 @@ public final class ImageRenderer { */ EngineLog.debug(.renderer, "Applies Crop to effected image") - let crop = edit.croppingRect ?? .init(imageSize: source.readImageSize()) + // TODO: Better management of orientation + let crop = edit.croppingRect ?? .init(imageSize: source.readImageSize().applying(cgOrientation: orientation)) let cropped_effected_CIImage = effected_CIImage.cropped(to: crop) @@ -195,7 +307,7 @@ public final class ImageRenderer { === */ EngineLog.debug(.renderer, "Creates CGImage from crop applied CIImage.") - + /** To keep wide-color(DisplayP3), use createCGImage instead drawing with CIContext */ @@ -203,8 +315,8 @@ public final class ImageRenderer { cropped_effected_CIImage, from: cropped_effected_CIImage.extent, format: options.workingFormat, - colorSpace: CGColorSpace(name: CGColorSpace.displayP3)!, - deferred: true + colorSpace: options.workingColorSpace, + deferred: false )! EngineLog.debug(.renderer, "Created effected CGImage => \(cropped_effected_CGImage)") @@ -248,159 +360,38 @@ public final class ImageRenderer { === === */ - + let resizedImage: CGImage switch options.resolution { case .full: - + EngineLog.debug(.renderer, "No resizing") resizedImage = drawings_CGImage case let .resize(maxPixelSize): - - EngineLog.debug(.renderer, "Resizing with maxPixelSize: \(maxPixelSize)") - let targetSize = Geometry.sizeThatAspectFit( - size: drawings_CGImage.size, - maxPixelSize: maxPixelSize - ) + EngineLog.debug(.renderer, "Resizing with maxPixelSize: \(maxPixelSize)") - let context = try CGContext.makeContext(for: drawings_CGImage, size: targetSize) - .perform { c in - c.draw(drawings_CGImage, in: c.boundingBoxOfClipPath) - } + resizedImage = try drawings_CGImage.resized(maxPixelSize: maxPixelSize) - resizedImage = try context.makeImage().unwrap() } - + /* === === === */ - - EngineLog.debug(.renderer, "Rotates image if needed") - - let rotatedImage = try resizedImage.makeRotatedIfNeeded(rotation: crop.rotation) - - return .init(cgImageDisplayP3: rotatedImage) - - } -} - -extension CGContext { - @discardableResult - func perform(_ drawing: (CGContext) -> Void) -> CGContext { - UIGraphicsPushContext(self) - defer { - UIGraphicsPopContext() - } - drawing(self) - return self - } - - static func makeContext(for image: CGImage, size: CGSize? = nil) throws -> CGContext { - let context = CGContext.init( - data: nil, - width: size.map { Int($0.width) } ?? image.width, - height: size.map { Int($0.height) } ?? image.height, - bitsPerComponent: image.bitsPerComponent, - bytesPerRow: 0, - space: try image.colorSpace.unwrap(), - bitmapInfo: image.bitmapInfo.rawValue - ) - - return try context.unwrap() - } -} - -extension UIGraphicsImageRenderer { - func _custom(_ perform: (UIGraphicsImageRendererContext) -> Void) -> CGImage { - let d = pngData(actions: perform) - return UIImage(data: d)!.cgImage! - } -} - -extension CGContext { - fileprivate func detached(_ perform: () -> Void) { - saveGState() - perform() - restoreGState() - } -} - -extension CGImage { - fileprivate var size: CGSize { - return .init(width: width, height: height) - } - - fileprivate func makeRotatedIfNeeded(rotation: EditingCrop.Rotation) throws -> CGImage { - guard rotation != .angle_0 else { - return self - } - - var rotatedSize: CGSize = size - .applying(rotation.transform) - - rotatedSize.width = abs(rotatedSize.width) - rotatedSize.height = abs(rotatedSize.height) - - let rotatingContext = try CGContext.makeContext(for: self, size: rotatedSize) - .perform { c in - c.translateBy(x: rotatedSize.width / 2, y: rotatedSize.height / 2) - c.rotate(by: -rotation.angle) - c.translateBy( - x: -size.width / 2, - y: -size.height / 2 - ) - c.draw(self, in: .init(origin: .zero, size: self.size)) - } - - return try rotatingContext.makeImage().unwrap() - } -} -extension Optional { - internal func unwrap( - orThrow debugDescription: String? = nil, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line - ) throws -> Wrapped { - if let value = self { - return value - } - throw Optional.BrightroomUnwrappedNilError( - debugDescription, - file: file, - function: function, - line: line - ) - } - - public struct BrightroomUnwrappedNilError: Swift.Error, CustomDebugStringConvertible { - let file: StaticString - let function: StaticString - let line: UInt + EngineLog.debug(.renderer, "Rotates image if needed") - // MARK: Public + let rotatedImage = try resizedImage.rotated(rotation: crop.rotation) - public init( - _ debugDescription: String? = nil, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line - ) { - self.debugDescription = debugDescription ?? "Failed to unwrap on \(file):\(function):\(line)" - self.file = file - self.function = function - self.line = line - } + let duration = CACurrentMediaTime() - startTime + EngineLog.debug(.renderer, "Rendering has completed - took \(duration * 1000)ms") - // MARK: CustomDebugStringConvertible + return .init(cgImage: rotatedImage, options: options, engine: .combined) - public let debugDescription: String } } diff --git a/Sources/BrightroomEngine/Engine/ImageTool.swift b/Sources/BrightroomEngine/Engine/ImageTool.swift index 5f42e8f1..d75a261b 100644 --- a/Sources/BrightroomEngine/Engine/ImageTool.swift +++ b/Sources/BrightroomEngine/Engine/ImageTool.swift @@ -25,9 +25,14 @@ import MobileCoreServices import UIKit enum ImageTool { - static func makeImageMetadata(from imageSource: CGImageSource) -> ImageProvider.State.ImageMetadata? { + static func makeImageMetadata(from imageSource: CGImageSource) -> ImageProvider.State + .ImageMetadata? + { let propertiesOptions = [kCGImageSourceShouldCache: false] as CFDictionary - guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) as? [CFString: Any] else { + guard + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) + as? [CFString: Any] + else { return nil } @@ -40,9 +45,10 @@ enum ImageTool { return nil } - let orientation: CGImagePropertyOrientation = (properties[kCGImagePropertyTIFFOrientation] as? UInt32).flatMap { - CGImagePropertyOrientation(rawValue: $0) - } ?? .up + let orientation: CGImagePropertyOrientation = + (properties[kCGImagePropertyTIFFOrientation] as? UInt32).flatMap { + CGImagePropertyOrientation(rawValue: $0) + } ?? .up let size = CGSize(width: width, height: height) @@ -56,7 +62,10 @@ enum ImageTool { */ static func readImageSize(from imageSource: CGImageSource) -> CGSize? { let propertiesOptions = [kCGImageSourceShouldCache: false] as CFDictionary - guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) as? [CFString: Any] else { + guard + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) + as? [CFString: Any] + else { return nil } @@ -72,7 +81,10 @@ enum ImageTool { static func readOrientation(from imageSource: CGImageSource) -> CGImagePropertyOrientation? { let propertiesOptions = [kCGImageSourceShouldCache: false] as CFDictionary - guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) as? [CFString: Any] else { + guard + let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, propertiesOptions) + as? [CFString: Any] + else { return nil } @@ -85,25 +97,35 @@ enum ImageTool { return orientation } - static func loadOriginalCGImage(from imageSource: CGImageSource) -> CGImage? { - CGImageSourceCreateImageAtIndex(imageSource, 0, [:] as CFDictionary) + static func loadOriginalCGImage(from imageSource: CGImageSource, fixesOrientation: Bool) -> CGImage? { + CGImageSourceCreateImageAtIndex( + imageSource, + 0, + [ + kCGImageSourceCreateThumbnailWithTransform: fixesOrientation, + ] as CFDictionary + ) } - static func loadThumbnailCGImage(from imageSource: CGImageSource, maxPixelSize: CGFloat) -> CGImage? { + static func loadThumbnailCGImage(from imageSource: CGImageSource, maxPixelSize: CGFloat, fixesOrientation: Bool) + -> CGImage? + { let scaledImage = CGImageSourceCreateThumbnailAtIndex( - imageSource, 0, [ + imageSource, + 0, + [ kCGImageSourceThumbnailMaxPixelSize: maxPixelSize, kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: false, ] as CFDictionary ) -// #if DEBUG -// let size = readImageSize(from: imageProvider)! -// let scaled = size.scaled(maxPixelSize: maxPixelSize) -// assert(CGSize(width: scaledImage!.width, height: scaledImage!.height) == scaled) -// #endif -// + // #if DEBUG + // let size = readImageSize(from: imageProvider)! + // let scaled = size.scaled(maxPixelSize: maxPixelSize) + // assert(CGSize(width: scaledImage!.width, height: scaledImage!.height) == scaled) + // #endif + // return scaledImage } @@ -211,34 +233,36 @@ enum ImageTool { destination, image, [ - kCGImageDestinationLossyCompressionQuality : quality, + kCGImageDestinationLossyCompressionQuality: quality, kCGImageDestinationOptimizeColorForSharing: true, - ] as CFDictionary) + ] as CFDictionary + ) CGImageDestinationFinalize(destination) return data as Data } - + static func makeImageForPNGOptimizedSharing(image: CGImage) -> Data { let data = NSMutableData() - + let destination = CGImageDestinationCreateWithData( data, kUTTypePNG, 1, [:] as CFDictionary )! - + CGImageDestinationAddImage( destination, image, [ - kCGImageDestinationOptimizeColorForSharing: true, - ] as CFDictionary) - + kCGImageDestinationOptimizeColorForSharing: true + ] as CFDictionary + ) + CGImageDestinationFinalize(destination) - + return data as Data } } diff --git a/Sources/BrightroomEngine/Library/Optional+.swift b/Sources/BrightroomEngine/Library/Optional+.swift new file mode 100644 index 00000000..21ff4477 --- /dev/null +++ b/Sources/BrightroomEngine/Library/Optional+.swift @@ -0,0 +1,63 @@ +// +// Copyright (c) 2021 Hiroshi Kimura(Muukii) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +extension Optional { + internal func unwrap( + orThrow debugDescription: String? = nil, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line + ) throws -> Wrapped { + if let value = self { + return value + } + throw Optional.BrightroomUnwrappedNilError( + debugDescription, + file: file, + function: function, + line: line + ) + } + + public struct BrightroomUnwrappedNilError: Swift.Error, CustomDebugStringConvertible { + let file: StaticString + let function: StaticString + let line: UInt + + // MARK: Public + + public init( + _ debugDescription: String? = nil, + file: StaticString = #file, + function: StaticString = #function, + line: UInt = #line + ) { + self.debugDescription = debugDescription ?? "Failed to unwrap on \(file):\(function):\(line)" + self.file = file + self.function = function + self.line = line + } + + // MARK: CustomDebugStringConvertible + + public let debugDescription: String + } +} diff --git a/Sources/Demo/Contents/Components.swift b/Sources/Demo/Contents/Components.swift index 5572463a..a25762a5 100644 --- a/Sources/Demo/Contents/Components.swift +++ b/Sources/Demo/Contents/Components.swift @@ -7,17 +7,17 @@ import UIKit @testable import BrightroomEngine func makeMetadataString(image: UIImage) -> String { - let formatter = ByteCountFormatter() - formatter.countStyle = .file - let jpegSize = formatter.string( - fromByteCount: Int64(image.jpegData(compressionQuality: 1)!.count) - ) - +// let formatter = ByteCountFormatter() +// formatter.countStyle = .file +// +// let jpegSize = formatter.string( +// fromByteCount: Int64(image.jpegData(compressionQuality: 1)!.count) +// ) +// let cgImage = image.cgImage! let meta = """ size: \(image.size.width * image.scale), \(image.size.height * image.scale) - estimated-jpegSize: \(jpegSize) colorSpace: \(cgImage.colorSpace.map { String(describing: $0) } ?? "null") bit-depth: \(cgImage.bitsPerPixel / 4) bytesPerRow: \(cgImage.bytesPerRow) @@ -83,14 +83,28 @@ enum Components { } final class ResultImageCell: ASCellNode { + + private var currentWorkingID = UUID() + var image: UIImage? { didSet { if let image = image { renderedImageNode.image = image - optimizedForSharingImageNode.image = UIImage( - data: ImageTool.makeImageForJPEGOptimizedSharing(image: image.cgImage!) - ) + + currentWorkingID = UUID() + DispatchQueue.global(qos: .background).async { [currentWorkingID, weak self] in + guard let self = self else { return } + + let image = UIImage( + data: ImageTool.makeImageForJPEGOptimizedSharing(image: image.cgImage!) + ) + DispatchQueue.main.async { + guard self.currentWorkingID == currentWorkingID else { return } + self.optimizedForSharingImageNode.image = image + } + + } } else { renderedImageNode.image = nil diff --git a/Sources/Demo/Contents/DemoBuiltInEditorViewController.swift b/Sources/Demo/Contents/DemoBuiltInEditorViewController.swift index ad7ab8df..9e2fc610 100644 --- a/Sources/Demo/Contents/DemoBuiltInEditorViewController.swift +++ b/Sources/Demo/Contents/DemoBuiltInEditorViewController.swift @@ -113,7 +113,7 @@ final class DemoBuiltInEditorViewController: StackScrollNodeViewController { try! stack.makeRenderer().render { result in switch result { case let .success(rendered): - self.resultCell.image = rendered.uiImageDisplayP3 + self.resultCell.image = rendered.uiImage case let .failure(error): print(error) } diff --git a/Sources/Demo/Contents/DemoCropMenuViewController.swift b/Sources/Demo/Contents/DemoCropMenuViewController.swift index a518c8ea..f1939041 100644 --- a/Sources/Demo/Contents/DemoCropMenuViewController.swift +++ b/Sources/Demo/Contents/DemoCropMenuViewController.swift @@ -187,7 +187,7 @@ final class DemoCropMenuViewController: StackScrollNodeViewController { .render { (result) in switch result { case .success(let rendered): - self?.resultCell.image = rendered.uiImageDisplayP3 + self?.resultCell.image = rendered.uiImage case .failure(let error): print(error) } diff --git a/Sources/Demo/Contents/DemoMaskingViewController.swift b/Sources/Demo/Contents/DemoMaskingViewController.swift index 11b2db2f..ae2f7af5 100644 --- a/Sources/Demo/Contents/DemoMaskingViewController.swift +++ b/Sources/Demo/Contents/DemoMaskingViewController.swift @@ -76,7 +76,7 @@ final class DemoMaskingViewController: StackScrollNodeViewController { try! editingStack.makeRenderer().render { (result) in switch result { case .success(let rendered): - self.resultCell.image = rendered.uiImageDisplayP3 + self.resultCell.image = rendered.uiImage case .failure(let error): print(error) } diff --git a/Sources/Demo/Contents/Imitations/ImitationTinderViewController.swift b/Sources/Demo/Contents/Imitations/ImitationTinderViewController.swift index 27f6086c..780e7f91 100644 --- a/Sources/Demo/Contents/Imitations/ImitationTinderViewController.swift +++ b/Sources/Demo/Contents/Imitations/ImitationTinderViewController.swift @@ -47,7 +47,7 @@ final class ImitationTinderViewController: UIViewController { private func cropImage() { - let image = try! wrapper.currentCropView?.renderImage()?.uiImageDisplayP3 + let image = try! wrapper.currentCropView?.renderImage()?.uiImage print(image as Any) diff --git a/Sources/SwiftUIDemo/ContentView.swift b/Sources/SwiftUIDemo/ContentView.swift index 44c524af..8a8ff118 100644 --- a/Sources/SwiftUIDemo/ContentView.swift +++ b/Sources/SwiftUIDemo/ContentView.swift @@ -46,7 +46,7 @@ struct ContentView: View { Button("Crop: Horizontal") { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stackForHorizontal, onCompleted: { - self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -55,7 +55,7 @@ struct ContentView: View { Button("Crop: Vertical") { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stackForVertical, onCompleted: { - self.image = try! stackForVertical.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForVertical.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -64,7 +64,7 @@ struct ContentView: View { Button("Crop: Square") { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stackForSquare, onCompleted: { - self.image = try! stackForSquare.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForSquare.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -73,7 +73,7 @@ struct ContentView: View { Button("Crop: Nasa") { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stackForNasa, onCompleted: { - self.image = try! stackForNasa.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForNasa.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -82,7 +82,7 @@ struct ContentView: View { Button("Crop: Super small") { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stackForSmall, onCompleted: { - self.image = try! stackForSmall.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForSmall.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -97,7 +97,7 @@ struct ContentView: View { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stack, onCompleted: { - self.image = try! stack.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stack.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -112,7 +112,7 @@ struct ContentView: View { fullScreenView = .init { SwiftUIPhotosCropView(editingStack: stack, onCompleted: { - self.image = try! stack.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stack.makeRenderer().render().swiftUIImage self.fullScreenView = nil }) } @@ -131,7 +131,7 @@ struct ContentView: View { ) fullScreenView = .init { PixelEditWrapper(editingStack: stack) { - self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImage self.fullScreenView = nil } } @@ -143,7 +143,7 @@ struct ContentView: View { ) fullScreenView = .init { PixelEditWrapper(editingStack: stack) { - self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImageDisplayP3 + self.image = try! stackForHorizontal.makeRenderer().render().swiftUIImage self.fullScreenView = nil } } diff --git a/Sources/SwiftUIDemo/DemoCropView.swift b/Sources/SwiftUIDemo/DemoCropView.swift index d21f26e7..1332855a 100644 --- a/Sources/SwiftUIDemo/DemoCropView.swift +++ b/Sources/SwiftUIDemo/DemoCropView.swift @@ -35,7 +35,7 @@ struct DemoCropView: View { .clipped() } Button("Done") { - let image = try! editingStack.makeRenderer().render().swiftUIImageDisplayP3 + let image = try! editingStack.makeRenderer().render().swiftUIImage print(image) } } diff --git a/Tests/BrightroomEngineTests/LoadingTests.swift b/Tests/BrightroomEngineTests/LoadingTests.swift index eca375c1..bc486677 100644 --- a/Tests/BrightroomEngineTests/LoadingTests.swift +++ b/Tests/BrightroomEngineTests/LoadingTests.swift @@ -53,21 +53,21 @@ final class LoadingTests: XCTestCase { return result! } - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5528", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.right.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_right", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.right.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5529", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.down.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_down", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.down.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5530", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.left.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_left", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.left.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5531", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.up.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_up", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.up.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5532", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.leftMirrored.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_left_mirrored", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.leftMirrored.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5533", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.downMirrored.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_down_mirrored", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.downMirrored.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5534", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.rightMirrored.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_right_mirrored", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.rightMirrored.rawValue) - XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "IMG_5535", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.upMirrored.rawValue) + XCTAssertEqual(fetch(image: try ImageProvider(fileURL: _url(forResource: "orientation_up_mirrored", ofType: "HEIC"))).rawValue, CGImagePropertyOrientation.upMirrored.rawValue) } diff --git a/Tests/BrightroomEngineTests/RendererOrientationTests.swift b/Tests/BrightroomEngineTests/RendererOrientationTests.swift new file mode 100644 index 00000000..ba4bcd7d --- /dev/null +++ b/Tests/BrightroomEngineTests/RendererOrientationTests.swift @@ -0,0 +1,106 @@ +// +// RendererOrientationTests.swift +// BrightroomEngineTests +// +// Created by Muukii on 2021/03/30. +// Copyright © 2021 muukii. All rights reserved. +// + +import XCTest + +@testable import BrightroomEngine + +final class RendererOrientationTests: XCTestCase { + + private func run(image: UIImage, orientation: CGImagePropertyOrientation) throws + -> ImageRenderer.Rendered + { + + let imageSource = ImageSource(image: image) + let renderer = ImageRenderer(source: imageSource, orientation: orientation) + + let rendered = try renderer.render() + XCTAssert(rendered.engine == .coreGraphics) + return rendered + } + + func testOrientationRight() throws { + let r = try run( + image: UIImage(named: "orientation_right.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .right + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationDown() throws { + let r = try run( + image: UIImage(named: "orientation_down.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .down + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationLeft() throws { + let r = try run( + image: UIImage(named: "orientation_left.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .left + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationUp() throws { + let r = try run( + image: UIImage(named: "orientation_up.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .up + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationLeftMirrored() throws { + let r = try run( + image: UIImage(named: "orientation_left_mirrored.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .leftMirrored + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationDownMirrored() throws { + let r = try run( + image: UIImage(named: "orientation_down_mirrored.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .downMirrored + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationRightMirrored() throws { + let r = try run( + image: UIImage(named: "orientation_right_mirrored.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .rightMirrored + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } + + func testOrientationUpMirrored() throws { + let r = try run( + image: UIImage(named: "orientation_up_mirrored.HEIC", in: _pixelengine_bundle, with: nil)!, + orientation: .upMirrored + ) + let cgImage = r.cgImage + let uiImage = r.uiImage + print(cgImage, uiImage) + } +} diff --git a/Tests/BrightroomEngineTests/RendererTests.swift b/Tests/BrightroomEngineTests/RendererTests.swift index b04b64ad..6aaacd84 100644 --- a/Tests/BrightroomEngineTests/RendererTests.swift +++ b/Tests/BrightroomEngineTests/RendererTests.swift @@ -57,7 +57,7 @@ final class RendererTests: XCTestCase { let renderer = ImageRenderer(source: imageSource, orientation: .up) - let image = try renderer.render().cgImageDisplayP3 + let image = try renderer.render(options: .init(workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) } @@ -70,7 +70,7 @@ final class RendererTests: XCTestCase { let renderer = ImageRenderer(source: imageSource, orientation: .up) - let image = try renderer.render().cgImageDisplayP3 + let image = try renderer.render(options: .init(workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) } @@ -88,7 +88,7 @@ final class RendererTests: XCTestCase { renderer.edit.modifiers = [filter] - let image = try renderer.render().cgImageDisplayP3 + let image = try renderer.render(options: .init(workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) } @@ -113,7 +113,7 @@ final class RendererTests: XCTestCase { drawer: [] ) - let image = try renderer.render().cgImageDisplayP3 + let image = try renderer.render(options: .init(workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) } @@ -138,7 +138,7 @@ final class RendererTests: XCTestCase { drawer: [] ) - let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300))).cgImageDisplayP3 + let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300), workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssert(image.width == 300 || image.height == 300) XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) @@ -162,7 +162,7 @@ final class RendererTests: XCTestCase { drawer: [] ) - let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300))).cgImageDisplayP3 + let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300), workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssert(image.width == 300 || image.height == 300) XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3) @@ -201,11 +201,11 @@ final class RendererTests: XCTestCase { drawer: [mask] ) - let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300))).cgImageDisplayP3 + let image = try renderer.render(options: .init(resolution: .resize(maxPixelSize: 300), workingColorSpace: ColorSpaces.displayP3)).cgImage #if false // for debugging quickly - try UIImage(cgImageDisplayP3: image).jpegData(compressionQuality: 1)?.write(to: URL(fileURLWithPath: "/Users/muukii/Desktop/rendered.jpg")) + try UIImage(cgImage: image).jpegData(compressionQuality: 1)?.write(to: URL(fileURLWithPath: "/Users/muukii/Desktop/rendered.jpg")) #endif // XCTAssert(image.width == 300 || image.height == 300) @@ -220,7 +220,7 @@ final class RendererTests: XCTestCase { let renderer = ImageRenderer(source: imageSource, orientation: .up) - let image = try renderer.render().cgImageDisplayP3 + let image = try renderer.render(options: .init(workingColorSpace: ColorSpaces.displayP3)).cgImage XCTAssertEqual(image.colorSpace, ColorSpaces.displayP3)