From 229f52535d80f614d16cc1e45cc3d4a7b7eace5b Mon Sep 17 00:00:00 2001 From: Duncan Wilcox Date: Sun, 17 May 2026 16:24:55 +0200 Subject: [PATCH] Sitely IJSVG branch: iOS port, SVG filters, PSNR test harness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This consolidates ~55 patches accumulated in Sitely's tree since 2024. The themes: iOS / iPadOS port - New IJSVGPlatform.{h,m} centralizes macOS/iOS differences: AppKit/UIKit imports, NSColor/Image/View/Font/BezierPath shims, geometry typedefs and macros, NSValue (IJSVGGeometry) category bridging NS-named methods to their CG-named iOS counterparts, and a UIView (IJSVGNeedsDisplay) category exposing a BOOL-taking -setNeedsDisplay: shim. - New IJSVGiOSXML.{h,m} provides a libxml2-backed NSXMLNode/Element/Document replacement for iOS (Foundation on iOS doesn't ship NSXML*). - Existing headers migrated from direct + imports to . - Sitely.h indirection removed from framework headers; NSGraphicsGetCurrentContext is provided by IJSVGPlatform (inline on macOS, UIGraphicsGetCurrentContext on iOS). SVG filter pipeline (biggest body of work) - Filter graph + 7 filter effects on top of the existing IJSVGFilterEffectGaussianBlur: IJSVGFilterEffectBlend, ColorMatrix, Composite, DisplacementMap, Flood, Lighting, Merge, Offset, Turbulence; plus IJSVGFilterGraph orchestrating the pipeline with CALayer→bitmap replacement rendering. - filterUnits parsing and filter region attributes honored so SVGs with missing attributes no longer trigger huge-bitmap allocations. - feOffset rendering fixed; filter export round-trip works. - Filter rasterization respects the root viewBox transform. - Filter layer CGImage lifetime made safe across redraws. - Filter region stabilized at 10% with vImage edge extension. - Filter-before-opacity rendering: source graphic rendered at full opacity (+26 dB on the gradient-filter baseline). - Blur calibration matching WebKit (CIGaussianBlur with padded source bitmap, after experimenting with a vImage 3-pass box blur path). - Turbulence Y-flip corrected. Gradients / patterns / masks - spreadMethod="reflect" implemented for linear gradients (+15.8 dB on the car baseline). - Gradient mask rendering works (masks with gradient content). - SVG viewport, gradient, and pattern regressions resolved. - Pattern image transform handling improved. - Clip-path transform propagation fixed. - Imported upstream gradient-stop-parsing fixes. Color space - All deviceRGB references converted to sRGB throughout the framework. - rgb() colors use sRGB (+9 dB on the tommek car baseline). Parser robustness - Forward-reference prescan so inside can target elements defined later in the document. - Realloc bug fixed for SVGs with more than 5 transforms. - NaN/Inf attribute workarounds in gradient parsing. - IJSVGParser crash on certain SVGs ("circled-arrow-left.svg") fixed. - IJSVGExporter crashes fixed. CoreAnimation / memory / stability - Background-thread CATransaction crash patched. - Numerous memory leaks fixed. - @available guards added for APIs not available in 10.14.4 / iOS 17. - All in-framework logging removed. Test harness (in IJSVGExample) - New IJSVGWebKitPSNRTests.m: renders an SVG with IJSVG and snapshots the same SVG in a WKWebView, writes IJSVG/WebKit/diff PNGs and a PSNR results log. Overridable via IJSVG_OUTPUT_DIR and IJSVG_WEBKIT_SETTLE_DELAY env vars. - IJSVGExampleTests converted to a standalone (logic) test bundle: TEST_HOST/BUNDLE_LOADER removed, IJSVG.framework linked directly and embedded in the .xctest bundle, SVG resources bundled in the test target, scheme promoted from xcuserdata to xcshareddata. - WebKit.framework linked into the test target. --- .../IJSVG/IJSVG.xcodeproj/project.pbxproj | 104 +++ .../Source/Additions/NSImage+IJSVGAdditions.h | 5 +- .../Source/Additions/NSImage+IJSVGAdditions.m | 20 + .../IJSVG/IJSVG/Source/Colors/IJSVGColor.h | 57 +- .../IJSVG/IJSVG/Source/Colors/IJSVGColor.m | 169 +++- .../IJSVG/Source/Colors/IJSVGTraitedColor.h | 3 +- .../IJSVG/Source/Commands/IJSVGCommand.m | 15 - Framework/IJSVG/IJSVG/Source/Core/IJSVG.h | 9 +- Framework/IJSVG/IJSVG/Source/Core/IJSVG.m | 272 +++++-- .../IJSVG/IJSVG/Source/Core/IJSVGImageRep.h | 6 +- .../IJSVG/IJSVG/Source/Core/IJSVGImageRep.m | 76 +- .../IJSVG/IJSVG/Source/Core/IJSVGPlatform.h | 70 ++ .../IJSVG/IJSVG/Source/Core/IJSVGPlatform.m | 32 + .../IJSVG/IJSVG/Source/Core/IJSVGUmbrella.h | 13 + Framework/IJSVG/IJSVG/Source/Core/IJSVGView.h | 2 +- Framework/IJSVG/IJSVG/Source/Core/IJSVGView.m | 4 + Framework/IJSVG/IJSVG/Source/Core/IJSVGXML.h | 16 + .../IJSVG/IJSVG/Source/Core/IJSVGiOSXML.h | 106 +++ .../IJSVG/IJSVG/Source/Core/IJSVGiOSXML.m | 759 ++++++++++++++++++ .../IJSVG/Source/Exporter/IJSVGExporter.h | 1 + .../IJSVG/Source/Exporter/IJSVGExporter.m | 36 +- .../IJSVG/Source/Layers/IJSVGFilterLayer.m | 66 +- .../IJSVG/Source/Layers/IJSVGGradientLayer.m | 26 +- .../IJSVG/Source/Layers/IJSVGImageLayer.h | 2 +- .../IJSVG/Source/Layers/IJSVGImageLayer.m | 3 +- .../IJSVG/IJSVG/Source/Layers/IJSVGLayer.h | 5 +- .../IJSVG/IJSVG/Source/Layers/IJSVGLayer.m | 283 +++++-- .../IJSVG/Source/Layers/IJSVGPatternLayer.m | 74 +- .../IJSVG/Source/Layers/IJSVGRootLayer.h | 3 + .../IJSVG/Source/Layers/IJSVGRootLayer.m | 67 +- .../IJSVG/Source/Layers/IJSVGShapeLayer.m | 5 +- .../IJSVG/Source/Layers/IJSVGTileLayer.m | 4 +- .../IJSVG/Source/Layers/IJSVGTransformLayer.m | 3 +- .../Filter Effects/IJSVGFilterEffectBlend.h | 21 + .../Filter Effects/IJSVGFilterEffectBlend.m | 50 ++ .../IJSVGFilterEffectColorMatrix.h | 20 + .../IJSVGFilterEffectColorMatrix.m | 125 +++ .../IJSVGFilterEffectComposite.h | 26 + .../IJSVGFilterEffectComposite.m | 130 +++ .../IJSVGFilterEffectDisplacementMap.h | 21 + .../IJSVGFilterEffectDisplacementMap.m | 230 ++++++ .../Filter Effects/IJSVGFilterEffectFlood.h | 13 + .../Filter Effects/IJSVGFilterEffectFlood.m | 53 ++ .../IJSVGFilterEffectGaussianBlur.h | 2 + .../IJSVGFilterEffectGaussianBlur.m | 50 +- .../IJSVGFilterEffectLighting.h | 72 ++ .../IJSVGFilterEffectLighting.m | 555 +++++++++++++ .../Filter Effects/IJSVGFilterEffectMerge.h | 9 + .../Filter Effects/IJSVGFilterEffectMerge.m | 40 + .../Filter Effects/IJSVGFilterEffectOffset.h | 13 + .../Filter Effects/IJSVGFilterEffectOffset.m | 28 + .../IJSVGFilterEffectTurbulence.h | 21 + .../IJSVGFilterEffectTurbulence.m | 297 +++++++ .../IJSVG/IJSVG/Source/Nodes/IJSVGFilter.h | 6 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGFilter.m | 357 +++++++- .../IJSVG/Source/Nodes/IJSVGFilterEffect.h | 10 + .../IJSVG/Source/Nodes/IJSVGFilterEffect.m | 86 +- .../IJSVG/Source/Nodes/IJSVGFilterGraph.h | 24 + .../IJSVG/Source/Nodes/IJSVGFilterGraph.m | 71 ++ .../IJSVG/IJSVG/Source/Nodes/IJSVGGradient.h | 9 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGGradient.m | 15 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGGroup.h | 3 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGGroup.m | 12 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGImage.m | 23 +- .../IJSVG/Source/Nodes/IJSVGLinearGradient.m | 101 ++- .../IJSVG/IJSVG/Source/Nodes/IJSVGMask.h | 2 + .../IJSVG/IJSVG/Source/Nodes/IJSVGNode.h | 6 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGNode.m | 15 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGPath.m | 20 +- .../IJSVG/Source/Nodes/IJSVGRadialGradient.m | 30 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.h | 5 +- .../IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.m | 22 +- .../IJSVG/IJSVG/Source/Parsing/IJSVGParser.h | 12 +- .../IJSVG/IJSVG/Source/Parsing/IJSVGParser.m | 664 +++++++++++---- .../IJSVG/Source/Rendering/IJSVGLayerTree.m | 372 ++++++++- .../IJSVG/IJSVG/Source/Rendering/IJSVGStyle.h | 3 +- .../IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.h | 2 - .../IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.m | 8 - .../IJSVG/Source/Utils/IJSVGBitFlags64.m | 10 - .../IJSVG/Source/Utils/IJSVGFeatureFlags.h | 1 - .../IJSVG/Source/Utils/IJSVGFeatureFlags.m | 5 +- .../IJSVG/Source/Utils/IJSVGThreadManager.h | 1 + .../IJSVG/Source/Utils/IJSVGThreadManager.m | 29 + .../IJSVG/Source/Utils/IJSVGTransaction.m | 8 +- .../IJSVG/IJSVG/Source/Utils/IJSVGTransform.h | 2 + .../IJSVG/IJSVG/Source/Utils/IJSVGTransform.m | 4 + .../IJSVG/IJSVG/Source/Utils/IJSVGUnitPoint.m | 1 + .../IJSVG/IJSVG/Source/Utils/IJSVGUnitRect.m | 1 + .../IJSVG/IJSVG/Source/Utils/IJSVGUnitSize.m | 1 + .../IJSVG/IJSVG/Source/Utils/IJSVGUtils.h | 5 +- .../IJSVG/IJSVG/Source/Utils/IJSVGUtils.m | 22 +- .../IJSVG/IJSVG/Source/Utils/IJSVGViewBox.h | 2 +- .../IJSVG/IJSVG/Source/Utils/IJSVGViewBox.m | 11 +- .../IJSVGExample.xcodeproj/project.pbxproj | 43 +- .../UserInterfaceState.xcuserstate | Bin 0 -> 9236 bytes .../xcschemes/IJSVGExample.xcscheme | 123 +++ .../IJSVGExampleTests/AJ_Digital_Camera.svg | 9 + IJSVGExample/IJSVGExampleTests/Group.svg | 13 + .../IJSVGExampleTests/IJSVGWebKitPSNRTests.m | 555 +++++++++++++ IJSVGExample/IJSVGExampleTests/NewTux.svg | 75 ++ IJSVGExample/IJSVGExampleTests/Rectangle.svg | 19 + 101 files changed, 6224 insertions(+), 686 deletions(-) create mode 100644 Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.h create mode 100644 Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.m create mode 100644 Framework/IJSVG/IJSVG/Source/Core/IJSVGXML.h create mode 100644 Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.h create mode 100644 Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.m create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.h create mode 100644 Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.m create mode 100644 IJSVGExample/IJSVGExample.xcodeproj/project.xcworkspace/xcuserdata/duncan.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 IJSVGExample/IJSVGExample.xcodeproj/xcshareddata/xcschemes/IJSVGExample.xcscheme create mode 100644 IJSVGExample/IJSVGExampleTests/AJ_Digital_Camera.svg create mode 100644 IJSVGExample/IJSVGExampleTests/Group.svg create mode 100644 IJSVGExample/IJSVGExampleTests/IJSVGWebKitPSNRTests.m create mode 100644 IJSVGExample/IJSVGExampleTests/NewTux.svg create mode 100644 IJSVGExample/IJSVGExampleTests/Rectangle.svg diff --git a/Framework/IJSVG/IJSVG.xcodeproj/project.pbxproj b/Framework/IJSVG/IJSVG.xcodeproj/project.pbxproj index e93fc822..15cea2cd 100644 --- a/Framework/IJSVG/IJSVG.xcodeproj/project.pbxproj +++ b/Framework/IJSVG/IJSVG.xcodeproj/project.pbxproj @@ -7,6 +7,14 @@ objects = { /* Begin PBXBuildFile section */ + 0893EEECA2CD4BFB9D30BE68 /* IJSVGFilterGraph.m in Sources */ = {isa = PBXBuildFile; fileRef = F071B24FEE8B4FEB9B6F773A /* IJSVGFilterGraph.m */; }; + 0917E1B8069A479A88642650 /* IJSVGFilterGraph.h in Headers */ = {isa = PBXBuildFile; fileRef = 1089C96FE8554A91A7FE3868 /* IJSVGFilterGraph.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F44633E892C4455A55FCB96 /* IJSVGFilterEffectOffset.m in Sources */ = {isa = PBXBuildFile; fileRef = 117060A9A4A44795B2C77B8D /* IJSVGFilterEffectOffset.m */; }; + 10C408B7CCEB45429F15B26B /* IJSVGPlatform.m in Sources */ = {isa = PBXBuildFile; fileRef = E523BF812CEF4113B273258A /* IJSVGPlatform.m */; }; + 1A11B9806F8740C486B6E1F4 /* IJSVGFilterEffectTurbulence.m in Sources */ = {isa = PBXBuildFile; fileRef = 1713064E8E98448196AFE790 /* IJSVGFilterEffectTurbulence.m */; }; + 1A7CD17725084FC98D31BECD /* IJSVGFilterEffectTurbulence.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B401A68E5974CC690CEDF16 /* IJSVGFilterEffectTurbulence.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2CACA7F70DC54122B22B01CF /* IJSVGiOSXML.m in Sources */ = {isa = PBXBuildFile; fileRef = 02E5E6EE963A418592727F39 /* IJSVGiOSXML.m */; }; + 3BF6F69290E848A0999946A8 /* IJSVGFilterEffectDisplacementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 54C59F29A552476586DEECDC /* IJSVGFilterEffectDisplacementMap.m */; }; 590AA08D280DDD90002BBE12 /* IJSVGFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 590AA08B280DDD90002BBE12 /* IJSVGFilter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 590AA08E280DDD90002BBE12 /* IJSVGFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = 590AA08C280DDD90002BBE12 /* IJSVGFilter.m */; }; 590AA091280DDDA3002BBE12 /* IJSVGFilterEffect.h in Headers */ = {isa = PBXBuildFile; fileRef = 590AA08F280DDDA3002BBE12 /* IJSVGFilterEffect.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -19,6 +27,7 @@ 5910A9A728427AD600BD1F03 /* IJSVGMask.m in Sources */ = {isa = PBXBuildFile; fileRef = 5910A9A528427AD600BD1F03 /* IJSVGMask.m */; }; 5910A9AA284377D600BD1F03 /* IJSVGClipPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 5910A9A8284377D600BD1F03 /* IJSVGClipPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5910A9AB284377D600BD1F03 /* IJSVGClipPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 5910A9A9284377D600BD1F03 /* IJSVGClipPath.m */; }; + 5912B27570F54C9A9347C602 /* IJSVGPlatform.h in Headers */ = {isa = PBXBuildFile; fileRef = 8FD98ACFA2B14E65AC8C3A3F /* IJSVGPlatform.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5919111E28C7F1160047791B /* IJSVGBitFlags.h in Headers */ = {isa = PBXBuildFile; fileRef = 5919111C28C7F1160047791B /* IJSVGBitFlags.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5919111F28C7F1160047791B /* IJSVGBitFlags.m in Sources */ = {isa = PBXBuildFile; fileRef = 5919111D28C7F1160047791B /* IJSVGBitFlags.m */; }; 5919E65723F47FF60051873A /* IJSVGUnitRect.h in Headers */ = {isa = PBXBuildFile; fileRef = 5919E65523F47FF60051873A /* IJSVGUnitRect.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -167,9 +176,39 @@ 59FDBF0127F3454800AF7038 /* IJSVGColorNode.m in Sources */ = {isa = PBXBuildFile; fileRef = 59FDBEFF27F3454800AF7038 /* IJSVGColorNode.m */; }; 59FE8E4328C7E70800AB38B3 /* IJSVGStop.m in Sources */ = {isa = PBXBuildFile; fileRef = 59FE8E4128C7E70800AB38B3 /* IJSVGStop.m */; }; 59FE8E4428C7E70800AB38B3 /* IJSVGStop.h in Headers */ = {isa = PBXBuildFile; fileRef = 59FE8E4228C7E70800AB38B3 /* IJSVGStop.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5A4B17FCBE654E498DFF5E5F /* IJSVGFilterEffectBlend.m in Sources */ = {isa = PBXBuildFile; fileRef = 4688D72B532C46CD97BA04BE /* IJSVGFilterEffectBlend.m */; }; + 66A8323C2F634D6F996A9BCF /* IJSVGFilterEffectColorMatrix.m in Sources */ = {isa = PBXBuildFile; fileRef = DB68D82674E641AB9D96FE3E /* IJSVGFilterEffectColorMatrix.m */; }; + 7CD3364BE36E4D95BC2CD9D4 /* IJSVGFilterEffectLighting.m in Sources */ = {isa = PBXBuildFile; fileRef = 8650C9A5545C4EE393ACFE21 /* IJSVGFilterEffectLighting.m */; }; + 8163C213FC644A15A0145CA3 /* IJSVGFilterEffectDisplacementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D6228142FC34B06B7059935 /* IJSVGFilterEffectDisplacementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A8702AEC22AD403A9090C9B1 /* IJSVGFilterEffectMerge.h in Headers */ = {isa = PBXBuildFile; fileRef = 79361B5FD3354A34BF79A6D0 /* IJSVGFilterEffectMerge.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B20D16FB6DAD44AB9A47AFCD /* IJSVGFilterEffectComposite.m in Sources */ = {isa = PBXBuildFile; fileRef = 0845BEC3D83646CB9F952495 /* IJSVGFilterEffectComposite.m */; }; + B2E2E3062CF144D7A09EBA8A /* IJSVGFilterEffectOffset.h in Headers */ = {isa = PBXBuildFile; fileRef = 622921DC36C1467B85F76B20 /* IJSVGFilterEffectOffset.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B381DF965BBC4A8F89A6ECC5 /* IJSVGiOSXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 59EF4D90DABF458EA6BBE02A /* IJSVGiOSXML.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BCCBC1A5FC8E4E93B3056AA0 /* IJSVGFilterEffectMerge.m in Sources */ = {isa = PBXBuildFile; fileRef = 4F1F6F27180B4F8BB5BB4224 /* IJSVGFilterEffectMerge.m */; }; + BFC1C1F3D038457A8CD90035 /* IJSVGFilterEffectFlood.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B5E904B0D2E471E8AA878A6 /* IJSVGFilterEffectFlood.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C71C71B08FD745439367968E /* IJSVGFilterEffectComposite.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F0368F2D97F4FDA99F4D180 /* IJSVGFilterEffectComposite.h */; settings = {ATTRIBUTES = (Public, ); }; }; + CE121F6FD5884612B33EAB40 /* IJSVGFilterEffectFlood.m in Sources */ = {isa = PBXBuildFile; fileRef = 33A3327E224C40AAAE5D8DD3 /* IJSVGFilterEffectFlood.m */; }; + CFE695A1EF6A409885709FAB /* IJSVGFilterEffectBlend.h in Headers */ = {isa = PBXBuildFile; fileRef = B86E7AE795BC46B78C571FBD /* IJSVGFilterEffectBlend.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D5B4087217124F46A1D31823 /* IJSVGFilterEffectColorMatrix.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D8777A279B649B4BF0D80F9 /* IJSVGFilterEffectColorMatrix.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F199F5BF12AA4B8DB9EE2F6A /* IJSVGFilterEffectLighting.h in Headers */ = {isa = PBXBuildFile; fileRef = AD5A182FDFBF49AD8B9277A2 /* IJSVGFilterEffectLighting.h */; settings = {ATTRIBUTES = (Public, ); }; }; + FDE43A264C4C4338A79D155C /* IJSVGXML.h in Headers */ = {isa = PBXBuildFile; fileRef = 00ACBC06AF2249999E867A88 /* IJSVGXML.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 00ACBC06AF2249999E867A88 /* IJSVGXML.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGXML.h; sourceTree = ""; }; + 02E5E6EE963A418592727F39 /* IJSVGiOSXML.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGiOSXML.m; sourceTree = ""; }; + 0845BEC3D83646CB9F952495 /* IJSVGFilterEffectComposite.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectComposite.m; sourceTree = ""; }; + 0B5E904B0D2E471E8AA878A6 /* IJSVGFilterEffectFlood.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectFlood.h; sourceTree = ""; }; + 1089C96FE8554A91A7FE3868 /* IJSVGFilterGraph.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterGraph.h; sourceTree = ""; }; + 117060A9A4A44795B2C77B8D /* IJSVGFilterEffectOffset.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectOffset.m; sourceTree = ""; }; + 1713064E8E98448196AFE790 /* IJSVGFilterEffectTurbulence.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectTurbulence.m; sourceTree = ""; }; + 1B401A68E5974CC690CEDF16 /* IJSVGFilterEffectTurbulence.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectTurbulence.h; sourceTree = ""; }; + 2D8777A279B649B4BF0D80F9 /* IJSVGFilterEffectColorMatrix.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectColorMatrix.h; sourceTree = ""; }; + 33A3327E224C40AAAE5D8DD3 /* IJSVGFilterEffectFlood.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectFlood.m; sourceTree = ""; }; + 3F0368F2D97F4FDA99F4D180 /* IJSVGFilterEffectComposite.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectComposite.h; sourceTree = ""; }; + 4688D72B532C46CD97BA04BE /* IJSVGFilterEffectBlend.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectBlend.m; sourceTree = ""; }; + 4F1F6F27180B4F8BB5BB4224 /* IJSVGFilterEffectMerge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectMerge.m; sourceTree = ""; }; + 54C59F29A552476586DEECDC /* IJSVGFilterEffectDisplacementMap.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectDisplacementMap.m; sourceTree = ""; }; 590AA08B280DDD90002BBE12 /* IJSVGFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilter.h; sourceTree = ""; }; 590AA08C280DDD90002BBE12 /* IJSVGFilter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilter.m; sourceTree = ""; }; 590AA08F280DDDA3002BBE12 /* IJSVGFilterEffect.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffect.h; sourceTree = ""; }; @@ -321,6 +360,7 @@ 59EB75D323905F7300F5AE63 /* IJSVGRendering.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IJSVGRendering.h; sourceTree = ""; }; 59EB75D423905F7300F5AE63 /* IJSVGExporter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IJSVGExporter.h; sourceTree = ""; }; 59EB75D523905F7300F5AE63 /* IJSVGRendering.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IJSVGRendering.m; sourceTree = ""; }; + 59EF4D90DABF458EA6BBE02A /* IJSVGiOSXML.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGiOSXML.h; sourceTree = ""; }; 59F36506262F1ABB00BCE3FD /* IJSVGTraitedColor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGTraitedColor.h; sourceTree = ""; }; 59F36507262F1ABB00BCE3FD /* IJSVGTraitedColor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGTraitedColor.m; sourceTree = ""; }; 59F38A61280ED99200804FE4 /* IJSVGBasicLayer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGBasicLayer.h; sourceTree = ""; }; @@ -333,6 +373,16 @@ 59FDBEFF27F3454800AF7038 /* IJSVGColorNode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGColorNode.m; sourceTree = ""; }; 59FE8E4128C7E70800AB38B3 /* IJSVGStop.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IJSVGStop.m; sourceTree = ""; }; 59FE8E4228C7E70800AB38B3 /* IJSVGStop.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IJSVGStop.h; sourceTree = ""; }; + 622921DC36C1467B85F76B20 /* IJSVGFilterEffectOffset.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectOffset.h; sourceTree = ""; }; + 79361B5FD3354A34BF79A6D0 /* IJSVGFilterEffectMerge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectMerge.h; sourceTree = ""; }; + 7D6228142FC34B06B7059935 /* IJSVGFilterEffectDisplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectDisplacementMap.h; sourceTree = ""; }; + 8650C9A5545C4EE393ACFE21 /* IJSVGFilterEffectLighting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectLighting.m; sourceTree = ""; }; + 8FD98ACFA2B14E65AC8C3A3F /* IJSVGPlatform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGPlatform.h; sourceTree = ""; }; + AD5A182FDFBF49AD8B9277A2 /* IJSVGFilterEffectLighting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectLighting.h; sourceTree = ""; }; + B86E7AE795BC46B78C571FBD /* IJSVGFilterEffectBlend.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = IJSVGFilterEffectBlend.h; sourceTree = ""; }; + DB68D82674E641AB9D96FE3E /* IJSVGFilterEffectColorMatrix.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterEffectColorMatrix.m; sourceTree = ""; }; + E523BF812CEF4113B273258A /* IJSVGPlatform.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGPlatform.m; sourceTree = ""; }; + F071B24FEE8B4FEB9B6F773A /* IJSVGFilterGraph.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGFilterGraph.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -355,6 +405,24 @@ children = ( 590AA094280DE668002BBE12 /* IJSVGFilterEffectGaussianBlur.h */, 590AA095280DE668002BBE12 /* IJSVGFilterEffectGaussianBlur.m */, + B86E7AE795BC46B78C571FBD /* IJSVGFilterEffectBlend.h */, + 4688D72B532C46CD97BA04BE /* IJSVGFilterEffectBlend.m */, + 2D8777A279B649B4BF0D80F9 /* IJSVGFilterEffectColorMatrix.h */, + DB68D82674E641AB9D96FE3E /* IJSVGFilterEffectColorMatrix.m */, + 3F0368F2D97F4FDA99F4D180 /* IJSVGFilterEffectComposite.h */, + 0845BEC3D83646CB9F952495 /* IJSVGFilterEffectComposite.m */, + 7D6228142FC34B06B7059935 /* IJSVGFilterEffectDisplacementMap.h */, + 54C59F29A552476586DEECDC /* IJSVGFilterEffectDisplacementMap.m */, + 0B5E904B0D2E471E8AA878A6 /* IJSVGFilterEffectFlood.h */, + 33A3327E224C40AAAE5D8DD3 /* IJSVGFilterEffectFlood.m */, + AD5A182FDFBF49AD8B9277A2 /* IJSVGFilterEffectLighting.h */, + 8650C9A5545C4EE393ACFE21 /* IJSVGFilterEffectLighting.m */, + 79361B5FD3354A34BF79A6D0 /* IJSVGFilterEffectMerge.h */, + 4F1F6F27180B4F8BB5BB4224 /* IJSVGFilterEffectMerge.m */, + 622921DC36C1467B85F76B20 /* IJSVGFilterEffectOffset.h */, + 117060A9A4A44795B2C77B8D /* IJSVGFilterEffectOffset.m */, + 1B401A68E5974CC690CEDF16 /* IJSVGFilterEffectTurbulence.h */, + 1713064E8E98448196AFE790 /* IJSVGFilterEffectTurbulence.m */, ); path = "Filter Effects"; sourceTree = ""; @@ -498,6 +566,8 @@ 5910A9A528427AD600BD1F03 /* IJSVGMask.m */, 5910A9A8284377D600BD1F03 /* IJSVGClipPath.h */, 5910A9A9284377D600BD1F03 /* IJSVGClipPath.m */, + 1089C96FE8554A91A7FE3868 /* IJSVGFilterGraph.h */, + F071B24FEE8B4FEB9B6F773A /* IJSVGFilterGraph.m */, ); path = Nodes; sourceTree = ""; @@ -625,6 +695,11 @@ 59EB75CD23905F7200F5AE63 /* IJSVGView.h */, 59EB75C123905F7100F5AE63 /* IJSVGView.m */, 595FD12E29EC56BA00666897 /* IJSVGUmbrella.h */, + 00ACBC06AF2249999E867A88 /* IJSVGXML.h */, + 8FD98ACFA2B14E65AC8C3A3F /* IJSVGPlatform.h */, + 59EF4D90DABF458EA6BBE02A /* IJSVGiOSXML.h */, + 02E5E6EE963A418592727F39 /* IJSVGiOSXML.m */, + E523BF812CEF4113B273258A /* IJSVGPlatform.m */, ); path = Core; sourceTree = ""; @@ -733,6 +808,19 @@ 5955731F286C643900156047 /* IJSVGTileLayer.h in Headers */, 59DB84E028C9F70000A9696A /* IJSVGBitFlags64.h in Headers */, 59EB760B23905F7300F5AE63 /* IJSVGStyleSheetSelector.h in Headers */, + CFE695A1EF6A409885709FAB /* IJSVGFilterEffectBlend.h in Headers */, + D5B4087217124F46A1D31823 /* IJSVGFilterEffectColorMatrix.h in Headers */, + C71C71B08FD745439367968E /* IJSVGFilterEffectComposite.h in Headers */, + 8163C213FC644A15A0145CA3 /* IJSVGFilterEffectDisplacementMap.h in Headers */, + BFC1C1F3D038457A8CD90035 /* IJSVGFilterEffectFlood.h in Headers */, + F199F5BF12AA4B8DB9EE2F6A /* IJSVGFilterEffectLighting.h in Headers */, + A8702AEC22AD403A9090C9B1 /* IJSVGFilterEffectMerge.h in Headers */, + B2E2E3062CF144D7A09EBA8A /* IJSVGFilterEffectOffset.h in Headers */, + 1A7CD17725084FC98D31BECD /* IJSVGFilterEffectTurbulence.h in Headers */, + 0917E1B8069A479A88642650 /* IJSVGFilterGraph.h in Headers */, + FDE43A264C4C4338A79D155C /* IJSVGXML.h in Headers */, + 5912B27570F54C9A9347C602 /* IJSVGPlatform.h in Headers */, + B381DF965BBC4A8F89A6ECC5 /* IJSVGiOSXML.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -882,6 +970,18 @@ 59EB75F123905F7300F5AE63 /* IJSVGUtils.m in Sources */, 59EB760A23905F7300F5AE63 /* IJSVGCommandMove.m in Sources */, 59EB761B23905F7300F5AE63 /* IJSVGPattern.m in Sources */, + 5A4B17FCBE654E498DFF5E5F /* IJSVGFilterEffectBlend.m in Sources */, + 66A8323C2F634D6F996A9BCF /* IJSVGFilterEffectColorMatrix.m in Sources */, + B20D16FB6DAD44AB9A47AFCD /* IJSVGFilterEffectComposite.m in Sources */, + 3BF6F69290E848A0999946A8 /* IJSVGFilterEffectDisplacementMap.m in Sources */, + CE121F6FD5884612B33EAB40 /* IJSVGFilterEffectFlood.m in Sources */, + 7CD3364BE36E4D95BC2CD9D4 /* IJSVGFilterEffectLighting.m in Sources */, + BCCBC1A5FC8E4E93B3056AA0 /* IJSVGFilterEffectMerge.m in Sources */, + 0F44633E892C4455A55FCB96 /* IJSVGFilterEffectOffset.m in Sources */, + 1A11B9806F8740C486B6E1F4 /* IJSVGFilterEffectTurbulence.m in Sources */, + 0893EEECA2CD4BFB9D30BE68 /* IJSVGFilterGraph.m in Sources */, + 2CACA7F70DC54122B22B01CF /* IJSVGiOSXML.m in Sources */, + 10C408B7CCEB45429F15B26B /* IJSVGPlatform.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1051,6 +1151,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.iconjar.ijsvg; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; }; name = Debug; }; @@ -1081,6 +1183,8 @@ PRODUCT_BUNDLE_IDENTIFIER = com.iconjar.ijsvg; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = macosx; + SUPPORTS_MACCATALYST = NO; }; name = Release; }; diff --git a/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.h b/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.h index 938a6197..ee4c2663 100644 --- a/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.h +++ b/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.h @@ -6,8 +6,9 @@ // Copyright © 2020 Curtis Hard. All rights reserved. // -#import -#import +#import + +@class IJSVG; IJSVG* IJSVGGetFromNSImage(NSImage* image); diff --git a/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.m b/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.m index f04a6441..cb599348 100644 --- a/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.m +++ b/Framework/IJSVG/IJSVG/Source/Additions/NSImage+IJSVGAdditions.m @@ -6,23 +6,42 @@ // Copyright © 2020 Curtis Hard. All rights reserved. // +#import +#if !TARGET_OS_IOS #import +#endif #import IJSVG* IJSVGGetFromNSImage(NSImage* image) { +#if TARGET_OS_IOS +#pragma unused(image) + return nil; +#else for (NSImageRep* rep in image.representations) { if([rep isKindOfClass:IJSVGImageRep.class]) { return ((IJSVGImageRep*)rep).SVG; } } return nil; +#endif } @implementation NSImage (IJSVGAdditions) + (NSImage*)SVGImageNamed:(NSString*)imageName { +#if TARGET_OS_IOS + IJSVG* svg = [IJSVG SVGNamed:imageName]; + if(svg == nil) { + return nil; + } + CGSize size = svg.size; + if(size.width <= 0.f || size.height <= 0.f) { + size = CGSizeMake(24.f, 24.f); + } + return [svg imageWithSize:size]; +#else // find the image NSBundle* bundle = NSBundle.mainBundle; NSString* str = nil; @@ -48,6 +67,7 @@ + (NSImage*)SVGImageNamed:(NSString*)imageName return image; } return nil; +#endif } @end diff --git a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.h b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.h index cdfa9e4e..d4ae4213 100644 --- a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.h +++ b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.h @@ -6,8 +6,9 @@ // Copyright (c) 2014 Curtis Hard. All rights reserved. // -#import -#import +#import + +NS_ASSUME_NONNULL_BEGIN typedef NS_OPTIONS(NSInteger, IJSVGColorStringOptions) { IJSVGColorStringOptionNone = 1 << 0, @@ -167,34 +168,46 @@ typedef NS_ENUM(NSInteger, IJSVGPredefinedColor) { IJSVGColorYellowgreen }; -extern NSString* const IJSVGColorCurrentColorName; +extern NSString * const IJSVGColorCurrentColorName; @interface IJSVGColor : NSObject -CGFloat* IJSVGColorCSSHSLToHSB(CGFloat hue, CGFloat saturation, CGFloat lightness); +CGFloat* _Nullable IJSVGColorCSSHSLToHSB(CGFloat hue, CGFloat saturation, CGFloat lightness); +BOOL IJSVGColorGetRGBAComponents(NSColor* _Nullable color, + CGFloat* _Nullable red, + CGFloat* _Nullable green, + CGFloat* _Nullable blue, + CGFloat* _Nullable alpha); +CGFloat IJSVGColorAlphaComponent(NSColor* _Nullable color); -+ (NSColor*)computeColorSpace:(NSColor*)color; -+ (NSColorSpace*)defaultColorSpace; -+ (BOOL)isColor:(NSString*)string; -+ (NSString*)colorStringFromColor:(NSColor*)color ++ (NSColor* _Nullable)computeColorSpace:(NSColor* _Nullable)color; +#if TARGET_OS_IOS ++ (CGColorSpaceRef _Nonnull)defaultColorSpace; +#else ++ (NSColorSpace* _Nonnull)defaultColorSpace; +#endif ++ (BOOL)isColor:(NSString* _Nullable)string; ++ (NSString*)colorStringFromColor:(NSColor* _Nullable)color options:(IJSVGColorStringOptions)options; -+ (NSString*)colorStringFromColor:(NSColor*)color; ++ (NSString*)colorStringFromColor:(NSColor* _Nullable)color; + (NSColor*)colorFromHEXInteger:(NSInteger)hex; -+ (NSColor*)computeColor:(id)colour; -+ (BOOL)isNoneOrTransparent:(NSString*)string; -+ (NSColor*)colorFromString:(NSString*)string; -+ (NSColor*)colorFromHEXString:(NSString*)string; -+ (NSColor*)colorFromHEXString:(NSString*)string - containsAlphaComponent:(BOOL*)containsAlphaComponent; ++ (NSColor* _Nullable)computeColor:(id _Nullable)colour; ++ (BOOL)isNoneOrTransparent:(NSString* _Nullable)string; ++ (NSColor* _Nullable)colorFromString:(NSString* _Nullable)string; ++ (NSColor* _Nullable)colorFromHEXString:(NSString* _Nullable)string; ++ (NSColor* _Nullable)colorFromHEXString:(NSString* _Nullable)string + containsAlphaComponent:(BOOL* _Nullable)containsAlphaComponent; + (BOOL)HEXContainsAlphaComponent:(NSUInteger)hex; + (unsigned long)lengthOfHEXInteger:(NSUInteger)hex; -+ (NSColor*)colorFromRString:(NSString*)rString - gString:(NSString*)gString - bString:(NSString*)bString - aString:(NSString*)aString; -+ (NSColor*)colorFromPredefinedColorName:(NSString*)name; -+ (NSString*)colorNameFromPredefinedColor:(IJSVGPredefinedColor)color; -+ (NSColor*)changeAlphaOnColor:(NSColor*)color ++ (NSColor*)colorFromRString:(NSString* _Nullable)rString + gString:(NSString* _Nullable)gString + bString:(NSString* _Nullable)bString + aString:(NSString* _Nullable)aString; ++ (NSColor* _Nullable)colorFromPredefinedColorName:(NSString* _Nullable)name; ++ (NSString* _Nullable)colorNameFromPredefinedColor:(IJSVGPredefinedColor)color; ++ (NSColor* _Nullable)changeAlphaOnColor:(NSColor* _Nullable)color to:(CGFloat)alphaValue; @end + +NS_ASSUME_NONNULL_END diff --git a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.m b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.m index 700c0b6c..e48e6caa 100644 --- a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.m +++ b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGColor.m @@ -18,8 +18,97 @@ @implementation IJSVGColor static NSDictionary* _colorTree = nil; +BOOL IJSVGColorGetRGBAComponents(NSColor* _Nullable color, + CGFloat* _Nullable red, + CGFloat* _Nullable green, + CGFloat* _Nullable blue, + CGFloat* _Nullable alpha) +{ + if(color == nil) { + if(red != NULL) { + *red = 0.f; + } + if(green != NULL) { + *green = 0.f; + } + if(blue != NULL) { + *blue = 0.f; + } + if(alpha != NULL) { + *alpha = 0.f; + } + return NO; + } +#if TARGET_OS_IOS + CGFloat r = 0.f; + CGFloat g = 0.f; + CGFloat b = 0.f; + CGFloat a = 0.f; + if([color getRed:&r + green:&g + blue:&b + alpha:&a] == NO) { + CGFloat w = 0.f; + if([color getWhite:&w + alpha:&a] == YES) { + r = w; + g = w; + b = w; + } else { + const CGFloat* components = CGColorGetComponents(color.CGColor); + size_t count = CGColorGetNumberOfComponents(color.CGColor); + if(components != NULL && count >= 2) { + if(count == 2) { + r = g = b = components[0]; + a = components[1]; + } else { + r = components[0]; + g = components[1]; + b = components[2]; + a = components[count - 1]; + } + } + } + } + if(red != NULL) { + *red = r; + } + if(green != NULL) { + *green = g; + } + if(blue != NULL) { + *blue = b; + } + if(alpha != NULL) { + *alpha = a; + } + return YES; +#else + if(red != NULL) { + *red = color.redComponent; + } + if(green != NULL) { + *green = color.greenComponent; + } + if(blue != NULL) { + *blue = color.blueComponent; + } + if(alpha != NULL) { + *alpha = color.alphaComponent; + } + return YES; +#endif +} + +CGFloat IJSVGColorAlphaComponent(NSColor* _Nullable color) +{ + CGFloat alpha = 0.f; + IJSVGColorGetRGBAComponents(color, NULL, NULL, NULL, &alpha); + return alpha; +} + -CGFloat* IJSVGColorCSSHSLToHSB(CGFloat hue, CGFloat saturation, CGFloat lightness) +CGFloat* _Nullable IJSVGColorCSSHSLToHSB(CGFloat hue, CGFloat saturation, CGFloat lightness) { hue *= (1.f / 360.f); hue = (hue - floorf(hue)); @@ -44,18 +133,34 @@ + (void)load [self.class _generateTree]; } +#if TARGET_OS_IOS ++ (CGColorSpaceRef)defaultColorSpace +{ + static CGColorSpaceRef colorSpace = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + }); + return colorSpace; +} +#else + (NSColorSpace*)defaultColorSpace { - return NSColorSpace.deviceRGBColorSpace; + return NSColorSpace.sRGBColorSpace; } +#endif + (NSColor*)computeColorSpace:(NSColor*)color { +#if TARGET_OS_IOS + return color; +#else NSColorSpace* space = [self defaultColorSpace]; if(color.colorSpace != space) { color = [color colorUsingColorSpace:space]; } return color; +#endif } + (void)_generateTree @@ -239,10 +344,10 @@ + (NSColor*)colorFromRUnit:(IJSVGUnitLength*)rUnit CGFloat g = gUnit.type == IJSVGUnitLengthTypePercentage ? [gUnit computeValue:255.f] : [gUnit computeValue:1.f]; CGFloat b = bUnit.type == IJSVGUnitLengthTypePercentage ? [bUnit computeValue:255.f] : [bUnit computeValue:1.f]; CGFloat a = [aUnit computeValue:100.f]; - return [self computeColorSpace:[NSColor colorWithDeviceRed:(r / 255.f) - green:(g / 255.f) - blue:(b / 255.f) - alpha:a]]; + return [self computeColorSpace:[NSColor colorWithRed:(r / 255.f) + green:(g / 255.f) + blue:(b / 255.f) + alpha:MIN(a, 1.f)]]; } + (BOOL)isNoneOrTransparent:(NSString*)string @@ -310,6 +415,13 @@ + (NSColor*)colorFromString:(NSString*)string IJSVGParsingStringMethodsRelease(methods, count); methods = NULL; + if(parts.count == 1) { + // failed parse + return [self colorFromRString:@"0" + gString:@"0" + bString:@"0" + aString:@"1"]; + } return [self colorFromRString:parts[0] gString:parts[1] bString:parts[2] @@ -328,10 +440,10 @@ + (NSColor*)colorFromString:(NSString*)string // convert HSL to HSB CGFloat* hsb = IJSVGColorCSSHSLToHSB(params[0], params[1], params[2]); - NSColor* color = [NSColor colorWithDeviceHue:hsb[0] - saturation:hsb[1] - brightness:hsb[2] - alpha:alpha]; + NSColor* color = [NSColor colorWithHue:hsb[0] + saturation:hsb[1] + brightness:hsb[2] + alpha:alpha]; color = [self computeColorSpace:color]; @@ -377,10 +489,19 @@ + (NSString*)colorStringFromColor:(NSColor*)color // convert to RGB color = [self computeColorSpace:color]; - int red = color.redComponent * 0xFF; - int green = color.greenComponent * 0xFF; - int blue = color.blueComponent * 0xFF; - int alpha = (int)(color.alphaComponent * 100); + CGFloat redComponent = 0.f; + CGFloat greenComponent = 0.f; + CGFloat blueComponent = 0.f; + CGFloat alphaComponent = 0.f; + IJSVGColorGetRGBAComponents(color, + &redComponent, + &greenComponent, + &blueComponent, + &alphaComponent); + int red = redComponent * 0xFF; + int green = greenComponent * 0xFF; + int blue = blueComponent * 0xFF; + int alpha = (int)(alphaComponent * 100); BOOL forceHex = (options & IJSVGColorStringOptionForceHEX) != 0; BOOL allowShortHand = (options & IJSVGColorStringOptionAllowShortHand) != 0; @@ -398,7 +519,7 @@ + (NSString*)colorStringFromColor:(NSColor*)color // are the same or we cant enable shorthand if(allowRRGGBBAA == YES) { NSString* alphaHexString = [NSString stringWithFormat:@"%02X", - (int)(color.alphaComponent * 0xFF)]; + (int)(alphaComponent * 0xFF)]; if([alphaHexString characterAtIndex:0] != [alphaHexString characterAtIndex:1]) { allowShortHand = NO; @@ -415,7 +536,7 @@ + (NSString*)colorStringFromColor:(NSColor*)color // allow shorthand alpha if(allowRRGGBBAA == YES && alpha != 100) { NSString* a = [NSString stringWithFormat:@"%02X", - (int)(color.alphaComponent * 0xFF)]; + (int)(alphaComponent * 0xFF)]; return [NSString stringWithFormat:@"#%c%c%c%c", [r characterAtIndex:0], [g characterAtIndex:0], [b characterAtIndex:0], [a characterAtIndex:0]]; @@ -426,7 +547,7 @@ + (NSString*)colorStringFromColor:(NSColor*)color } if(allowRRGGBBAA == YES && alpha != 100) { return [NSString stringWithFormat:@"#%02X%02X%02X%02X", red, green, - blue, (int)(color.alphaComponent * 0xFF)]; + blue, (int)(alphaComponent * 0xFF)]; } return [NSString stringWithFormat:@"#%02X%02X%02X", red, green, blue]; } @@ -741,10 +862,14 @@ + (NSColor*)changeAlphaOnColor:(NSColor*)color to:(CGFloat)alphaValue { color = [self computeColorSpace:color]; - return [self computeColorSpace:[NSColor colorWithDeviceRed:color.redComponent +#if TARGET_OS_IOS + return [color colorWithAlphaComponent:alphaValue]; +#else + return [self computeColorSpace:[NSColor colorWithSRGBRed:color.redComponent green:color.greenComponent blue:color.blueComponent alpha:alphaValue]]; +#endif } + (BOOL)isColor:(NSString*)string @@ -778,10 +903,10 @@ + (NSColor*)colorFromHEXInteger:(NSInteger)hex alpha = (hex & 0xFF) / 255.f; hex = hex >> 8; } - return [self computeColorSpace:[NSColor colorWithDeviceRed:((hex >> 16) & 0xFF) / 255.f - green:((hex >> 8) & 0xFF) / 255.f - blue:(hex & 0xFF) / 255.f - alpha:alpha]]; + return [self computeColorSpace:[NSColor colorWithRed:((hex >> 16) & 0xFF) / 255.f + green:((hex >> 8) & 0xFF) / 255.f + blue:(hex & 0xFF) / 255.f + alpha:alpha]]; } + (unsigned long)HEXFromArbitraryHexString:(NSString*)aString diff --git a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGTraitedColor.h b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGTraitedColor.h index a43ecb8c..fcb3ee0b 100644 --- a/Framework/IJSVG/IJSVG/Source/Colors/IJSVGTraitedColor.h +++ b/Framework/IJSVG/IJSVG/Source/Colors/IJSVGTraitedColor.h @@ -6,8 +6,7 @@ // Copyright © 2021 Curtis Hard. All rights reserved. // -#import -#import +#import typedef NS_OPTIONS(NSInteger, IJSVGColorUsageTraits) { IJSVGColorUsageTraitNone = 0, diff --git a/Framework/IJSVG/IJSVG/Source/Commands/IJSVGCommand.m b/Framework/IJSVG/IJSVG/Source/Commands/IJSVGCommand.m index a71e2e5f..e02fe6f4 100644 --- a/Framework/IJSVG/IJSVG/Source/Commands/IJSVGCommand.m +++ b/Framework/IJSVG/IJSVG/Source/Commands/IJSVGCommand.m @@ -95,13 +95,6 @@ + (Class)commandClassForCommandChar:(char)aChar + (CGMutablePathRef)newPathForCommandsArray:(NSArray*)commands { CGMutablePathRef path = CGPathCreateMutable(); - // If there are some commands in the array and the first one is NOT a move - // command then we need to implicitly moe the path to zero point or CG will - // throw warnings at us. - if(commands.count > 0 && ![commands.firstObject isKindOfClass:IJSVGCommandMove.class]) { - CGPathMoveToPoint(path, NULL, 0, 0); - } - IJSVGCommand* preCommand = nil; for(IJSVGCommand* command in commands) { for (IJSVGCommand* subCommand in command.subCommands) { @@ -171,14 +164,6 @@ + (CGMutablePathRef)newPathForCommandsArray:(NSArray*)commands [commands addObject:command]; } -#if DEBUG - else { - // if we get here then the command buffer is invalid, nothing we can - // do about it, just invalid data. - NSLog(@"\"%s\" is not a valid command instruction set", commandString); - } -#endif - // free the memory as at this point, we are done with it (void)free(commandString), commandString = NULL; } diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVG.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVG.h index 53abe85f..ae6d943b 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVG.h +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVG.h @@ -18,12 +18,15 @@ #import #import #import -#import +#import @class IJSVG; -@class IJSVGParser; +#if TARGET_OS_IOS +@interface IJSVG : NSObject { +#else @interface IJSVG : NSObject { +#endif @private IJSVGRootNode* _rootNode; @@ -31,7 +34,6 @@ CGRect _viewBox; CGFloat _backingScale; IJSVGUnitSize* _intrinsicSize; - IJSVGParser* _parser; } // set this to be called when the layer is about to draw, it will call this @@ -86,6 +88,7 @@ floatingPointOptions:(IJSVGFloatingPointOptions)floatingPointOptions; + (id)SVGNamed:(NSString*)string; ++ (NSString*)normalizedSVGStringForEmbedding:(NSString*)string; + (IJSVG*)SVGFromCGPathRef:(CGPathRef)path; + (IJSVG*)SVGFromCGPathRef:(CGPathRef)path diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVG.m b/Framework/IJSVG/IJSVG/Source/Core/IJSVG.m index 9fbd520a..f669fb34 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVG.m +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVG.m @@ -8,12 +8,161 @@ #import #import +#import #import #import -@interface IJSVG (private) -@property (nonatomic, strong) IJSVGParser* parser; -@end +static NSString* IJSVGNormalizedSVGStringForEmbedding(NSString* string) +{ + if(string.length == 0) { + return string; + } + + NSError* error = nil; + NSRegularExpression* entityPattern = [NSRegularExpression regularExpressionWithPattern:@"(?is)" + options:0 + error:&error]; + if(entityPattern != nil) { + NSArray* matches = [entityPattern matchesInString:string + options:0 + range:NSMakeRange(0, string.length)]; + NSMutableDictionary* entities = [[NSMutableDictionary alloc] initWithCapacity:matches.count]; + for(NSTextCheckingResult* match in matches) { + if(match.numberOfRanges < 4) { + continue; + } + NSString* name = [string substringWithRange:[match rangeAtIndex:1]]; + NSString* value = [string substringWithRange:[match rangeAtIndex:3]]; + if(name.length != 0) { + entities[name] = value ?: @""; + } + } + for(NSString* name in entities) { + NSString* entityRef = [NSString stringWithFormat:@"&%@;", name]; + string = [string stringByReplacingOccurrencesOfString:entityRef + withString:entities[name]]; + } + } + + error = nil; + NSRegularExpression* xmlDecl = [NSRegularExpression regularExpressionWithPattern:@"(?is)<\\?xml[^>]*\\?>" + options:0 + error:&error]; + if(xmlDecl != nil) { + string = [xmlDecl stringByReplacingMatchesInString:string + options:0 + range:NSMakeRange(0, string.length) + withTemplate:@""]; + } + + NSRange doctypeStart = [string rangeOfString:@" 0) { + bracketDepth -= 1; + } + continue; + } + if(character == '>' && bracketDepth == 0) { + removalEnd = index + 1; + break; + } + } + if(removalEnd != NSNotFound && removalEnd > doctypeStart.location) { + string = [string stringByReplacingCharactersInRange:NSMakeRange(doctypeStart.location, + removalEnd - doctypeStart.location) + withString:@""]; + } + } + + NSRange svgStart = [string rangeOfString:@"" + options:0 + range:NSMakeRange(svgStart.location, + string.length - svgStart.location)]; + if(tagEnd.location != NSNotFound) { + NSRange tagRange = NSMakeRange(svgStart.location, + NSMaxRange(tagEnd) - svgStart.location); + NSString* tag = [string substringWithRange:tagRange]; + NSMutableString* replacement = [tag mutableCopy]; + NSUInteger insertIndex = 4; + while(insertIndex < replacement.length) { + unichar ch = [replacement characterAtIndex:insertIndex]; + if([[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:ch] || + ch == '>' || + ch == '/') { + break; + } + insertIndex += 1; + } + if([tag rangeOfString:@"xmlns=" + options:NSCaseInsensitiveSearch].location == NSNotFound) { + [replacement insertString:@" xmlns=\"http://www.w3.org/2000/svg\"" + atIndex:insertIndex]; + insertIndex += @" xmlns=\"http://www.w3.org/2000/svg\"".length; + } + if([string rangeOfString:@"xlink:" + options:NSCaseInsensitiveSearch].location != NSNotFound && + [tag rangeOfString:@"xmlns:xlink=" + options:NSCaseInsensitiveSearch].location == NSNotFound) { + [replacement insertString:@" xmlns:xlink=\"http://www.w3.org/1999/xlink\"" + atIndex:insertIndex]; + } + string = [string stringByReplacingCharactersInRange:tagRange + withString:replacement]; + } + } + return string; +} + +static void IJSVGDetachLayerTree(CALayer* layer) +{ + if(layer == nil) { + return; + } + + NSArray* sublayers = [layer.sublayers copy]; + for(CALayer* sublayer in sublayers) { + if([sublayer conformsToProtocol:@protocol(IJSVGDrawableLayer)]) { + IJSVGDetachLayerTree((CALayer*)sublayer); + } + } + + if([layer isKindOfClass:IJSVGFilterLayer.class]) { + ((IJSVGFilterLayer*)layer).sublayer = nil; + } + layer.clipLayers = nil; + layer.maskLayer = nil; + layer.referencingLayer = nil; + layer.filter = nil; + layer.mask = nil; + layer.delegate = nil; + layer.contents = nil; + layer.sublayers = nil; +} @implementation IJSVG @@ -28,6 +177,7 @@ - (void)dealloc // to quick. IJSVGThreadManager* threadManager = IJSVGThreadManager.currentManager; BOOL flag = IJSVGBeginTransaction(); + IJSVGDetachLayerTree(_rootLayer); _layerTree = nil; _rootLayer = nil; if(flag == YES) { @@ -64,6 +214,11 @@ + (id)SVGNamed:(NSString*)string error:error]; } ++ (NSString*)normalizedSVGStringForEmbedding:(NSString*)string +{ + return IJSVGNormalizedSVGStringForEmbedding(string); +} + + (IJSVG*)SVGFromCGPathRef:(CGPathRef)path { return [self SVGFromCGPathRef:path @@ -122,8 +277,6 @@ - (id)initWithImage:(NSImage*)image size:size]; imageNode.width = size.width.copy; imageNode.height = size.height.copy; - rootNode.intrinsicSize = [IJSVGUnitSize sizeWithWidth:imageNode.width.copy - height:imageNode.height.copy]; rootNode.viewBox = viewBox; [rootNode addChild:imageNode]; return [self initWithRootNode:rootNode]; @@ -180,10 +333,10 @@ - (id)initWithFilePathURL:(NSURL*)aURL NSError* anError = nil; // create the group - IJSVGParser *parser = [IJSVGParser parserForFileURL:aURL + IJSVGParser* parser = [IJSVGParser parserForFileURL:aURL error:&anError]; - self.parser = parser; - + _rootNode = parser.rootNode; + [self _setupBasicInfoFromGroup]; [self _setupBasicsFromAnyInitializer]; @@ -205,16 +358,6 @@ - (id)initWithSVGData:(NSData*)data error:nil]; } -- (void)setParser:(IJSVGParser*)parser { - _rootNode = [parser rootNodeWithSize:CGSizeZero]; - - // if the rootNode has any form of relative units, we need to keep hold of - // the parser so when we render, we can ask for new values. - if(_rootNode.viewBoxContainsRelativeUnits) { - _parser = parser; - } -} - - (id)initWithSVGData:(NSData*)data error:(NSError**)error { @@ -240,10 +383,9 @@ - (id)initWithSVGString:(NSString*)string // setup the parser IJSVGParser* parser = [[IJSVGParser alloc] initWithSVGString:string - fileURL:nil - error:&anError]; - self.parser = parser; - + error:&anError]; + _rootNode = parser.rootNode; + [self _setupBasicInfoFromGroup]; [self _setupBasicsFromAnyInitializer]; @@ -289,9 +431,15 @@ - (void)_setupBasicsFromAnyInitializer self.renderQuality = kIJSVGRenderQualityFullResolution; self.defaultSize = CGSizeMake(200.f, 200.f); self.renderingBackingScaleHelper = ^CGFloat { +#if TARGET_OS_IOS + if(UIScreen.mainScreen != nil) { + return UIScreen.mainScreen.scale; + } +#else if(NSScreen.mainScreen != nil) { return NSScreen.mainScreen.backingScaleFactor; } +#endif return 1.f; }; @@ -350,7 +498,7 @@ - (IJSVGRootNode*)rootNode - (NSSet*)directDescendSVGs { NSMutableSet* svgs = [[NSMutableSet alloc] init]; - NSArray* nodes = [self.rootNode childrenOfType:IJSVGNodeTypeSVG]; + NSSet* nodes = [self.rootNode childrenOfType:IJSVGNodeTypeSVG]; for(IJSVGNode* node in nodes) { IJSVG* newSVG = nil; newSVG = [[self.class alloc] initWithRootNode:(IJSVGRootNode*)node]; @@ -441,21 +589,10 @@ - (CGImageRef)newCGImageRefWithSize:(CGSize)size CGFloat scale = [self backingScaleFactor]; // create the context and colorspace - int width = (int)size.width * scale; - int height = (int)size.height * scale; - - // use correct memory alignment for performance. - size_t bytesPerRow = ((width * 4) + 15) & ~15; - size_t bufferSize = height * bytesPerRow; - - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - void* data = valloc(bufferSize); - memset(data, 0, bufferSize); - - CGBitmapInfo info = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host; - CGContextRef ref = CGBitmapContextCreate(data, width, height, 8, - bytesPerRow, colorSpace, - info); + CGColorSpaceRef colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef ref = CGBitmapContextCreate(NULL, (int)size.width * scale, + (int)size.height * scale, 8, 0, colorSpace, + kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little); // scale the context CGContextScaleCTM(ref, scale, scale); @@ -476,7 +613,6 @@ - (CGImageRef)newCGImageRefWithSize:(CGSize)size // release all things! CGColorSpaceRelease(colorSpace); CGContextRelease(ref); - free(data); return imageRef; } @@ -488,8 +624,14 @@ - (NSImage*)imageWithSize:(CGSize)aSize flipped:flipped error:error]; +#if TARGET_OS_IOS + NSImage* image = [NSImage imageWithCGImage:ref + scale:1.f + orientation:UIImageOrientationUp]; +#else NSImage* image = [[NSImage alloc] initWithCGImage:ref size:aSize]; +#endif CGImageRelease(ref); return image; } @@ -517,10 +659,6 @@ - (NSImage*)imageByMaintainingAspectRatioWithSize:(CGSize)aSize error:(NSError**)error { CGSize ogSize = [self sizeByMaintainingAspectRatioWithSize:aSize]; - // Make suer we actually have a size, if not, just return nil. - if(isnan(ogSize.width) || isnan(ogSize.height)) { - return nil; - } return [self imageWithSize:ogSize flipped:flipped error:error]; @@ -592,7 +730,15 @@ - (void)prepForDrawingInView:(NSView*)view // set the scale __weak NSView* weakView = view; self.renderingBackingScaleHelper = ^CGFloat { +#if TARGET_OS_IOS + CGFloat scale = weakView.window.screen.scale; + if(scale == 0.f) { + scale = UIScreen.mainScreen.scale; + } + return scale; +#else return weakView.window.screen.backingScaleFactor; +#endif }; } @@ -622,7 +768,7 @@ - (BOOL)drawInRect:(CGRect)rect error:(NSError**)error { CGContextRef currentCGContext; - currentCGContext = NSGraphicsContext.currentContext.CGContext; + currentCGContext = NSGraphicsGetCurrentContext(); return [self _drawInRect:rect context:currentCGContext error:error]; @@ -658,7 +804,7 @@ - (BOOL)_drawInRect:(CGRect)rect } } CGContextSetInterpolationQuality(ctx, quality); - IJSVGRootLayer* rootLayer = [self rootLayerWithRect:rect]; + IJSVGRootLayer* rootLayer = self.rootLayer; [rootLayer renderInContext:ctx viewPort:rect backingScale:backingScale @@ -680,35 +826,16 @@ - (IJSVGLayerTree*)layerTree return _layerTree; } -- (IJSVGRootLayer*)rootLayerWithRect:(CGRect)rect { - // no parser, which means there is no need to recompute the value - if(_parser == nil || !_rootNode.viewBoxContainsRelativeUnits || - CGSizeEqualToSize(_rootNode.clientSize, rect.size)) { - return self.rootLayer; - } - - // if we do have a parser, that means the node has relative values, lets recompute - __weak IJSVG* weakSelf = self; - [self performBlock:^{ - IJSVG* strongSelf = weakSelf; - strongSelf->_rootNode = [strongSelf->_parser rootNodeWithSize:rect.size]; - strongSelf->_rootLayer = [strongSelf.layerTree rootLayerForRootNode:strongSelf->_rootNode]; - }]; - return _rootLayer; -} - - (IJSVGRootLayer*)rootLayer { - if(_rootLayer != nil) { + if(_rootLayer == nil) { + __weak IJSVG* weakSelf = self; + [self performBlock:^{ + IJSVG* strongSelf = weakSelf; + strongSelf->_rootLayer = [strongSelf.layerTree rootLayerForRootNode:strongSelf->_rootNode]; + }]; + } return _rootLayer; - } - - __weak IJSVG* weakSelf = self; - [self performBlock:^{ - IJSVG* strongSelf = weakSelf; - strongSelf->_rootLayer = [strongSelf.layerTree rootLayerForRootNode:strongSelf->_rootNode]; - }]; - return _rootLayer; } - (CGFloat)backingScaleFactor @@ -731,6 +858,7 @@ - (void)invalidateLayerTree __weak IJSVG* weakSelf = self; [self performBlock:^{ IJSVG* strongSelf = weakSelf; + IJSVGDetachLayerTree(strongSelf->_rootLayer); strongSelf->_rootLayer = nil; strongSelf->_layerTree = nil; }]; @@ -741,6 +869,7 @@ - (IJSVGTraitedColorStorage*)colors return self.rootLayer.colors; } +#if !TARGET_OS_IOS #pragma mark NSPasteboard - (NSArray*)writableTypesForPasteboard:(NSPasteboard*)pasteboard @@ -755,6 +884,7 @@ - (id)pasteboardPropertyListForType:(NSString*)type } return nil; } +#endif #pragma mark matching diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.h index dd729296..85c0e854 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.h +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.h @@ -7,11 +7,15 @@ // #import -#import +#import @class IJSVG; +#if TARGET_OS_IOS +@interface IJSVGImageRep : NSObject { +#else @interface IJSVGImageRep : NSImageRep { +#endif @private IJSVG* _svg; diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.m b/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.m index 9f2bfd3c..26e383aa 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.m +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGImageRep.m @@ -11,6 +11,77 @@ @implementation IJSVGImageRep +#if TARGET_OS_IOS + ++ (NSArray*)imageTypes +{ + return @[ @"public.svg-image", @"svg" ]; +} + ++ (NSArray*)imageUnfilteredTypes +{ + return @[ @"public.svg-image", @"svg" ]; +} + ++ (NSArray*)imageRepsWithData:(NSData*)data +{ + IJSVGImageRep* instance = [self imageRepWithData:data]; + if(instance == nil) { + return @[]; + } + return @[ instance ]; +} + ++ (instancetype)imageRepWithData:(NSData*)data +{ + return [[self alloc] initWithData:data]; +} + +- (instancetype)initWithData:(NSData*)data +{ + if((self = [super init]) != nil) { + NSString* string = [[NSString alloc] initWithData:data + encoding:NSUTF8StringEncoding]; + _svg = [[IJSVG alloc] initWithSVGString:string]; + if(_svg == nil) { + return nil; + } + } + return self; +} + +- (BOOL)draw +{ + [_svg drawInRect:self.viewBox]; + return YES; +} + +- (BOOL)drawAtPoint:(NSPoint)point +{ + [_svg drawAtPoint:point + size:_svg.viewBox.size]; + return YES; +} + +- (BOOL)drawInRect:(NSRect)rect +{ + [_svg drawInRect:rect]; + return YES; +} + +- (CGRect)viewBox +{ + return _svg.viewBox; +} + +- (IJSVG*)SVG +{ + return _svg; +} + +#else + +#if 0 + (void)load { [NSBitmapImageRep registerImageRepClass:self]; @@ -18,8 +89,9 @@ + (void)load + (BOOL)canInitWithData:(NSData*)data { - return [IJSVGParser isDataSVG:data]; + return data.length ? [IJSVGParser isDataSVG:data] : NO; } +#endif + (NSArray*)imageTypes { @@ -105,4 +177,6 @@ - (IJSVG*)SVG return _svg; } +#endif + @end diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.h new file mode 100644 index 00000000..5fa8771c --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.h @@ -0,0 +1,70 @@ +// +// IJSVGPlatform.h +// IJSVG +// +// Platform abstraction for shared macOS / iOS code. +// + +#import +#import + +#if TARGET_OS_OSX + +#import + +static inline CGContextRef NSGraphicsGetCurrentContext(void) { + return NSGraphicsContext.currentContext.CGContext; +} + +#elif TARGET_OS_IOS + +#import + +#define NSColor UIColor +#define NSImage UIImage +#define NSBezierPath UIBezierPath +#define NSEdgeInsets UIEdgeInsets +#define NSEdgeInsetsZero UIEdgeInsetsZero +#define NSFont UIFont +#define NSView UIView +#define NSGraphicsGetCurrentContext() UIGraphicsGetCurrentContext() + +typedef CGPoint NSPoint; +typedef CGSize NSSize; +typedef CGRect NSRect; + +#define NSZeroPoint CGPointZero +#define NSZeroSize CGSizeZero +#define NSZeroRect CGRectZero +#define NSMakePoint(x, y) CGPointMake((x), (y)) +#define NSMakeSize(w, h) CGSizeMake((w), (h)) +#define NSMakeRect(x, y, w, h) CGRectMake((x), (y), (w), (h)) + +// On macOS NSRect is typedef'd to CGRect, so the conversion functions are +// identity. On iOS NSRect doesn't exist natively — we typedef it to CGRect +// above, so the same identity rule applies. Same for Point/Size. +#define NSRectFromCGRect(r) (r) +#define NSRectToCGRect(r) (r) +#define NSPointFromCGPoint(p) (p) +#define NSPointToCGPoint(p) (p) +#define NSSizeFromCGSize(s) (s) +#define NSSizeToCGSize(s) (s) + +// AppKit's -[NSView setNeedsDisplay:] takes a BOOL; UIKit's takes none. +// Provide a BOOL-taking variant so shared call sites compile unchanged. +@interface UIView (IJSVGNeedsDisplay) +- (void)setNeedsDisplay:(BOOL)flag; +@end + +// Foundation on iOS only ships the CG-named NSValue geometry methods; AppKit +// uses the NS-named ones. Bridge them so cross-platform call sites work. +@interface NSValue (IJSVGGeometry) ++ (NSValue*)valueWithPoint:(CGPoint)point; ++ (NSValue*)valueWithSize:(CGSize)size; ++ (NSValue*)valueWithRect:(CGRect)rect; +@property (readonly) CGPoint pointValue; +@property (readonly) CGSize sizeValue; +@property (readonly) CGRect rectValue; +@end + +#endif diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.m b/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.m new file mode 100644 index 00000000..5caa6a25 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGPlatform.m @@ -0,0 +1,32 @@ +// +// IJSVGPlatform.m +// IJSVG +// + +#import "IJSVGPlatform.h" + +#if TARGET_OS_IOS + +@implementation UIView (IJSVGNeedsDisplay) + +- (void)setNeedsDisplay:(BOOL)flag +{ + if(flag) { + [self setNeedsDisplay]; + } +} + +@end + +@implementation NSValue (IJSVGGeometry) + ++ (NSValue*)valueWithPoint:(CGPoint)point { return [NSValue valueWithCGPoint:point]; } ++ (NSValue*)valueWithSize:(CGSize)size { return [NSValue valueWithCGSize:size]; } ++ (NSValue*)valueWithRect:(CGRect)rect { return [NSValue valueWithCGRect:rect]; } +- (CGPoint)pointValue { return [self CGPointValue]; } +- (CGSize)sizeValue { return [self CGSizeValue]; } +- (CGRect)rectValue { return [self CGRectValue]; } + +@end + +#endif diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGUmbrella.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGUmbrella.h index 1eef4256..91c9ad0f 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVGUmbrella.h +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGUmbrella.h @@ -23,15 +23,28 @@ #import #import #import +#import +#import +#import +#import +#import #import +#import +#import +#import +#import +#import #import #import #import #import #import +#import #import #import #import +#import #import +#import #endif /* IJSVGUmbrella_h */ diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.h index 34c4bd46..5bcd2505 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.h +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.h @@ -7,7 +7,7 @@ // #import -#import +#import IB_DESIGNABLE @interface IJSVGView : NSView { diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.m b/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.m index e873e087..61d6f29e 100644 --- a/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.m +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGView.m @@ -52,7 +52,11 @@ - (void)setSVG:(IJSVG*)anSVG // redisplay ourself! [SVG prepForDrawingInView:self]; +#if TARGET_OS_OSX [self setNeedsDisplay:YES]; +#else + [self setNeedsDisplay]; +#endif } - (BOOL)isFlipped diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGXML.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGXML.h new file mode 100644 index 00000000..e5383015 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGXML.h @@ -0,0 +1,16 @@ +// +// IJSVGXML.h +// IJSVG +// + +#import + +#if TARGET_OS_IOS + +#import + +typedef IJSVGiOSXMLNode IJSVGXMLNode; +typedef IJSVGiOSXMLElement IJSVGXMLElement; +typedef IJSVGiOSXMLDocument IJSVGXMLDocument; + +#endif diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.h b/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.h new file mode 100644 index 00000000..f8817682 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.h @@ -0,0 +1,106 @@ +// +// IJSVGiOSXML.h +// IJSVG +// + +#import +#import + +#if TARGET_OS_IOS + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_OPTIONS(NSUInteger, NSXMLNodeOptions) { + NSXMLNodeOptionsNone = 0, + NSXMLNodePrettyPrint = 1UL << 0, + NSXMLNodeCompactEmptyElement = 1UL << 1 +}; + +typedef NS_ENUM(NSUInteger, NSXMLNodeKind) { + NSXMLInvalidKind = 0, + NSXMLDocumentKind = 1, + NSXMLElementKind = 2, + NSXMLAttributeKind = 3, + NSXMLTextKind = 4, + NSXMLCommentKind = 5 +}; + +@class IJSVGiOSXMLDocument; +@class IJSVGiOSXMLElement; + +@interface IJSVGiOSXMLNode : NSObject + +@property (nonatomic, assign) NSXMLNodeKind kind; +@property (nonatomic, copy, nullable) NSString* name; +@property (nonatomic, copy, nullable) NSString* localName; +@property (nonatomic, copy, nullable) NSString* URI; +@property (nonatomic, copy, nullable) NSString* stringValue; +@property (nonatomic, weak, nullable) id parent; +@property (nonatomic, weak, nullable) IJSVGiOSXMLDocument* document; +@property (nonatomic, readonly, nullable) NSArray* attributes; +@property (nonatomic, readonly, nullable) NSArray* children; +@property (nonatomic, readonly) NSUInteger childCount; +@property (nonatomic, readonly) NSUInteger index; +@property (nonatomic, readonly, nullable) IJSVGiOSXMLNode* nextSibling; + +- (instancetype)initWithKind:(NSXMLNodeKind)kind; +- (nullable IJSVGiOSXMLNode*)attributeForName:(NSString*)name; +- (nullable IJSVGiOSXMLNode*)attributeForLocalName:(NSString*)localName + URI:(nullable NSString*)URI; +- (void)detach; + +@end + +@interface IJSVGiOSXMLElement : IJSVGiOSXMLNode + +@property (nonatomic, readonly) NSArray* attributes; +@property (nonatomic, readonly) NSArray* children; +@property (nonatomic, readonly) NSUInteger childCount; + +- (instancetype)initWithName:(nullable NSString*)name; +- (nullable IJSVGiOSXMLNode*)attributeForName:(NSString*)name; +- (nullable IJSVGiOSXMLNode*)attributeForLocalName:(NSString*)localName + URI:(nullable NSString*)URI; +- (void)setAttributesAsDictionary:(NSDictionary*)attributes; +- (void)addAttribute:(IJSVGiOSXMLNode*)attribute; +- (void)removeAttributeForName:(NSString*)name; +- (void)addChild:(IJSVGiOSXMLNode*)child; +- (void)setChildren:(nullable NSArray*)children; +- (void)insertChild:(IJSVGiOSXMLNode*)child + atIndex:(NSUInteger)index; +- (void)removeChildAtIndex:(NSUInteger)index; +- (void)replaceChildAtIndex:(NSUInteger)index + withNode:(IJSVGiOSXMLNode*)node; + +@end + +@interface IJSVGiOSXMLDocument : NSObject + +@property (nonatomic, copy, nullable) NSString* version; +@property (nonatomic, copy, nullable) NSString* characterEncoding; +@property (nonatomic, strong, nullable) IJSVGiOSXMLElement* rootElement; +@property (nonatomic, readonly) id rootDocument; + +- (instancetype)initWithRootElement:(IJSVGiOSXMLElement*)rootElement; +- (instancetype)initWithXMLString:(NSString*)string + options:(NSXMLNodeOptions)options + error:(NSError**)error; +- (instancetype)initWithData:(NSData*)data + options:(NSXMLNodeOptions)options + error:(NSError**)error; +- (instancetype)initWithContentsOfURL:(NSURL*)URL + options:(NSXMLNodeOptions)options + error:(NSError**)error; +- (NSArray*)nodesForXPath:(NSString*)xPath + error:(NSError**)error; +- (NSString*)XMLStringWithOptions:(NSXMLNodeOptions)options; + +@end + +typedef IJSVGiOSXMLNode NSXMLNode; +typedef IJSVGiOSXMLElement NSXMLElement; +typedef IJSVGiOSXMLDocument NSXMLDocument; + +NS_ASSUME_NONNULL_END + +#endif diff --git a/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.m b/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.m new file mode 100644 index 00000000..752c5b05 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Core/IJSVGiOSXML.m @@ -0,0 +1,759 @@ +// +// IJSVGiOSXML.m +// IconJar +// +// Created by Curtis Hard on 30/08/2014. +// Copyright (c) 2014 Curtis Hard. All rights reserved. +// + +#import "IJSVGiOSXML.h" + +#if TARGET_OS_IOS +#import +#import + +static NSString* IJSVGiOSXMLStringFromXMLChar(const xmlChar* chars) +{ + if(chars == NULL) { + return nil; + } + return [NSString stringWithUTF8String:(const char*)chars]; +} + +static NSString* IJSVGiOSXMLQualifiedName(NSString* localName, NSString* prefix) +{ + if(prefix.length == 0 || localName.length == 0) { + return localName; + } + return [NSString stringWithFormat:@"%@:%@", prefix, localName]; +} + +static NSString* IJSVGiOSXMLEscapeString(NSString* string, BOOL isAttribute) +{ + if(string.length == 0) { + return @""; + } + NSMutableString* output = [string mutableCopy]; + [output replaceOccurrencesOfString:@"&" + withString:@"&" + options:0 + range:NSMakeRange(0, output.length)]; + [output replaceOccurrencesOfString:@"<" + withString:@"<" + options:0 + range:NSMakeRange(0, output.length)]; + [output replaceOccurrencesOfString:@">" + withString:@">" + options:0 + range:NSMakeRange(0, output.length)]; + if(isAttribute == YES) { + [output replaceOccurrencesOfString:@"\"" + withString:@""" + options:0 + range:NSMakeRange(0, output.length)]; + } + return output; +} + +@interface IJSVGiOSXMLElement () + +@property (nonatomic, strong) NSMutableArray* mutableAttributes; +@property (nonatomic, strong) NSMutableArray* mutableChildren; + +@end + +@implementation IJSVGiOSXMLNode + +- (instancetype)init +{ + return [self initWithKind:NSXMLInvalidKind]; +} + +- (instancetype)initWithKind:(NSXMLNodeKind)kind +{ + if((self = [super init]) != nil) { + _kind = kind; + } + return self; +} + +- (void)setName:(NSString*)name +{ + _name = [name copy]; + if(_name == nil) { + _localName = nil; + return; + } + NSRange range = [_name rangeOfString:@":" options:NSBackwardsSearch]; + _localName = (range.location == NSNotFound) ? _name : [_name substringFromIndex:(range.location + 1)]; +} + +- (NSUInteger)index +{ + if([_parent isKindOfClass:[NSXMLElement class]] == YES) { + NSXMLElement* element = (NSXMLElement*)_parent; + NSArray* nodes = (_kind == NSXMLAttributeKind) ? element.attributes : element.children; + NSUInteger idx = [nodes indexOfObjectIdenticalTo:self]; + return idx == NSNotFound ? NSNotFound : idx; + } + if([_parent isKindOfClass:[NSXMLDocument class]] == YES) { + NSXMLDocument* doc = (NSXMLDocument*)_parent; + if(doc.rootElement == (NSXMLElement*)self) { + return 0; + } + } + return NSNotFound; +} + +- (NSArray*)attributes +{ + return @[]; +} + +- (NSArray*)children +{ + return @[]; +} + +- (NSUInteger)childCount +{ + return 0; +} + +- (NSXMLNode*)attributeForName:(NSString*)name +{ + return nil; +} + +- (NSXMLNode*)attributeForLocalName:(NSString*)localName + URI:(NSString* _Nullable)URI +{ + return nil; +} + +- (NSXMLNode*)nextSibling +{ + if([_parent isKindOfClass:[NSXMLElement class]] == NO) { + return nil; + } + NSXMLElement* element = (NSXMLElement*)_parent; + NSUInteger idx = self.index; + if(idx == NSNotFound || idx + 1 >= element.children.count) { + return nil; + } + return element.children[idx + 1]; +} + +- (void)detach +{ + if([_parent isKindOfClass:[NSXMLElement class]] == YES) { + NSXMLElement* parent = (NSXMLElement*)_parent; + if(_kind == NSXMLAttributeKind) { + [parent removeAttributeForName:self.name]; + } else { + NSUInteger idx = self.index; + if(idx != NSNotFound) { + [parent removeChildAtIndex:idx]; + } + } + return; + } + if([_parent isKindOfClass:[NSXMLDocument class]] == YES) { + NSXMLDocument* doc = (NSXMLDocument*)_parent; + if(doc.rootElement == (NSXMLElement*)self) { + doc.rootElement = nil; + } + } +} + +- (id)copyWithZone:(NSZone*)zone +{ + IJSVGiOSXMLNode* copy = [[self.class allocWithZone:zone] initWithKind:_kind]; + copy.name = _name; + copy.localName = _localName; + copy.URI = _URI; + copy.stringValue = _stringValue; + return copy; +} + +@end + +@implementation IJSVGiOSXMLElement + +- (instancetype)init +{ + return [self initWithName:nil]; +} + +- (instancetype)initWithName:(NSString*)name +{ + if((self = [super initWithKind:NSXMLElementKind]) != nil) { + _mutableAttributes = [[NSMutableArray alloc] init]; + _mutableChildren = [[NSMutableArray alloc] init]; + self.name = name; + } + return self; +} + +- (void)setDocument:(NSXMLDocument*)document +{ + [super setDocument:document]; + for(NSXMLNode* attribute in _mutableAttributes) { + attribute.document = document; + } + for(NSXMLNode* child in _mutableChildren) { + child.document = document; + } +} + +- (NSArray*)attributes +{ + return [_mutableAttributes copy]; +} + +- (NSArray*)children +{ + return [_mutableChildren copy]; +} + +- (NSUInteger)childCount +{ + return _mutableChildren.count; +} + +- (NSXMLNode*)attributeForName:(NSString*)name +{ + if(name.length == 0) { + return nil; + } + for(NSXMLNode* node in _mutableAttributes) { + if([node.name isEqualToString:name] == YES) { + return node; + } + } + return nil; +} + +- (NSXMLNode*)attributeForLocalName:(NSString*)localName + URI:(NSString* _Nullable)URI +{ + if(localName.length == 0) { + return nil; + } + for(NSXMLNode* node in _mutableAttributes) { + BOOL localMatch = [node.localName isEqualToString:localName]; + BOOL uriMatch = (URI == nil && node.URI == nil) || [node.URI isEqualToString:URI]; + if(localMatch == YES && uriMatch == YES) { + return node; + } + } + return nil; +} + +- (void)setAttributesAsDictionary:(NSDictionary*)attributes +{ + if(attributes.count == 0) { + return; + } + NSArray* keys = [attributes.allKeys sortedArrayUsingSelector:@selector(compare:)]; + for(NSString* key in keys) { + NSString* value = attributes[key]; + if(value == nil) { + continue; + } + NSXMLNode* node = [[NSXMLNode alloc] initWithKind:NSXMLAttributeKind]; + node.name = key; + node.stringValue = value; + [self addAttribute:node]; + } +} + +- (void)addAttribute:(NSXMLNode*)attribute +{ + if(attribute == nil) { + return; + } + attribute.kind = NSXMLAttributeKind; + if(attribute.name.length != 0) { + [self removeAttributeForName:attribute.name]; + } + attribute.parent = self; + attribute.document = self.document; + [_mutableAttributes addObject:attribute]; +} + +- (void)removeAttributeForName:(NSString*)name +{ + if(name.length == 0) { + return; + } + NSIndexSet* indexes = [_mutableAttributes indexesOfObjectsPassingTest:^BOOL(NSXMLNode* _Nonnull obj, NSUInteger idx, BOOL* _Nonnull stop) { + return [obj.name isEqualToString:name]; + }]; + if(indexes.count == 0) { + return; + } + NSArray* removing = [_mutableAttributes objectsAtIndexes:indexes]; + [_mutableAttributes removeObjectsAtIndexes:indexes]; + for(NSXMLNode* node in removing) { + node.parent = nil; + node.document = nil; + } +} + +- (void)addChild:(NSXMLNode*)child +{ + [self insertChild:child + atIndex:_mutableChildren.count]; +} + +- (void)setChildren:(NSArray*)children +{ + for(NSXMLNode* child in _mutableChildren) { + child.parent = nil; + child.document = nil; + } + [_mutableChildren removeAllObjects]; + for(NSXMLNode* child in children) { + [self addChild:child]; + } +} + +- (void)insertChild:(NSXMLNode*)child + atIndex:(NSUInteger)index +{ + if(child == nil) { + return; + } + if(child.kind == NSXMLAttributeKind) { + [self addAttribute:child]; + return; + } + child.parent = self; + child.document = self.document; + NSUInteger insertIndex = MIN(index, _mutableChildren.count); + [_mutableChildren insertObject:child + atIndex:insertIndex]; +} + +- (void)removeChildAtIndex:(NSUInteger)index +{ + if(index >= _mutableChildren.count) { + return; + } + NSXMLNode* child = _mutableChildren[index]; + [_mutableChildren removeObjectAtIndex:index]; + child.parent = nil; + child.document = nil; +} + +- (void)replaceChildAtIndex:(NSUInteger)index + withNode:(NSXMLNode*)node +{ + if(index >= _mutableChildren.count || node == nil) { + return; + } + if(node.kind == NSXMLAttributeKind) { + return; + } + NSXMLNode* oldNode = _mutableChildren[index]; + oldNode.parent = nil; + oldNode.document = nil; + node.parent = self; + node.document = self.document; + _mutableChildren[index] = node; +} + +- (NSString*)stringValue +{ + if([super stringValue] != nil) { + return [super stringValue]; + } + NSMutableString* output = nil; + for(NSXMLNode* child in _mutableChildren) { + NSString* value = child.stringValue; + if(value.length == 0) { + continue; + } + if(output == nil) { + output = [[NSMutableString alloc] init]; + } + [output appendString:value]; + } + return output; +} + +- (id)copyWithZone:(NSZone*)zone +{ + NSXMLElement* copy = [[self.class allocWithZone:zone] initWithName:self.name]; + copy.localName = self.localName; + copy.URI = self.URI; + copy.stringValue = [super stringValue]; + for(NSXMLNode* attribute in _mutableAttributes) { + [copy addAttribute:attribute.copy]; + } + for(NSXMLNode* child in _mutableChildren) { + [copy addChild:child.copy]; + } + return copy; +} + +@end + +static BOOL IJSVGiOSXMLNodeIsElement(NSXMLNode* node) +{ + return [node isKindOfClass:[NSXMLElement class]] == YES && node.kind == NSXMLElementKind; +} + +static void IJSVGiOSXMLVisitElements(NSXMLElement* element, void (^visitor)(NSXMLElement* _Nonnull)) +{ + visitor(element); + for(NSXMLNode* child in element.children) { + if(IJSVGiOSXMLNodeIsElement(child) == NO) { + continue; + } + IJSVGiOSXMLVisitElements((NSXMLElement*)child, visitor); + } +} + +static NSXMLNode* IJSVGiOSXMLNodeFromLibXMLNode(xmlNodePtr node, xmlDocPtr doc) +{ + if(node == NULL) { + return nil; + } + switch(node->type) { + case XML_ELEMENT_NODE: { + NSString* localName = IJSVGiOSXMLStringFromXMLChar(node->name); + NSString* prefix = IJSVGiOSXMLStringFromXMLChar(node->ns == NULL ? NULL : node->ns->prefix); + NSString* qualifiedName = IJSVGiOSXMLQualifiedName(localName, prefix); + NSXMLElement* element = [[NSXMLElement alloc] initWithName:qualifiedName]; + element.localName = localName; + element.URI = IJSVGiOSXMLStringFromXMLChar(node->ns == NULL ? NULL : node->ns->href); + + for(xmlAttrPtr attr = node->properties; attr != NULL; attr = attr->next) { + NSXMLNode* attribute = [[NSXMLNode alloc] initWithKind:NSXMLAttributeKind]; + NSString* attrLocalName = IJSVGiOSXMLStringFromXMLChar(attr->name); + NSString* attrPrefix = IJSVGiOSXMLStringFromXMLChar(attr->ns == NULL ? NULL : attr->ns->prefix); + attribute.name = IJSVGiOSXMLQualifiedName(attrLocalName, attrPrefix); + attribute.localName = attrLocalName; + attribute.URI = IJSVGiOSXMLStringFromXMLChar(attr->ns == NULL ? NULL : attr->ns->href); + xmlChar* value = xmlNodeListGetString(doc, attr->children, 1); + attribute.stringValue = IJSVGiOSXMLStringFromXMLChar(value); + if(value != NULL) { + xmlFree(value); + } + [element addAttribute:attribute]; + } + + for(xmlNodePtr child = node->children; child != NULL; child = child->next) { + NSXMLNode* childNode = IJSVGiOSXMLNodeFromLibXMLNode(child, doc); + if(childNode != nil) { + [element addChild:childNode]; + } + } + return element; + } + case XML_TEXT_NODE: + case XML_CDATA_SECTION_NODE: { + NSXMLNode* textNode = [[NSXMLNode alloc] initWithKind:NSXMLTextKind]; + textNode.stringValue = IJSVGiOSXMLStringFromXMLChar(node->content); + return textNode; + } + case XML_COMMENT_NODE: { + NSXMLNode* commentNode = [[NSXMLNode alloc] initWithKind:NSXMLCommentKind]; + commentNode.stringValue = IJSVGiOSXMLStringFromXMLChar(node->content); + return commentNode; + } + default: + break; + } + return nil; +} + +static BOOL IJSVGiOSXMLChildrenContainTextNodes(NSXMLElement* element) +{ + for(NSXMLNode* child in element.children) { + if(child.kind == NSXMLTextKind) { + return YES; + } + } + return NO; +} + +static void IJSVGiOSXMLAppendNodeString(NSXMLNode* node, + NSMutableString* output, + NSXMLNodeOptions options, + NSUInteger depth) +{ + if(node == nil) { + return; + } + if(node.kind == NSXMLTextKind) { + [output appendString:IJSVGiOSXMLEscapeString(node.stringValue ?: @"", NO)]; + return; + } + if(node.kind == NSXMLCommentKind) { + [output appendFormat:@"", node.stringValue ?: @""]; + return; + } + if(IJSVGiOSXMLNodeIsElement(node) == NO) { + return; + } + + NSXMLElement* element = (NSXMLElement*)node; + NSString* name = element.name ?: element.localName ?: @"node"; + [output appendFormat:@"<%@", name]; + for(NSXMLNode* attribute in element.attributes) { + NSString* attributeName = attribute.name ?: attribute.localName ?: @""; + NSString* attributeValue = IJSVGiOSXMLEscapeString(attribute.stringValue ?: @"", YES); + [output appendFormat:@" %@=\"%@\"", attributeName, attributeValue]; + } + + BOOL hasChildren = element.childCount != 0; + if(hasChildren == NO) { + if((options & NSXMLNodeCompactEmptyElement) != 0) { + [output appendString:@"/>"]; + } else { + [output appendFormat:@">", name]; + } + return; + } + + BOOL prettyPrint = (options & NSXMLNodePrettyPrint) != 0; + BOOL containsTextChildren = IJSVGiOSXMLChildrenContainTextNodes(element); + + [output appendString:@">"]; + if(prettyPrint == YES && containsTextChildren == NO) { + [output appendString:@"\n"]; + } + for(NSXMLNode* child in element.children) { + if(prettyPrint == YES && containsTextChildren == NO) { + for(NSUInteger i = 0; i < depth + 1; i++) { + [output appendString:@" "]; + } + } + IJSVGiOSXMLAppendNodeString(child, output, options, depth + 1); + if(prettyPrint == YES && containsTextChildren == NO) { + [output appendString:@"\n"]; + } + } + if(prettyPrint == YES && containsTextChildren == NO) { + for(NSUInteger i = 0; i < depth; i++) { + [output appendString:@" "]; + } + } + [output appendFormat:@"", name]; +} + +@implementation IJSVGiOSXMLDocument + +- (instancetype)init +{ + if((self = [super init]) != nil) { + _version = @"1.0"; + _characterEncoding = @"UTF-8"; + } + return self; +} + +- (instancetype)initWithRootElement:(NSXMLElement*)rootElement +{ + if((self = [self init]) != nil) { + self.rootElement = rootElement; + } + return self; +} + +- (instancetype)initWithXMLString:(NSString*)string + options:(NSXMLNodeOptions)options + error:(NSError**)error +{ + NSData* data = [string dataUsingEncoding:NSUTF8StringEncoding]; + return [self initWithData:data + options:options + error:error]; +} + +- (instancetype)initWithData:(NSData*)data + options:(NSXMLNodeOptions)options + error:(NSError**)error +{ + if((self = [self init]) != nil) { + if(data.length == 0) { + if(error != nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1001 + userInfo:nil]; + } + return nil; + } + + xmlDocPtr doc = xmlReadMemory(data.bytes, + (int)data.length, + NULL, + NULL, + XML_PARSE_NONET | XML_PARSE_RECOVER | XML_PARSE_NOERROR | XML_PARSE_NOWARNING); + if(doc == NULL) { + if(error != nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1002 + userInfo:nil]; + } + return nil; + } + + xmlNodePtr root = xmlDocGetRootElement(doc); + NSXMLNode* rootNode = IJSVGiOSXMLNodeFromLibXMLNode(root, doc); + if(IJSVGiOSXMLNodeIsElement(rootNode) == NO) { + xmlFreeDoc(doc); + if(error != nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1003 + userInfo:nil]; + } + return nil; + } + + self.rootElement = (NSXMLElement*)rootNode; + xmlFreeDoc(doc); + } + return self; +} + +- (instancetype)initWithContentsOfURL:(NSURL*)URL + options:(NSXMLNodeOptions)options + error:(NSError**)error +{ + if(URL == nil) { + if(error != nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1005 + userInfo:nil]; + } + return nil; + } + + NSData* data = [NSData dataWithContentsOfURL:URL + options:0 + error:error]; + if(data == nil) { + if(error != nil && *error == nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1005 + userInfo:nil]; + } + return nil; + } + return [self initWithData:data + options:options + error:error]; +} + +- (void)setRootElement:(NSXMLElement*)rootElement +{ + if(_rootElement == rootElement) { + return; + } + _rootElement.parent = nil; + _rootElement.document = nil; + _rootElement = rootElement; + _rootElement.parent = self; + _rootElement.document = self; +} + +- (id)rootDocument +{ + return self; +} + +- (NSArray*)nodesForXPath:(NSString*)xPath + error:(NSError**)error +{ + if(_rootElement == nil || xPath.length == 0) { + return @[]; + } + + NSMutableArray* output = [[NSMutableArray alloc] init]; + if([xPath isEqualToString:@"//use"] == YES) { + IJSVGiOSXMLVisitElements(_rootElement, ^(NSXMLElement* element) { + if([element.localName.lowercaseString isEqualToString:@"use"] == YES) { + [output addObject:element]; + } + }); + return output; + } + + if([xPath isEqualToString:@"//*[@display='none']"] == YES) { + IJSVGiOSXMLVisitElements(_rootElement, ^(NSXMLElement* element) { + NSString* value = [element attributeForName:@"display"].stringValue; + if([value.lowercaseString isEqualToString:@"none"] == YES) { + [output addObject:element]; + } + }); + return output; + } + + if([xPath isEqualToString:@"//defs/*[self::linearGradient or self::radialGradient]"] == YES) { + IJSVGiOSXMLVisitElements(_rootElement, ^(NSXMLElement* element) { + if([element.localName.lowercaseString isEqualToString:@"defs"] == NO) { + return; + } + for(NSXMLNode* child in element.children) { + if(IJSVGiOSXMLNodeIsElement(child) == NO) { + continue; + } + NSString* localName = ((NSXMLElement*)child).localName.lowercaseString; + if([localName isEqualToString:@"lineargradient"] == YES || + [localName isEqualToString:@"radialgradient"] == YES) { + [output addObject:(NSXMLElement*)child]; + } + } + }); + return output; + } + + if([xPath isEqualToString:@"//g"] == YES) { + IJSVGiOSXMLVisitElements(_rootElement, ^(NSXMLElement* element) { + if([element.localName.lowercaseString isEqualToString:@"g"] == YES) { + [output addObject:element]; + } + }); + return output; + } + + if([xPath isEqualToString:@"//path"] == YES) { + IJSVGiOSXMLVisitElements(_rootElement, ^(NSXMLElement* element) { + if([element.localName.lowercaseString isEqualToString:@"path"] == YES) { + [output addObject:element]; + } + }); + return output; + } + + if(error != nil) { + *error = [NSError errorWithDomain:@"IJSVGiOSXML" + code:1004 + userInfo:nil]; + } + return @[]; +} + +- (NSString*)XMLStringWithOptions:(NSXMLNodeOptions)options +{ + if(_rootElement == nil) { + return @""; + } + NSString* version = _version ?: @"1.0"; + NSString* charset = _characterEncoding ?: @"UTF-8"; + NSMutableString* output = [[NSMutableString alloc] initWithFormat:@"", + version, + charset]; + if((options & NSXMLNodePrettyPrint) != 0) { + [output appendString:@"\n"]; + } + IJSVGiOSXMLAppendNodeString(_rootElement, output, options, 0); + return output; +} + +@end +#endif diff --git a/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.h b/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.h index 2fbd4f0c..dc49d689 100644 --- a/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.h +++ b/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.h @@ -9,6 +9,7 @@ #import #import #import +#import @class IJSVG; @class IJSVGExporter; diff --git a/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.m b/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.m index 6b3475a5..60a00d97 100644 --- a/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.m +++ b/Framework/IJSVG/IJSVG/Source/Exporter/IJSVGExporter.m @@ -1109,12 +1109,15 @@ - (NSString*)base64EncodedStringFromCGImage:(CGImageRef)image return nil; } - // convert the CGImage into an NSImage + // convert the CGImage into PNG data +#if TARGET_OS_IOS + NSImage* repImage = [NSImage imageWithCGImage:image]; + NSData* data = UIImagePNGRepresentation(repImage); +#else NSBitmapImageRep* rep = [[NSBitmapImageRep alloc] initWithCGImage:image]; - - // work out the data NSData* data = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}]; +#endif NSString* base64String = [data base64EncodedStringWithOptions:0]; return [@"data:image/png;base64," stringByAppendingString:base64String]; @@ -1305,7 +1308,7 @@ - (void)applyGradientFromLayer:(IJSVGGradientLayer*)layer } // we need to work out the color at this point, annoyingly... - CGFloat opacity = aColor.alphaComponent; + CGFloat opacity = IJSVGColorAlphaComponent(aColor); // is opacity is equal to 1, no need to add it as spec // defaults opacity to 1 anyway :) @@ -1355,6 +1358,27 @@ - (void)applyGradientFromLayer:(IJSVGGradientLayer*)layer - (NSXMLElement*)elementForFilter:(IJSVGFilterLayer*)layer fromParent:(NSXMLElement*)parent { + IJSVGFilter* filter = layer.filter; + if(filter.defElement != nil) { + NSXMLElement* filterDef = (NSXMLElement*)[filter.defElement copy]; + NSString* filterKey = [self identifierForElement:filterDef]; + [filterDef removeAttributeForName:IJSVGAttributeID]; + NSXMLNode* idAttribute = [[NSXMLNode alloc] initWithKind:NSXMLAttributeKind]; + idAttribute.name = IJSVGAttributeID; + idAttribute.stringValue = filterKey; + [filterDef addAttribute:idAttribute]; + [[self defElement] addChild:filterDef]; + + NSXMLElement* wrapper = [[NSXMLElement alloc] init]; + wrapper.name = @"g"; + IJSVGApplyAttributesToElement(@{ + IJSVGAttributeFilter: IJSVGHashURL(filterKey) + }, wrapper); + [self applyDefaultsToElement:wrapper fromLayer:layer]; + [self _recursiveParseFromLayer:(CALayer*)layer.sublayer + intoElement:wrapper]; + return wrapper; + } [self _recursiveParseFromLayer:(CALayer*)layer.sublayer intoElement:parent]; return nil; @@ -1395,9 +1419,13 @@ - (NSXMLElement*)elementForImage:(IJSVGImageLayer*)layer imageHeight*ratio); NSImage* actualImage = [IJSVGUtils resizeImage:nsImage toSize:newImageRect.size]; +#if TARGET_OS_IOS + cgImage = actualImage.CGImage; +#else cgImage = [actualImage CGImageForProposedRect:&newImageRect context:NULL hints:NULL]; +#endif } else { cgImage = image.CGImage; } diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGFilterLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGFilterLayer.m index 5b39be80..61ab3bf5 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGFilterLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGFilterLayer.m @@ -9,11 +9,35 @@ #import #import -@implementation IJSVGFilterLayer +@implementation IJSVGFilterLayer { + CGRect _filterFrame; +} + +- (void)setOwnedImage:(CGImageRef)image +{ + if(_image == image) { + return; + } + + _hostingLayer.contents = nil; + + if(_image != NULL) { + CGImageRelease(_image); + _image = NULL; + } + + if(image != NULL) { + _image = CGImageRetain(image); + } +} - (void)dealloc { - (void)CGImageRelease(_image), _image = nil; + _hostingLayer.contents = nil; + if(_image != NULL) { + CGImageRelease(_image); + _image = NULL; + } } - (instancetype)init @@ -32,12 +56,10 @@ - (BOOL)requiresBackingScale - (void)setBackingScaleFactor:(CGFloat)backingScaleFactor { - // we are responsible for recursively calling the sublayer - // with the new backing scale factor [IJSVGLayer setBackingScaleFactor:backingScaleFactor renderQuality:self.renderQuality recursivelyToLayer:_sublayer]; - + BOOL needsChange = self.backingScaleFactor != backingScaleFactor; [super setBackingScaleFactor:backingScaleFactor]; if(needsChange == YES) { @@ -47,17 +69,39 @@ - (void)setBackingScaleFactor:(CGFloat)backingScaleFactor - (void)updateImage { - if(_image != nil) { - (void)CGImageRelease(_image), _image = nil; + CGFloat scale = self.backingScaleFactor; + if(scale <= 0) scale = 1.0; + [IJSVGLayer setBackingScaleFactor:scale + renderQuality:self.renderQuality + recursivelyToLayer:_sublayer]; + _filterFrame = _sublayer.innerBoundingBox; + CGImageRef image = [self.filter newImageByApplyFilterToLayer:_sublayer + scale:scale + outputFrame:&_filterFrame]; + [self setOwnedImage:image]; + if(image != NULL) { + CGImageRelease(image); + } +} + +- (void)performRenderInContext:(CGContextRef)ctx +{ + if(_image == NULL) { + [self updateImage]; + } + CGImageRef image = _image; + if(image != NULL) { + CGImageRetain(image); + } + if(image != NULL) { + CGContextDrawImage(ctx, _filterFrame, image); + CGImageRelease(image); } - _image = [self.filter newImageByApplyFilterToLayer:_sublayer - scale:self.backingScaleFactor]; } - (void)layoutSublayers { - CGRect frame = _sublayer.innerBoundingBox; - _hostingLayer.frame = frame; + _hostingLayer.frame = _filterFrame; _hostingLayer.contents = (__bridge id)_image; } diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGGradientLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGGradientLayer.m index 4d9397ee..dc27bbd3 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGGradientLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGGradientLayer.m @@ -8,6 +8,7 @@ #import #import +#import @implementation IJSVGGradientLayer @@ -23,7 +24,7 @@ - (void)setGradient:(IJSVGGradient*)newGradient // lets check its alpha properties on the colors BOOL hasAlphaChannel = NO; for (NSColor* color in newGradient.colors) { - if(color.alphaComponent != 1.f) { + if(IJSVGColorAlphaComponent(color) != 1.f) { hasAlphaChannel = YES; break; } @@ -76,9 +77,28 @@ - (void)drawInContext:(CGContextRef)ctx CGAffineTransform transform = CGAffineTransformIdentity; CALayer* layer = (CALayer*)self.referencingLayer; if(self.gradient.units == IJSVGUnitUserSpaceOnUse) { - IJSVGRootLayer* rootNode = (IJSVGRootLayer*)[IJSVGLayer rootLayerForLayer:self]; - bounds = [rootNode.viewBox computeValue:CGSizeZero]; + CALayer* rootCandidate = [IJSVGLayer rootLayerForLayer:self]; + if([rootCandidate isKindOfClass:IJSVGRootLayer.class]) { + IJSVGRootLayer* rootNode = (IJSVGRootLayer*)rootCandidate; + bounds = [rootNode.viewBox computeValue:CGSizeZero]; + } else { + bounds = layer.frame; + } transform = [IJSVGLayer userSpaceTransformForLayer:layer]; + + // When rendering inside a filter context, the element was moved to (0,0) + // but the gradient needs the original position to compute coordinates. + NSValue* filterOffset = [IJSVGThreadManager.currentManager userInfoObjectForKey:@"IJSVGFilterElementOffset"]; + // In filter rendering we only need to restore the saved offset for the + // top-level filtered layer that was normalized to the origin. Descendant + // layers still carry their absolute outerBoundingBox, so applying the + // saved element offset again shifts user-space gradients twice. + if(filterOffset != nil && + CGRectGetMinX(layer.outerBoundingBox) == 0.f && + CGRectGetMinY(layer.outerBoundingBox) == 0.f) { + NSPoint offset = filterOffset.pointValue; + transform = CGAffineTransformTranslate(transform, -offset.x, -offset.y); + } } else { bounds = IJSVGLayerGetBoundingBoxBounds(layer); } diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.h b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.h index 7b220b1d..2bb79c13 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.h +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.h @@ -11,8 +11,8 @@ #import #import #import -#import #import +#import @interface IJSVGImageLayer : IJSVGTileLayer { } diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.m index ad8d184e..6afbf574 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGImageLayer.m @@ -32,10 +32,11 @@ - (void)setImage:(IJSVGImage *)image - (void)drawInContext:(CGContextRef)ctx { CGImageRef image = _image.CGImage; - CGRect imageDrawRect = _image.bounds; + CGRect imageDrawRect = _image.intrinsicBounds; CGRect currentBounds = self.bounds; IJSVGViewBoxDrawingBlock drawBlock = ^(CGFloat scale[]) { // image will be upside down, so just translate it back on itself + CGContextSetInterpolationQuality(ctx, kCGInterpolationHigh); CGContextConcatCTM(ctx, CGAffineTransformMakeScale(1.f, -1.f)); CGContextTranslateCTM(ctx, 0.f, -CGRectGetHeight(imageDrawRect)); CGContextDrawImage(ctx, imageDrawRect, image); diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.h b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.h index 0a549b5b..496d9abd 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.h +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.h @@ -69,6 +69,7 @@ typedef NS_ENUM(NSUInteger, IJSVGLayerUsageType) { @required @property (nonatomic, assign) CGRect maskingBoundingBox; @property (nonatomic, assign) CGRect maskingClippingRect; +@property (nonatomic, assign) BOOL maskUsesAlpha; @end @@ -164,6 +165,9 @@ NSMapTable*>* IJSVGLayerDefaultUsageMapTa inContext:(CGContextRef)ctx options:(IJSVGLayerDrawingOptions)options; ++ (void)renderLayerTree:(CALayer*)layer + inContext:(CGContextRef)ctx; + + (void)applyBlendingMode:(CGBlendMode)blendMode toContext:(CGContextRef)ctx drawingBlock:(dispatch_block_t)drawingBlock; @@ -186,6 +190,5 @@ NSMapTable*>* IJSVGLayerDefaultUsageMapTa + (void)transformLayer:(CALayer*)layer intoUserSpaceUnitsFrom:(CALayer*)fromLayer; -+ (void)logLayer:(CALayer*)layer; @end diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.m index b37a529a..2e5c21eb 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGLayer.m @@ -12,6 +12,7 @@ #import #import #import +#import CGRect IJSVGLayerGetBoundingBoxBounds(CALayer* drawableLayer) { @@ -28,6 +29,19 @@ CGRect IJSVGLayerGetBoundingBoxBounds(CALayer* drawableLayer capacity:3]; } +static inline BOOL IJSVGRectIsFinite(CGRect rect) +{ + return isfinite(rect.origin.x) && + isfinite(rect.origin.y) && + isfinite(rect.size.width) && + isfinite(rect.size.height); +} + +static void IJSVGMaskImageReleaseData(void *info, const void *data, size_t size) +{ + free((void *)data); +} + @implementation IJSVGLayer @@ -42,6 +56,7 @@ @implementation IJSVGLayer @synthesize clippingTransform; @synthesize clippingBoundingBox; @synthesize maskingClippingRect; +@synthesize maskUsesAlpha; @synthesize clipPathTransform; @synthesize colors; @synthesize boundingBox = _boundingBox; @@ -220,6 +235,128 @@ + (void)performBasicRenderOfLayer:(CALayer*)layer }]; } ++ (void)renderLayerTree:(CALayer*)layer + inContext:(CGContextRef)ctx +{ + if(layer == nil) { + return; + } + + // 1. Let the layer draw its own content (gradients, patterns, images) + // Skip CAShapeLayer subclasses — we draw those manually below + if(![layer isKindOfClass:CAShapeLayer.class]) { + [layer drawInContext:ctx]; + } + + // 2. If this is a CAShapeLayer, manually draw the path fill and stroke + if([layer isKindOfClass:CAShapeLayer.class]) { + CAShapeLayer* shapeLayer = (CAShapeLayer*)layer; + CGPathRef path = shapeLayer.path; + if(path != NULL) { + if(shapeLayer.fillColor != NULL) { + CGContextAddPath(ctx, path); + CGContextSetFillColorWithColor(ctx, shapeLayer.fillColor); + if([shapeLayer.fillRule isEqualToString:kCAFillRuleEvenOdd]) { + CGContextEOFillPath(ctx); + } else { + CGContextFillPath(ctx); + } + } + if(shapeLayer.strokeColor != NULL && shapeLayer.lineWidth > 0) { + CGContextSetStrokeColorWithColor(ctx, shapeLayer.strokeColor); + CGContextSetLineWidth(ctx, shapeLayer.lineWidth); + if([shapeLayer.lineCap isEqualToString:kCALineCapRound]) { + CGContextSetLineCap(ctx, kCGLineCapRound); + } else if([shapeLayer.lineCap isEqualToString:kCALineCapSquare]) { + CGContextSetLineCap(ctx, kCGLineCapSquare); + } else { + CGContextSetLineCap(ctx, kCGLineCapButt); + } + if([shapeLayer.lineJoin isEqualToString:kCALineJoinRound]) { + CGContextSetLineJoin(ctx, kCGLineJoinRound); + } else if([shapeLayer.lineJoin isEqualToString:kCALineJoinBevel]) { + CGContextSetLineJoin(ctx, kCGLineJoinBevel); + } else { + CGContextSetLineJoin(ctx, kCGLineJoinMiter); + } + CGContextSetMiterLimit(ctx, shapeLayer.miterLimit); + NSArray* dashPattern = shapeLayer.lineDashPattern; + if(dashPattern.count > 0) { + CGFloat* dashes = (CGFloat*)malloc(dashPattern.count * sizeof(CGFloat)); + for(NSUInteger i = 0; i < dashPattern.count; i++) { + dashes[i] = [dashPattern[i] doubleValue]; + } + CGContextSetLineDash(ctx, shapeLayer.lineDashPhase, dashes, dashPattern.count); + free(dashes); + } + CGContextAddPath(ctx, path); + CGContextStrokePath(ctx); + } + } + } + + // 3. Render sublayers + for(CALayer* sublayer in layer.sublayers) { + if(sublayer.isHidden) { + continue; + } + if([sublayer conformsToProtocol:@protocol(IJSVGDrawableLayer)]) { + CALayer* drawableSublayer = (CALayer*)sublayer; + + CGContextSaveGState(ctx); + + // Apply sublayer position and transform (anchor-point aware) + CGRect sublayerBounds = drawableSublayer.bounds; + CGPoint sublayerPosition = drawableSublayer.position; + CGPoint anchorPoint = drawableSublayer.anchorPoint; + CGFloat anchorX = anchorPoint.x * sublayerBounds.size.width; + CGFloat anchorY = anchorPoint.y * sublayerBounds.size.height; + + CGContextTranslateCTM(ctx, sublayerPosition.x, sublayerPosition.y); + + CGAffineTransform transform = drawableSublayer.affineTransform; + if(!CGAffineTransformIsIdentity(transform)) { + CGContextConcatCTM(ctx, transform); + } + + CGContextTranslateCTM(ctx, -anchorX, -anchorY); + + float sublayerOpacity = drawableSublayer.opacity; + + if(sublayerOpacity < 1.0f && sublayerOpacity > 0.0f) { + // Use transparency layer for correct opacity compositing + CGContextSetAlpha(ctx, sublayerOpacity); + CGContextBeginTransparencyLayer(ctx, NULL); + CGContextSetAlpha(ctx, 1.0f); + float savedOpacity = drawableSublayer.opacity; + drawableSublayer.opacity = 1.0f; + [self performBasicRenderOfLayer:drawableSublayer + inContext:ctx + options:IJSVGLayerDrawingOptionNone]; + drawableSublayer.opacity = savedOpacity; + CGContextEndTransparencyLayer(ctx); + } else if(sublayerOpacity > 0.0f) { + [self performBasicRenderOfLayer:drawableSublayer + inContext:ctx + options:IJSVGLayerDrawingOptionNone]; + } + + CGContextRestoreGState(ctx); + } else { + // Non-IJSVG layers (e.g., IJSVGBasicLayer hosting filtered content) + CGContextSaveGState(ctx); + CGRect frame = sublayer.frame; + CGContextTranslateCTM(ctx, frame.origin.x, frame.origin.y); + id contents = sublayer.contents; + if(contents != nil) { + CGImageRef image = (__bridge CGImageRef)contents; + CGContextDrawImage(ctx, sublayer.bounds, image); + } + CGContextRestoreGState(ctx); + } + } +} + + (void)renderLayer:(CALayer*)layer inContext:(CGContextRef)ctx options:(IJSVGLayerDrawingOptions)options @@ -343,7 +480,9 @@ + (void)clipContextWithClipLayers:(NSArray*>*)clipLa CGRect maskRect = CGRectInset(rect, -maskTolerance, -maskTolerance); CGContextClipToMask(maskCtx, maskRect, maskImage); } - CGContextDrawImage(maskCtx, layerRect, layerMask); + CGContextClipToMask(maskCtx, layerRect, layerMask); + CGContextSetGrayFillColor(maskCtx, 1.f, 1.f); + CGContextFillRect(maskCtx, layerRect); CGImageRelease(layerMask); if(maskImage != NULL) { CGImageRelease(maskImage); @@ -360,10 +499,10 @@ + (void)clipContextWithClipLayers:(NSArray*>*)clipLa bitmapInfo:kCGImageAlphaNone scale:scale]; - // we need to transform the mask rect back based on the inner bounding - // box of the layer, as this could be a group layer that inner box is - // not at 0,0. - CGRect layerRect = layer.innerBoundingBox; + // Clip masks are applied in the target layer's local coordinate space. + // `innerBoundingBox` reintroduces absolute origin after the relative- + // coordinate transition and shifts chained clip-path results. + CGRect layerRect = layer.bounds; CGAffineTransform transform = CGAffineTransformMakeTranslation(CGRectGetMinX(layerRect), CGRectGetMinY(layerRect)); rect = CGRectApplyAffineTransform(rect, transform); @@ -395,33 +534,83 @@ + (CGImageRef)newMaskImageForLayer:(CALayer*)layer options:(IJSVGLayerDrawingOptions)options scale:(CGFloat)scale { - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray(); - CGImageRef alphaMask = [self newImageForLayer:layer - options:options - colorSpace:colorSpace - bitmapInfo:kCGImageAlphaNone - scale:scale]; - // low - high pairs - const CGFloat colors[6] = { - 0.f, 11.f, - 0.f, 11.f, - 0.f, 11.f - }; - CGImageRef masked = CGImageCreateWithMaskingColors(alphaMask, colors); - CGImageRelease(alphaMask); - CGColorSpaceRelease(colorSpace); - return masked; + // Render the mask content in RGBA (not grayscale) so gradient fills + // and other complex content renders correctly. + CGColorSpaceRef rgbCS = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGImageRef rgbImage = [self newImageForLayer:layer + options:options + colorSpace:rgbCS + bitmapInfo:kCGImageAlphaPremultipliedLast + scale:scale]; + CGColorSpaceRelease(rgbCS); + if(rgbImage == NULL) return NULL; + + // Convert to grayscale luminance mask. + // SVG spec: mask uses luminance of mask content to determine opacity. + size_t w = CGImageGetWidth(rgbImage); + size_t h = CGImageGetHeight(rgbImage); + size_t rgbRowBytes = w * 4; + uint8_t* rgbBuf = (uint8_t*)calloc(h, rgbRowBytes); + + CGColorSpaceRef srgb = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef readCtx = CGBitmapContextCreate(rgbBuf, w, h, 8, rgbRowBytes, srgb, + kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); + CGContextDrawImage(readCtx, CGRectMake(0, 0, w, h), rgbImage); + CGContextRelease(readCtx); + CGImageRelease(rgbImage); + + BOOL usesAlphaMask = layer.maskUsesAlpha; + // Create grayscale mask from either the alpha channel or SVG luminance. + size_t grayRowBytes = w; + uint8_t* grayBuf = (uint8_t*)calloc(h, grayRowBytes); + for(size_t y = 0; y < h; y++) { + for(size_t x = 0; x < w; x++) { + size_t idx = y * rgbRowBytes + x * 4; + uint8_t r = rgbBuf[idx]; + uint8_t g = rgbBuf[idx + 1]; + uint8_t b = rgbBuf[idx + 2]; + uint8_t a = rgbBuf[idx + 3]; + // Unpremultiply + if(a > 0 && a < 255) { + r = (uint8_t)MIN(255, r * 255 / a); + g = (uint8_t)MIN(255, g * 255 / a); + b = (uint8_t)MIN(255, b * 255 / a); + } + if(usesAlphaMask == YES) { + grayBuf[y * grayRowBytes + x] = 255 - a; + } else { + uint8_t lum = (uint8_t)(0.2126f * r + 0.7152f * g + 0.0722f * b); + grayBuf[y * grayRowBytes + x] = 255 - (uint8_t)(lum * a / 255); + } + } + } + free(rgbBuf); + + CGColorSpaceRelease(srgb); + + CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, + grayBuf, + h * grayRowBytes, + IJSVGMaskImageReleaseData); + CGImageRef maskImage = CGImageMaskCreate(w, + h, + 8, + 8, + grayRowBytes, + provider, + NULL, + false); + CGDataProviderRelease(provider); + return maskImage; } + + (CGImageRef)newImageWithSize:(CGSize)size drawBlock:(void (^)(CGContextRef context))drawBlock colorSpace:(CGColorSpaceRef)colorSpace bitmapInfo:(uint32_t)bitmapInfo scale:(CGFloat)scale { - if (!IJSVGIsValidContextSize(size)) { - return nil; - } CGContextRef offscreenContext = CGBitmapContextCreate(NULL, ceilf(size.width*scale), ceilf(size.height*scale), @@ -441,14 +630,7 @@ + (CGImageRef)newImageForLayer:(CALayer*)layer { CALayer* referenceLayer = layer.referencingLayer ?: layer; CGRect frame = layer.outerBoundingBox; - - // Make sure we actually have a width and a height or there is no point of - // attemping to create an image as it will fail with the context creation. - if (!IJSVGIsValidContextSize(frame.size)) { - return nil; - } - - CGRect bounds = CGRectApplyAffineTransform(layer.innerBoundingBox, + CGRect bounds = CGRectApplyAffineTransform(layer.bounds, [self userSpaceTransformForLayer:referenceLayer]); CGContextRef offscreenContext = CGBitmapContextCreate(NULL, ceilf(frame.size.width*scale), @@ -529,26 +711,6 @@ + (void)setBackingScaleFactor:(CGFloat)scale }]; } -+ (void)logLayer:(CALayer*)layer -{ - [self logLayer:layer - depth:0]; -} - -+ (void)logLayer:(CALayer*)layer - depth:(NSUInteger)depth -{ - NSLog(@"%@ %@ frame: %@ transform: %@",[@"" stringByPaddingToLength:depth - withString:@"- - " - startingAtIndex:0], layer, - NSStringFromRect(layer.frame), - [IJSVGTransform affineTransformToSVGTransformComponentString:layer.affineTransform]); - for(CALayer* sublayer in layer.debugLayers) { - [self logLayer:sublayer - depth:depth+1]; - } -} - + (CGRect)calculateFrameForSublayers:(NSArray*>*)layers { CGRect rect = CGRectNull; @@ -558,8 +720,15 @@ + (CGRect)calculateFrameForSublayers:(NSArray*>*)lay // to its sublayers and keep going down the tree if([layer isKindOfClass:IJSVGTransformLayer.class] == YES) { CGRect frame = [self calculateFrameForSublayers:layer.sublayers]; - frame = CGRectApplyAffineTransform(frame, layer.affineTransform); - layerFrame = frame; + if(CGRectIsNull(frame) == NO && + IJSVGRectIsFinite(frame) == YES) { + frame = CGRectApplyAffineTransform(frame, layer.affineTransform); + layerFrame = frame; + } + } + if(CGRectIsNull(layerFrame) == YES || + IJSVGRectIsFinite(layerFrame) == NO) { + continue; } if(CGRectIsNull(rect)) { rect = layerFrame; @@ -567,7 +736,7 @@ + (CGRect)calculateFrameForSublayers:(NSArray*>*)lay } rect = CGRectUnion(rect, layerFrame); } - return rect; + return CGRectIsNull(rect) == NO ? rect : CGRectZero; } - (void)setBackingScaleFactor:(CGFloat)newFactor @@ -594,11 +763,11 @@ - (void)performRenderInContext:(CGContextRef)ctx toLayer:self inContext:ctx drawingBlock:^{ - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; }]; return; } - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; } - (void)applySublayerMaskToContext:(CGContextRef)context diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGPatternLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGPatternLayer.m index 50c663c4..73835aec 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGPatternLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGPatternLayer.m @@ -15,11 +15,25 @@ @interface IJSVGPatternLayer () @property (nonatomic, assign, readonly) CGSize cellSize; @property (nonatomic, assign) CGRect viewBox; +@property (nonatomic, assign) BOOL usesImplicitObjectBoundingBoxViewBox; @end @implementation IJSVGPatternLayer +static BOOL IJSVGPatternLayerContainsImageLayer(CALayer* layer) +{ + if([layer isKindOfClass:NSClassFromString(@"IJSVGImageLayer")]) { + return YES; + } + for(CALayer* sublayer in layer.sublayers) { + if(IJSVGPatternLayerContainsImageLayer(sublayer) == YES) { + return YES; + } + } + return NO; +} + - (BOOL)requiresBackingScale { return YES; @@ -44,15 +58,38 @@ void IJSVGPatternDrawingCallBack(void* info, CGContextRef ctx) IJSVGViewBoxAlignment alignment = layer.patternNode.viewBoxAlignment; IJSVGViewBoxMeetOrSlice meetOrSlice = layer.patternNode.viewBoxMeetOrSlice; + if(layer.usesImplicitObjectBoundingBoxViewBox == YES) { + alignment = IJSVGViewBoxAlignmentNone; + } CGRect viewBox = layer.viewBox; IJSVGViewBoxDrawingBlock drawBlock = ^(CGFloat scale[]) { + // Pattern content is rendered as a standalone layer subtree, so the + // root pattern layer's frame origin is otherwise ignored. Preserve + // that local origin here so stroked or offset pattern content aligns + // to the tile the same way WebKit does. + CGRect patternFrame = layer.pattern.frame; + BOOL hasMeaningfulExtent = isfinite(patternFrame.size.width) && + isfinite(patternFrame.size.height) && + patternFrame.size.width > 0.f && + patternFrame.size.height > 0.f; + if(layer.usesImplicitObjectBoundingBoxViewBox == NO && + hasMeaningfulExtent == YES && + (patternFrame.origin.x != 0.f || patternFrame.origin.y != 0.f)) { + CGContextSaveGState(ctx); + CGContextTranslateCTM(ctx, patternFrame.origin.x, patternFrame.origin.y); + [IJSVGLayer renderLayer:layer.pattern + inContext:ctx + options:IJSVGLayerDrawingOptionNone]; + CGContextRestoreGState(ctx); + return; + } [IJSVGLayer renderLayer:layer.pattern inContext:ctx options:IJSVGLayerDrawingOptionNone]; }; IJSVGContextDrawViewBox(ctx, viewBox, rect, alignment, meetOrSlice, drawBlock); - CGContextSaveGState(ctx); + CGContextRestoreGState(ctx); }; - (CALayer*)referencingLayer @@ -66,6 +103,7 @@ - (void)computeCellSize:(CGSize*)cellSize { CALayer* layer = (CALayer*)self.referencingLayer; CGRect rect = IJSVGLayerGetBoundingBoxBounds(layer); + self.usesImplicitObjectBoundingBoxViewBox = NO; // get the bounds, we need these as when we render we might need to swap // the coordinates over to objectBoundingBox @@ -98,8 +136,24 @@ - (void)computeCellSize:(CGSize*)cellSize } *viewBox = [nViewBox computeValue:rect.size]; } else { - // no viewbox is assigned, so just map it 1:1 with its cellSize - *viewBox = CGRectMake(0.f, 0.f, cellSize->width, cellSize->height); + // Without an explicit viewBox, pattern content defaults to the pattern + // content coordinate system. For objectBoundingBox content with raster + // content the implicit viewBox is the OBB tile (0,0,1,1); anything an + // authored transform or oversized places outside that + // tile is clipped by the pattern boundary, matching WebKit. Using the + // children's outer bounding box here instead would re-fit the + // transformed content back into the tile and cancel out the very + // transforms that should determine what's visible. + if(_patternNode.contentUnits == IJSVGUnitObjectBoundingBox) { + if(IJSVGPatternLayerContainsImageLayer(_pattern) == YES) { + *viewBox = CGRectMake(0.f, 0.f, 1.f, 1.f); + self.usesImplicitObjectBoundingBoxViewBox = YES; + } else { + *viewBox = CGRectMake(0.f, 0.f, cellSize->width, cellSize->height); + } + } else { + *viewBox = CGRectMake(0.f, 0.f, cellSize->width, cellSize->height); + } } } @@ -133,10 +187,20 @@ - (void)drawInContext:(CGContextRef)ctx // its possible that this layer is shifted inwards due to a stroke on the // parent layer transform = CGAffineTransformConcat(transform, [IJSVGLayer userSpaceTransformForLayer:self]); - + + // Quartz invokes the CGPattern callback in pattern space rather than the + // caller's transformed user space. Preserve the active context transform + // in the pattern matrix so userSpaceOnUse tiles track root scaling and + // element transforms such as rotation. + CGAffineTransform contextTransform = CGContextGetCTM(ctx); + if(CGAffineTransformIsIdentity(contextTransform) == NO) { + transform = CGAffineTransformConcat(transform, contextTransform); + } + // create the pattern CGRect selfBounds = IJSVGLayerGetBoundingBoxBounds(self); - CGPatternRef ref = CGPatternCreate((void*)self, selfBounds, + CGRect patternBounds = CGRectMake(0.f, 0.f, _cellSize.width, _cellSize.height); + CGPatternRef ref = CGPatternCreate((void*)self, patternBounds, transform, _cellSize.width, _cellSize.height, kCGPatternTilingConstantSpacing, true, &callbacks); diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.h b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.h index 123a5423..ff3ea3f0 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.h +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.h @@ -15,6 +15,9 @@ } +@property (nonatomic, assign) BOOL rendersWithViewBoxTransform; +@property (nonatomic, assign) BOOL hasExplicitViewBox; + - (void)renderInContext:(CGContextRef)ctx viewPort:(CGRect)viewPort backingScale:(CGFloat)backingScale diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.m index 19bcc245..35021bef 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGRootLayer.m @@ -12,26 +12,36 @@ @implementation IJSVGRootLayer - (void)performRenderInContext:(CGContextRef)ctx { - if(self.viewBox != nil) { + if(self.rendersWithViewBoxTransform == YES && self.viewBox != nil) { CGRect viewBox = [self.viewBox computeValue:CGSizeZero]; - __weak IJSVGRootLayer* weakSelf = self; - IJSVGViewBoxDrawingBlock drawingBlock = ^(CGFloat scale[]) { - CGContextSaveGState(ctx); - // we have to make sure we set the backing scale factor once - // we know how scale this will be drawn at - CGFloat nScale = MIN(scale[0], scale[1]); - nScale += weakSelf.backingScaleFactor; - weakSelf.backingScaleFactor = nScale; - - // perform the actual render now we have computed backing scale - [super performRenderInContext:ctx]; - CGContextRestoreGState(ctx); - }; - - IJSVGContextDrawViewBox(ctx, viewBox, IJSVGLayerGetBoundingBoxBounds(self), - self.viewBoxAlignment, - self.viewBoxMeetOrSlice, drawingBlock); - return; + BOOL hasViewBox = (isfinite(CGRectGetWidth(viewBox)) && + isfinite(CGRectGetHeight(viewBox)) && + CGRectGetWidth(viewBox) > 0.f && + CGRectGetHeight(viewBox) > 0.f); + // For SVGs without an explicit viewBox, the parser synthesises one + // from the intrinsic width/height. When such an SVG is rendered into + // a viewport of different aspect, browsers embedding it via + // stretch the canvas to fill the box (object-fit: fill default), so + // use IJSVGViewBoxAlignmentNone here. Otherwise — explicit viewBox + // — honour the SVG's own preserveAspectRatio settings. + IJSVGViewBoxAlignment alignment = self.hasExplicitViewBox + ? self.viewBoxAlignment + : IJSVGViewBoxAlignmentNone; + IJSVGViewBoxMeetOrSlice meetOrSlice = self.viewBoxMeetOrSlice; + if(hasViewBox == YES) { + __weak IJSVGRootLayer* weakSelf = self; + IJSVGViewBoxDrawingBlock drawingBlock = ^(CGFloat scale[]) { + CGContextSaveGState(ctx); + CGFloat nScale = MIN(scale[0], scale[1]); + nScale += weakSelf.backingScaleFactor; + weakSelf.backingScaleFactor = nScale; + [super performRenderInContext:ctx]; + CGContextRestoreGState(ctx); + }; + IJSVGContextDrawViewBox(ctx, viewBox, IJSVGLayerGetBoundingBoxBounds(self), + alignment, meetOrSlice, drawingBlock); + return; + } } [super performRenderInContext:ctx]; } @@ -63,15 +73,18 @@ - (void)renderInContext:(CGContextRef)ctx ignoreIntrinsicSize:(BOOL)ignoreIntrinsicSize { CGRect frame = viewPort; - if(CGRectIsInfinite(viewPort)) { - return; - } - IJSVGUnitSize* size = nil; - - // The SVG might have an intrinsic size against it, if so, we need to use - // that instead of the viewPort size to make sure we obey the render correctly. - if(ignoreIntrinsicSize == NO && (size = self.intrinsicSize) != nil) { + + // For SVGs *without* an explicit viewBox we keep the frame at the + // viewport rect, so the implicit viewBox synthesised in + // performRenderInContext: actually scales content into the requested + // rect (matching -style fill). For SVGs *with* an explicit viewBox + // we preserve the original behaviour and use the intrinsic size as the + // frame — that's the existing render path the broader sweep corpus + // relies on and it would regress here otherwise. + if(ignoreIntrinsicSize == NO && + self.hasExplicitViewBox == YES && + (size = self.intrinsicSize) != nil) { CGFloat width = 0.f; CGFloat height = 0.f; if((width = [size.width computeValue:frame.size.width]) != 0.f) { diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGShapeLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGShapeLayer.m index 729b6606..85f5e9c1 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGShapeLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGShapeLayer.m @@ -36,6 +36,7 @@ @implementation IJSVGShapeLayer @synthesize outerBoundingBox; @synthesize maskLayer = _maskLayer; @synthesize treatImplicitOriginAsTransform; +@synthesize maskUsesAlpha = _maskUsesAlpha; - (void)dealloc { @@ -117,11 +118,11 @@ - (void)performRenderInContext:(CGContextRef)ctx toLayer:self inContext:ctx drawingBlock:^{ - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; }]; return; } - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; } - (void)applySublayerMaskToContext:(CGContextRef)context diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTileLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTileLayer.m index e8160a7f..c762ef84 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTileLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTileLayer.m @@ -7,6 +7,7 @@ // #import "IJSVGTileLayer.h" +#import "IJSVGLayer.h" @implementation IJSVGTileLayer @@ -33,6 +34,7 @@ @implementation IJSVGTileLayer @synthesize absoluteOrigin; @synthesize blendingMode; @synthesize colors; +@synthesize maskUsesAlpha = _maskUsesAlpha; - (void)dealloc { @@ -65,7 +67,7 @@ - (void)renderInContext:(CGContextRef)ctx - (void)performRenderInContext:(CGContextRef)ctx { - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; } - (NSArray*>*)debugLayers diff --git a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTransformLayer.m b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTransformLayer.m index a816e660..d79ef5cf 100644 --- a/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTransformLayer.m +++ b/Framework/IJSVG/IJSVG/Source/Layers/IJSVGTransformLayer.m @@ -34,6 +34,7 @@ @implementation IJSVGTransformLayer @synthesize absoluteOrigin; @synthesize blendingMode; @synthesize referencingLayer = _referencingLayer; +@synthesize maskUsesAlpha = _maskUsesAlpha; - (void)dealloc { @@ -66,7 +67,7 @@ - (void)renderInContext:(CGContextRef)ctx - (void)performRenderInContext:(CGContextRef)ctx { - [super renderInContext:ctx]; + [IJSVGLayer renderLayerTree:self inContext:ctx]; } - (NSArray*>*)debugLayers diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.h new file mode 100644 index 00000000..01123b47 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.h @@ -0,0 +1,21 @@ +// +// IJSVGFilterEffectBlend.h +// IJSVG +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGFilterBlendMode) { + IJSVGFilterBlendModeNormal, + IJSVGFilterBlendModeMultiply, + IJSVGFilterBlendModeScreen, + IJSVGFilterBlendModeDarken, + IJSVGFilterBlendModeLighten, + IJSVGFilterBlendModeOverlay, +}; + +@interface IJSVGFilterEffectBlend : IJSVGFilterEffect + +@property (nonatomic, assign) IJSVGFilterBlendMode filterBlendMode; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.m new file mode 100644 index 00000000..3997d0d5 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectBlend.m @@ -0,0 +1,50 @@ +// +// IJSVGFilterEffectBlend.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectBlend + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + NSString* mode = attributes[@"mode"]; + if(mode != nil) { + NSString* lower = mode.lowercaseString; + if([lower isEqualToString:@"multiply"]) _filterBlendMode = IJSVGFilterBlendModeMultiply; + else if([lower isEqualToString:@"screen"]) _filterBlendMode = IJSVGFilterBlendModeScreen; + else if([lower isEqualToString:@"darken"]) _filterBlendMode = IJSVGFilterBlendModeDarken; + else if([lower isEqualToString:@"lighten"]) _filterBlendMode = IJSVGFilterBlendModeLighten; + else if([lower isEqualToString:@"overlay"]) _filterBlendMode = IJSVGFilterBlendModeOverlay; + } +} + +- (NSString*)ciFilterNameForBlendMode +{ + switch(_filterBlendMode) { + case IJSVGFilterBlendModeNormal: return @"CISourceOverCompositing"; + case IJSVGFilterBlendModeMultiply: return @"CIMultiplyBlendMode"; + case IJSVGFilterBlendModeScreen: return @"CIScreenBlendMode"; + case IJSVGFilterBlendModeDarken: return @"CIDarkenBlendMode"; + case IJSVGFilterBlendModeLighten: return @"CILightenBlendMode"; + case IJSVGFilterBlendModeOverlay: return @"CIOverlayBlendMode"; + } + return @"CISourceOverCompositing"; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* in1 = [graph imageForInput:self.inputName]; + CIImage* in2 = [graph imageForInput:self.inputName2]; + CIFilter* filter = [CIFilter filterWithName:[self ciFilterNameForBlendMode]]; + [filter setDefaults]; + [filter setValue:in1 forKey:kCIInputImageKey]; + [filter setValue:in2 forKey:kCIInputBackgroundImageKey]; + CIImage* output = [filter valueForKey:kCIOutputImageKey]; + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.h new file mode 100644 index 00000000..84e44bf4 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.h @@ -0,0 +1,20 @@ +// +// IJSVGFilterEffectColorMatrix.h +// IJSVG +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGColorMatrixType) { + IJSVGColorMatrixTypeMatrix, + IJSVGColorMatrixTypeSaturate, + IJSVGColorMatrixTypeHueRotate, + IJSVGColorMatrixTypeLuminanceToAlpha, +}; + +@interface IJSVGFilterEffectColorMatrix : IJSVGFilterEffect + +@property (nonatomic, assign) IJSVGColorMatrixType matrixType; +@property (nonatomic, strong) NSArray* matrixValues; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.m new file mode 100644 index 00000000..a19201da --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectColorMatrix.m @@ -0,0 +1,125 @@ +// +// IJSVGFilterEffectColorMatrix.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectColorMatrix + +- (instancetype)init +{ + if((self = [super init]) != nil) { + _matrixType = IJSVGColorMatrixTypeMatrix; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + NSString* typeStr = attributes[@"type"]; + if(typeStr != nil) { + NSString* lower = typeStr.lowercaseString; + if([lower isEqualToString:@"matrix"]) _matrixType = IJSVGColorMatrixTypeMatrix; + else if([lower isEqualToString:@"saturate"]) _matrixType = IJSVGColorMatrixTypeSaturate; + else if([lower isEqualToString:@"huerotate"]) _matrixType = IJSVGColorMatrixTypeHueRotate; + else if([lower isEqualToString:@"luminancetoalpha"]) _matrixType = IJSVGColorMatrixTypeLuminanceToAlpha; + } + NSString* valuesStr = attributes[@"values"]; + if(valuesStr != nil) { + NSMutableArray* values = [NSMutableArray array]; + NSArray* components = [valuesStr componentsSeparatedByCharactersInSet: + [NSCharacterSet characterSetWithCharactersInString:@" ,\t\n\r"]]; + for(NSString* comp in components) { + NSString* trimmed = [comp stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet]; + if(trimmed.length > 0) [values addObject:@(trimmed.doubleValue)]; + } + _matrixValues = values; + } +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* input = [graph imageForInput:self.inputName]; + CIImage* output = nil; + switch(_matrixType) { + case IJSVGColorMatrixTypeMatrix: output = [self applyMatrix:input]; break; + case IJSVGColorMatrixTypeSaturate: output = [self applySaturate:input]; break; + case IJSVGColorMatrixTypeHueRotate: output = [self applyHueRotate:input]; break; + case IJSVGColorMatrixTypeLuminanceToAlpha: output = [self applyLuminanceToAlpha:input]; break; + } + if(output == nil) output = input; + [graph setImage:output forResult:self.resultName]; + return output; +} + +- (CIImage*)applyColorMatrix:(CIImage*)input + rVector:(CIVector*)rVec gVector:(CIVector*)gVec + bVector:(CIVector*)bVec aVector:(CIVector*)aVec + biasVector:(CIVector*)biasVec +{ + CIFilter* filter = [CIFilter filterWithName:@"CIColorMatrix"]; + [filter setDefaults]; + [filter setValue:input forKey:kCIInputImageKey]; + [filter setValue:rVec forKey:@"inputRVector"]; + [filter setValue:gVec forKey:@"inputGVector"]; + [filter setValue:bVec forKey:@"inputBVector"]; + [filter setValue:aVec forKey:@"inputAVector"]; + [filter setValue:biasVec forKey:@"inputBiasVector"]; + return [filter valueForKey:kCIOutputImageKey]; +} + +- (CIImage*)applyMatrix:(CIImage*)input +{ + if(_matrixValues.count < 20) return input; + CGFloat* m = (CGFloat*)malloc(20 * sizeof(CGFloat)); + for(int i = 0; i < 20; i++) m[i] = _matrixValues[i].doubleValue; + // CIColorMatrix: each vector is a ROW of the SVG matrix + CIVector* rVec = [CIVector vectorWithX:m[0] Y:m[1] Z:m[2] W:m[3]]; + CIVector* gVec = [CIVector vectorWithX:m[5] Y:m[6] Z:m[7] W:m[8]]; + CIVector* bVec = [CIVector vectorWithX:m[10] Y:m[11] Z:m[12] W:m[13]]; + CIVector* aVec = [CIVector vectorWithX:m[15] Y:m[16] Z:m[17] W:m[18]]; + CIVector* biasVec = [CIVector vectorWithX:m[4] Y:m[9] Z:m[14] W:m[19]]; + free(m); + return [self applyColorMatrix:input rVector:rVec gVector:gVec bVector:bVec aVector:aVec biasVector:biasVec]; +} + +- (CIImage*)applySaturate:(CIImage*)input +{ + CGFloat s = (_matrixValues.count > 0) ? _matrixValues[0].doubleValue : 1.0; + CIVector* rVec = [CIVector vectorWithX:0.213+0.787*s Y:0.715-0.715*s Z:0.072-0.072*s W:0]; + CIVector* gVec = [CIVector vectorWithX:0.213-0.213*s Y:0.715+0.285*s Z:0.072-0.072*s W:0]; + CIVector* bVec = [CIVector vectorWithX:0.213-0.213*s Y:0.715-0.715*s Z:0.072+0.928*s W:0]; + CIVector* aVec = [CIVector vectorWithX:0 Y:0 Z:0 W:1]; + CIVector* biasVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + return [self applyColorMatrix:input rVector:rVec gVector:gVec bVector:bVec aVector:aVec biasVector:biasVec]; +} + +- (CIImage*)applyHueRotate:(CIImage*)input +{ + CGFloat angle = (_matrixValues.count > 0) ? _matrixValues[0].doubleValue : 0; + CGFloat rad = angle * M_PI / 180.0; + CGFloat c = cos(rad), s = sin(rad); + CGFloat a00 = 0.213 + 0.787*c - 0.213*s, a01 = 0.715 - 0.715*c - 0.715*s, a02 = 0.072 - 0.072*c + 0.928*s; + CGFloat a10 = 0.213 - 0.213*c + 0.143*s, a11 = 0.715 + 0.285*c + 0.140*s, a12 = 0.072 - 0.072*c - 0.283*s; + CGFloat a20 = 0.213 - 0.213*c - 0.787*s, a21 = 0.715 - 0.715*c + 0.715*s, a22 = 0.072 + 0.928*c + 0.072*s; + CIVector* rVec = [CIVector vectorWithX:a00 Y:a01 Z:a02 W:0]; + CIVector* gVec = [CIVector vectorWithX:a10 Y:a11 Z:a12 W:0]; + CIVector* bVec = [CIVector vectorWithX:a20 Y:a21 Z:a22 W:0]; + CIVector* aVec = [CIVector vectorWithX:0 Y:0 Z:0 W:1]; + CIVector* biasVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + return [self applyColorMatrix:input rVector:rVec gVector:gVec bVector:bVec aVector:aVec biasVector:biasVec]; +} + +- (CIImage*)applyLuminanceToAlpha:(CIImage*)input +{ + CIVector* rVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + CIVector* gVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + CIVector* bVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + CIVector* aVec = [CIVector vectorWithX:0.2126 Y:0.7152 Z:0.0722 W:0]; + CIVector* biasVec = [CIVector vectorWithX:0 Y:0 Z:0 W:0]; + return [self applyColorMatrix:input rVector:rVec gVector:gVec bVector:bVec aVector:aVec biasVector:biasVec]; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.h new file mode 100644 index 00000000..f2c655f6 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.h @@ -0,0 +1,26 @@ +// +// IJSVGFilterEffectComposite.h +// IJSVG +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGFilterCompositeOperator) { + IJSVGFilterCompositeOperatorOver, + IJSVGFilterCompositeOperatorIn, + IJSVGFilterCompositeOperatorOut, + IJSVGFilterCompositeOperatorAtop, + IJSVGFilterCompositeOperatorXor, + IJSVGFilterCompositeOperatorLighter, + IJSVGFilterCompositeOperatorArithmetic, +}; + +@interface IJSVGFilterEffectComposite : IJSVGFilterEffect + +@property (nonatomic, assign) IJSVGFilterCompositeOperator compositeOperator; +@property (nonatomic, assign) CGFloat k1; +@property (nonatomic, assign) CGFloat k2; +@property (nonatomic, assign) CGFloat k3; +@property (nonatomic, assign) CGFloat k4; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.m new file mode 100644 index 00000000..63323578 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectComposite.m @@ -0,0 +1,130 @@ +// +// IJSVGFilterEffectComposite.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectComposite + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + NSString* op = attributes[@"operator"]; + if(op != nil) { + NSString* lower = op.lowercaseString; + if([lower isEqualToString:@"over"]) _compositeOperator = IJSVGFilterCompositeOperatorOver; + else if([lower isEqualToString:@"in"]) _compositeOperator = IJSVGFilterCompositeOperatorIn; + else if([lower isEqualToString:@"out"]) _compositeOperator = IJSVGFilterCompositeOperatorOut; + else if([lower isEqualToString:@"atop"]) _compositeOperator = IJSVGFilterCompositeOperatorAtop; + else if([lower isEqualToString:@"xor"]) _compositeOperator = IJSVGFilterCompositeOperatorXor; + else if([lower isEqualToString:@"lighter"]) _compositeOperator = IJSVGFilterCompositeOperatorLighter; + else if([lower isEqualToString:@"arithmetic"]) _compositeOperator = IJSVGFilterCompositeOperatorArithmetic; + } + if(attributes[@"k1"]) _k1 = [attributes[@"k1"] doubleValue]; + if(attributes[@"k2"]) _k2 = [attributes[@"k2"] doubleValue]; + if(attributes[@"k3"]) _k3 = [attributes[@"k3"] doubleValue]; + if(attributes[@"k4"]) _k4 = [attributes[@"k4"] doubleValue]; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* in1 = [graph imageForInput:self.inputName]; + CIImage* in2 = [graph imageForInput:self.inputName2]; + CIImage* output = nil; + + switch(_compositeOperator) { + case IJSVGFilterCompositeOperatorOver: + output = [self compositeImage:in1 over:in2 filterName:@"CISourceOverCompositing"]; + break; + case IJSVGFilterCompositeOperatorIn: + output = [self compositeImage:in1 over:in2 filterName:@"CISourceInCompositing"]; + break; + case IJSVGFilterCompositeOperatorOut: + output = [self compositeImage:in1 over:in2 filterName:@"CISourceOutCompositing"]; + break; + case IJSVGFilterCompositeOperatorAtop: + output = [self compositeImage:in1 over:in2 filterName:@"CISourceAtopCompositing"]; + break; + case IJSVGFilterCompositeOperatorXor: { + CIImage* aOutB = [self compositeImage:in1 over:in2 filterName:@"CISourceOutCompositing"]; + CIImage* bOutA = [self compositeImage:in2 over:in1 filterName:@"CISourceOutCompositing"]; + output = [self compositeImage:aOutB over:bOutA filterName:@"CIAdditionCompositing"]; + break; + } + case IJSVGFilterCompositeOperatorLighter: + output = [self compositeImage:in1 over:in2 filterName:@"CIAdditionCompositing"]; + break; + case IJSVGFilterCompositeOperatorArithmetic: + output = [self arithmeticComposite:in1 with:in2]; + break; + } + if(output == nil) output = in1; + [graph setImage:output forResult:self.resultName]; + return output; +} + +- (CIImage*)arithmeticComposite:(CIImage*)in1 with:(CIImage*)in2 +{ + // SVG arithmetic: result = k1*in1*in2 + k2*in1 + k3*in2 + k4 + // Use CIColorMatrix on each input to scale, then combine. + // For the general case, we render to bitmaps and compute per-pixel. + + CGRect extent = CGRectUnion(in1.extent, in2.extent); + if (CGRectIsInfinite(extent) || CGRectIsEmpty(extent)) return in1; + + NSInteger w = (NSInteger)CGRectGetWidth(extent); + NSInteger h = (NSInteger)CGRectGetHeight(extent); + if (w <= 0 || h <= 0) return in1; + + NSInteger bytesPerRow = w * 4; + uint8_t* pix1 = (uint8_t*)calloc(bytesPerRow * h, 1); + uint8_t* pix2 = (uint8_t*)calloc(bytesPerRow * h, 1); + uint8_t* outPix = (uint8_t*)calloc(bytesPerRow * h, 1); + + CIContext* ctx = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer: @NO}]; + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB); + [ctx render:in1 toBitmap:pix1 rowBytes:bytesPerRow bounds:extent + format:kCIFormatRGBA8 colorSpace:cs]; + [ctx render:in2 toBitmap:pix2 rowBytes:bytesPerRow bounds:extent + format:kCIFormatRGBA8 colorSpace:cs]; + + CGFloat k1 = _k1, k2 = _k2, k3 = _k3, k4 = _k4; + NSInteger numPixels = w * h; + for (NSInteger p = 0; p < numPixels; p++) { + NSInteger i = p * 4; + for (NSInteger c = 0; c < 4; c++) { + CGFloat a = pix1[i + c] / 255.0; + CGFloat b = pix2[i + c] / 255.0; + CGFloat result = k1 * a * b + k2 * a + k3 * b + k4; + result = fmax(0.0, fmin(1.0, result)); + outPix[i + c] = (uint8_t)(result * 255.0 + 0.5); + } + } + + free(pix1); + free(pix2); + + NSData* data = [NSData dataWithBytesNoCopy:outPix length:bytesPerRow * h freeWhenDone:YES]; + CIImage* output = [CIImage imageWithBitmapData:data bytesPerRow:bytesPerRow + size:CGSizeMake(w, h) + format:kCIFormatRGBA8 colorSpace:cs]; + CGColorSpaceRelease(cs); + + if (extent.origin.x != 0 || extent.origin.y != 0) { + output = [output imageByApplyingTransform: + CGAffineTransformMakeTranslation(extent.origin.x, extent.origin.y)]; + } + return output; +} + +- (CIImage*)compositeImage:(CIImage*)foreground over:(CIImage*)background filterName:(NSString*)filterName +{ + CIFilter* filter = [CIFilter filterWithName:filterName]; + [filter setDefaults]; + [filter setValue:foreground forKey:kCIInputImageKey]; + [filter setValue:background forKey:kCIInputBackgroundImageKey]; + return [filter valueForKey:kCIOutputImageKey]; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.h new file mode 100644 index 00000000..ccf31773 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.h @@ -0,0 +1,21 @@ +// +// IJSVGFilterEffectDisplacementMap.h +// IJSVG +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGChannelSelector) { + IJSVGChannelSelectorR = 0, + IJSVGChannelSelectorG = 1, + IJSVGChannelSelectorB = 2, + IJSVGChannelSelectorA = 3, +}; + +@interface IJSVGFilterEffectDisplacementMap : IJSVGFilterEffect + +@property (nonatomic, assign) CGFloat scale; +@property (nonatomic, assign) IJSVGChannelSelector xChannelSelector; +@property (nonatomic, assign) IJSVGChannelSelector yChannelSelector; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.m new file mode 100644 index 00000000..d6783d64 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectDisplacementMap.m @@ -0,0 +1,230 @@ +// +// IJSVGFilterEffectDisplacementMap.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectDisplacementMap + +- (instancetype)init +{ + self = [super init]; + if (self) { + _scale = 0.0; + _xChannelSelector = IJSVGChannelSelectorA; + _yChannelSelector = IJSVGChannelSelectorA; + } + return self; +} + ++ (IJSVGChannelSelector)channelSelectorForString:(NSString*)string +{ + if (string.length == 0) return IJSVGChannelSelectorA; + NSString* lower = string.lowercaseString; + if ([lower isEqualToString:@"r"]) return IJSVGChannelSelectorR; + if ([lower isEqualToString:@"g"]) return IJSVGChannelSelectorG; + if ([lower isEqualToString:@"b"]) return IJSVGChannelSelectorB; + if ([lower isEqualToString:@"a"]) return IJSVGChannelSelectorA; + return IJSVGChannelSelectorA; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + [super parseEffectAttributes:attributes]; + + NSString* scaleStr = attributes[@"scale"]; + if (scaleStr.length > 0) { + _scale = scaleStr.doubleValue; + } + + NSString* xChan = attributes[@"xChannelSelector"]; + if (xChan.length > 0) { + _xChannelSelector = [IJSVGFilterEffectDisplacementMap channelSelectorForString:xChan]; + } + + NSString* yChan = attributes[@"yChannelSelector"]; + if (yChan.length > 0) { + _yChannelSelector = [IJSVGFilterEffectDisplacementMap channelSelectorForString:yChan]; + } +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* inputImage = [graph imageForInput:self.inputName]; + CIImage* displacementImage = [graph imageForInput:self.inputName2]; + + if (inputImage == nil || displacementImage == nil || _scale == 0.0) { + CIImage* result = inputImage ?: [CIImage emptyImage]; + [graph setImage:result forResult:self.resultName]; + return result; + } + + CGFloat scaledAmount = _scale * graph.scale; + + // The filter region is the area we operate over. Use the displacement + // map extent (which comes from the turbulence covering the full filter + // region) intersected with an expanded source bounds. + CGRect filterRegion = displacementImage.extent; + if (CGRectIsInfinite(filterRegion)) { + filterRegion = graph.sourceBounds; + } + + CIImage* output = [self applyDisplacementToImage:inputImage + displacementMap:displacementImage + scale:scaledAmount + filterRegion:filterRegion]; + + [graph setImage:output forResult:self.resultName]; + return output; +} + +- (CIImage*)applyDisplacementToImage:(CIImage*)input + displacementMap:(CIImage*)dispMap + scale:(CGFloat)scale + filterRegion:(CGRect)filterRegion +{ + CIContext* ctx = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer: @NO}]; + + // Work over the filter region (not just the input extent). + // This ensures we process all pixels where displacement can move + // source pixels to, including areas outside the source bounds. + NSInteger w = (NSInteger)CGRectGetWidth(filterRegion); + NSInteger h = (NSInteger)CGRectGetHeight(filterRegion); + if (w <= 0 || h <= 0) return input; + + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + + // Render displacement map over the filter region + NSInteger bytesPerRow = w * 4; + uint8_t* dispPixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (dispPixels == NULL) { + CGColorSpaceRelease(cs); + return input; + } + + CIImage* croppedDisp = [dispMap imageByCroppingToRect:filterRegion]; + [ctx render:croppedDisp + toBitmap:dispPixels + rowBytes:bytesPerRow + bounds:filterRegion + format:kCIFormatRGBA8 + colorSpace:cs]; + + // Render the source image over an expanded region (filter region + scale padding) + // so we can sample source pixels that are offset by displacement. + // Do NOT clamp — pixels outside the source graphic must be transparent (RGBA=0). + CGFloat absScale = fabs(scale); + NSInteger padW = (NSInteger)(w + absScale * 2 + 2); + NSInteger padH = (NSInteger)(h + absScale * 2 + 2); + NSInteger padBytesPerRow = padW * 4; + uint8_t* srcPixels = (uint8_t*)calloc(padBytesPerRow * padH, 1); + if (srcPixels == NULL) { + free(dispPixels); + CGColorSpaceRelease(cs); + return input; + } + + CGFloat padOffX = absScale + 1; + CGFloat padOffY = absScale + 1; + CGRect srcRenderBounds = CGRectMake(filterRegion.origin.x - padOffX, + filterRegion.origin.y - padOffY, + padW, padH); + + // Crop the input to its own extent first — this ensures pixels outside + // the source graphic remain zero (transparent), not clamped edge pixels. + CIImage* croppedInput = [input imageByCroppingToRect:input.extent]; + [ctx render:croppedInput + toBitmap:srcPixels + rowBytes:padBytesPerRow + bounds:srcRenderBounds + format:kCIFormatRGBA8 + colorSpace:cs]; + + // Create output bitmap + uint8_t* outPixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (outPixels == NULL) { + free(dispPixels); + free(srcPixels); + CGColorSpaceRelease(cs); + return input; + } + + // Apply displacement per pixel. + // SVG spec: P'(x,y) = P(x + scale*(XC(x,y) - 0.5), y + scale*(YC(x,y) - 0.5)) + for (NSInteger y = 0; y < h; y++) { + for (NSInteger x = 0; x < w; x++) { + NSInteger dispIdx = (y * w + x) * 4; + + CGFloat xChanVal = (CGFloat)dispPixels[dispIdx + _xChannelSelector] / 255.0; + CGFloat yChanVal = (CGFloat)dispPixels[dispIdx + _yChannelSelector] / 255.0; + + CGFloat dx = scale * (xChanVal - 0.5); + CGFloat dy = scale * (yChanVal - 0.5); + + // Map to padded source buffer coordinates + CGFloat srcX = x + padOffX + dx; + CGFloat srcY = y + padOffY + dy; + + // Bilinear interpolation + NSInteger sx0 = (NSInteger)floor(srcX); + NSInteger sy0 = (NSInteger)floor(srcY); + NSInteger sx1 = sx0 + 1; + NSInteger sy1 = sy0 + 1; + CGFloat fx = srcX - sx0; + CGFloat fy = srcY - sy0; + + NSInteger outIdx = (y * w + x) * 4; + + // If completely outside padded buffer, output transparent + if (sx1 < 0 || sx0 >= padW || sy1 < 0 || sy0 >= padH) { + outPixels[outIdx + 0] = 0; + outPixels[outIdx + 1] = 0; + outPixels[outIdx + 2] = 0; + outPixels[outIdx + 3] = 0; + continue; + } + + // Clamp to padded buffer (edges are transparent from calloc) + sx0 = MAX(0, MIN(sx0, padW - 1)); + sy0 = MAX(0, MIN(sy0, padH - 1)); + sx1 = MAX(0, MIN(sx1, padW - 1)); + sy1 = MAX(0, MIN(sy1, padH - 1)); + + for (int c = 0; c < 4; c++) { + CGFloat v00 = srcPixels[(sy0 * padW + sx0) * 4 + c]; + CGFloat v10 = srcPixels[(sy0 * padW + sx1) * 4 + c]; + CGFloat v01 = srcPixels[(sy1 * padW + sx0) * 4 + c]; + CGFloat v11 = srcPixels[(sy1 * padW + sx1) * 4 + c]; + CGFloat top = v00 + fx * (v10 - v00); + CGFloat bot = v01 + fx * (v11 - v01); + CGFloat val = top + fy * (bot - top); + outPixels[outIdx + c] = (uint8_t)MAX(0, MIN(255, (int)round(val))); + } + } + } + + NSData* outData = [NSData dataWithBytesNoCopy:outPixels + length:bytesPerRow * h + freeWhenDone:YES]; + CIImage* result = [CIImage imageWithBitmapData:outData + bytesPerRow:bytesPerRow + size:CGSizeMake(w, h) + format:kCIFormatRGBA8 + colorSpace:cs]; + + // Offset to match filter region origin + if (filterRegion.origin.x != 0 || filterRegion.origin.y != 0) { + result = [result imageByApplyingTransform: + CGAffineTransformMakeTranslation(filterRegion.origin.x, filterRegion.origin.y)]; + } + + free(dispPixels); + free(srcPixels); + CGColorSpaceRelease(cs); + + return result; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.h new file mode 100644 index 00000000..2f7c2cca --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.h @@ -0,0 +1,13 @@ +// +// IJSVGFilterEffectFlood.h +// IJSVG +// + +#import + +@interface IJSVGFilterEffectFlood : IJSVGFilterEffect + +@property (nonatomic, strong) NSColor* floodColor; +@property (nonatomic, assign) CGFloat floodOpacity; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.m new file mode 100644 index 00000000..608cc025 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectFlood.m @@ -0,0 +1,53 @@ +// +// IJSVGFilterEffectFlood.m +// IJSVG +// + +#import +#import +#import + +@implementation IJSVGFilterEffectFlood + +- (instancetype)init +{ + if((self = [super init]) != nil) { + _floodColor = NSColor.blackColor; + _floodOpacity = 1.0; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + NSString* colorStr = attributes[@"flood-color"]; + if(colorStr != nil) { + _floodColor = [IJSVGColor colorFromString:colorStr]; + if(_floodColor == nil) { + _floodColor = NSColor.blackColor; + } + } + NSString* opacityStr = attributes[@"flood-opacity"]; + if(opacityStr != nil) { + _floodOpacity = opacityStr.doubleValue; + } +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CGFloat r = 0.f; + CGFloat g = 0.f; + CGFloat b = 0.f; + CGFloat a = 0.f; + NSColor* color = [IJSVGColor computeColorSpace:_floodColor]; + IJSVGColorGetRGBAComponents(color, &r, &g, &b, &a); + a *= _floodOpacity; + + CIColor* ciColor = [CIColor colorWithRed:r green:g blue:b alpha:a]; + CIImage* flood = [CIImage imageWithColor:ciColor]; + CIImage* output = [flood imageByCroppingToRect:graph.sourceBounds]; + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.h index dcb405f9..158ff1a5 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.h @@ -10,4 +10,6 @@ @interface IJSVGFilterEffectGaussianBlur : IJSVGFilterEffect +@property (nonatomic, assign) BOOL usesSRGBColorInterpolation; + @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.m index 5c280732..760bc268 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectGaussianBlur.m @@ -7,26 +7,56 @@ // #import +#import #import +#import @implementation IJSVGFilterEffectGaussianBlur -+ (CIFilter*)sharedFilter +- (void)parseEffectAttributes:(NSDictionary *)attributes { - IJSVGThreadManager* manager = IJSVGThreadManager.currentManager; - NSString* key = @"CIFilterGaussianBlur"; - CIFilter* filter = nil; - if((filter = [manager userInfoObjectForKey:key]) == nil) { - filter = [CIFilter filterWithName:@"CIGaussianBlur"]; - [manager setUserInfoObject:filter - forKey:key]; + [super parseEffectAttributes:attributes]; + + NSString *colorInterpolation = attributes[@"color-interpolation-filters"]; + if(colorInterpolation.length == 0) { + return; + } + + _usesSRGBColorInterpolation = [colorInterpolation caseInsensitiveCompare:@"sRGB"] == NSOrderedSame; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* input = [graph imageForInput:self.inputName]; + CGRect inputExtent = input.extent; + CGFloat scaledSigma = self.stdDeviation.value * graph.scale; + + if(scaledSigma < 0.5f) { + [graph setImage:input forResult:self.resultName]; + return input; } - return filter; + CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [filter setDefaults]; + [filter setValue:[input imageByClampingToExtent] forKey:kCIInputImageKey]; + [filter setValue:@(scaledSigma) forKey:kCIInputRadiusKey]; + CIImage *output = [filter valueForKey:kCIOutputImageKey]; + + CGFloat expand = scaledSigma * 3.0; + CGRect blurRegion = CGRectInset(inputExtent, -expand, -expand); + output = [output imageByCroppingToRect:blurRegion]; + [graph setImage:output forResult:self.resultName]; + return output; } - (CIImage*)processImage:(CIImage*)image { - CIFilter* filter = [self.class sharedFilter]; + IJSVGThreadManager* manager = IJSVGThreadManager.currentManager; + NSString* key = @"CIFilterGaussianBlur"; + CIFilter* filter = [manager userInfoObjectForKey:key]; + if(filter == nil) { + filter = [CIFilter filterWithName:@"CIGaussianBlur"]; + [manager setUserInfoObject:filter forKey:key]; + } [filter setDefaults]; [filter setValue:image forKey:kCIInputImageKey]; [filter setValue:@(self.stdDeviation.value) forKey:kCIInputRadiusKey]; diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.h new file mode 100644 index 00000000..47c82381 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.h @@ -0,0 +1,72 @@ +// +// IJSVGFilterEffectLighting.h +// IJSVG +// +// Shared base for feSpecularLighting and feDiffuseLighting. +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGLightType) { + IJSVGLightTypeDistant, + IJSVGLightTypePoint, + IJSVGLightTypeSpot, +}; + +@interface IJSVGFilterEffectLighting : IJSVGFilterEffect + +@property (nonatomic, assign) CGFloat surfaceScale; +@property (nonatomic, assign) CGFloat lightColorR; +@property (nonatomic, assign) CGFloat lightColorG; +@property (nonatomic, assign) CGFloat lightColorB; + +// Light source +@property (nonatomic, assign) IJSVGLightType lightType; + +// Point / spot light +@property (nonatomic, assign) CGFloat lightX; +@property (nonatomic, assign) CGFloat lightY; +@property (nonatomic, assign) CGFloat lightZ; + +// Spot light target +@property (nonatomic, assign) CGFloat pointsAtX; +@property (nonatomic, assign) CGFloat pointsAtY; +@property (nonatomic, assign) CGFloat pointsAtZ; +@property (nonatomic, assign) CGFloat spotExponent; +@property (nonatomic, assign) CGFloat limitingConeAngle; + +// Distant light +@property (nonatomic, assign) CGFloat azimuth; +@property (nonatomic, assign) CGFloat elevation; + +// Helpers for subclasses +- (uint8_t*)renderInputToBitmapWithGraph:(id)graph + width:(NSInteger*)outW + height:(NSInteger*)outH; +- (void)computeNormalAtX:(NSInteger)x y:(NSInteger)y + pixels:(const uint8_t*)pixels + width:(NSInteger)w height:(NSInteger)h + scale:(CGFloat)scale + nx:(CGFloat*)nx ny:(CGFloat*)ny nz:(CGFloat*)nz; +- (void)lightDirectionAtSvgX:(CGFloat)sx svgY:(CGFloat)sy + surfaceZ:(CGFloat)sz + lx:(CGFloat*)lx ly:(CGFloat*)ly lz:(CGFloat*)lz; +- (void)parseLightSourceElement:(NSString*)elementName + attributes:(NSDictionary*)attributes; + +@end + + +@interface IJSVGFilterEffectSpecularLighting : IJSVGFilterEffectLighting + +@property (nonatomic, assign) CGFloat specularConstant; +@property (nonatomic, assign) CGFloat specularExponent; + +@end + + +@interface IJSVGFilterEffectDiffuseLighting : IJSVGFilterEffectLighting + +@property (nonatomic, assign) CGFloat diffuseConstant; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.m new file mode 100644 index 00000000..ab4eb4e7 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectLighting.m @@ -0,0 +1,555 @@ +// +// IJSVGFilterEffectLighting.m +// IJSVG +// + +#import +#import + +// --------------------------------------------------------------------------- +// SVG feSpecularLighting / feDiffuseLighting implementation. +// +// Reference: https://www.w3.org/TR/SVG11/filters.html#feDiffuseLightingElement +// https://www.w3.org/TR/SVG11/filters.html#feSpecularLightingElement +// +// Both filters use the alpha channel of the input as a height map, compute +// surface normals via the Sobel operator, then apply Phong lighting. +// --------------------------------------------------------------------------- + +// sRGB → linear conversion per IEC 61966-2-1 +static CGFloat _sRGBToLinear(CGFloat s) +{ + if (s <= 0.04045) return s / 12.92; + return pow((s + 0.055) / 1.055, 2.4); +} + +static void _parseColor(NSString* colorStr, CGFloat* r, CGFloat* g, CGFloat* b) +{ + *r = 1.0; *g = 1.0; *b = 1.0; + if (colorStr.length == 0) return; + + NSString* str = [colorStr stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceCharacterSet].lowercaseString; + + if ([str hasPrefix:@"#"]) { + unsigned int hex = 0; + NSScanner* scanner = [NSScanner scannerWithString:[str substringFromIndex:1]]; + [scanner scanHexInt:&hex]; + if (str.length == 4) { // #RGB + *r = ((hex >> 8) & 0xF) / 15.0; + *g = ((hex >> 4) & 0xF) / 15.0; + *b = (hex & 0xF) / 15.0; + } else { // #RRGGBB + *r = ((hex >> 16) & 0xFF) / 255.0; + *g = ((hex >> 8) & 0xFF) / 255.0; + *b = (hex & 0xFF) / 255.0; + } + } else if ([str isEqualToString:@"white"]) { + *r = 1; *g = 1; *b = 1; + } else if ([str isEqualToString:@"black"]) { + *r = 0; *g = 0; *b = 0; + } + // Add more named colors as needed + + // SVG color-interpolation-filters defaults to linearRGB. + // Convert parsed sRGB values to linear for filter computations. + *r = _sRGBToLinear(*r); + *g = _sRGBToLinear(*g); + *b = _sRGBToLinear(*b); +} + +@implementation IJSVGFilterEffectLighting + +- (instancetype)init +{ + self = [super init]; + if (self) { + _surfaceScale = 1.0; + _lightColorR = 1.0; + _lightColorG = 1.0; + _lightColorB = 1.0; + _lightType = IJSVGLightTypeDistant; + _azimuth = 0.0; + _elevation = 0.0; + _spotExponent = 1.0; + _limitingConeAngle = 180.0; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + [super parseEffectAttributes:attributes]; + + NSString* ss = attributes[@"surfaceScale"]; + if (ss.length > 0) _surfaceScale = ss.doubleValue; + + NSString* lc = attributes[@"lighting-color"]; + if (lc.length > 0) { + _parseColor(lc, &_lightColorR, &_lightColorG, &_lightColorB); + } +} + +- (void)parseLightSourceFromChildren:(NSArray*)children attributes:(NSDictionary*)attrs +{ + // Light source elements are parsed as child nodes by the IJSVG parser. + // However, since we receive attributes as a flat dict, the light source + // attributes may be passed via a secondary mechanism. For now, we handle + // them via a post-parse step where the parser provides nested element data. + // + // This is handled by parseChildElement: below. +} + +- (void)parseLightSourceElement:(NSString*)elementName + attributes:(NSDictionary*)attributes +{ + NSString* name = elementName.lowercaseString; + + if ([name isEqualToString:@"fepointlight"]) { + _lightType = IJSVGLightTypePoint; + NSString* x = attributes[@"x"]; + NSString* y = attributes[@"y"]; + NSString* z = attributes[@"z"]; + if (x.length > 0) _lightX = x.doubleValue; + if (y.length > 0) _lightY = y.doubleValue; + if (z.length > 0) _lightZ = z.doubleValue; + } else if ([name isEqualToString:@"fedistantlight"]) { + _lightType = IJSVGLightTypeDistant; + NSString* az = attributes[@"azimuth"]; + NSString* el = attributes[@"elevation"]; + if (az.length > 0) _azimuth = az.doubleValue; + if (el.length > 0) _elevation = el.doubleValue; + } else if ([name isEqualToString:@"fespotlight"]) { + _lightType = IJSVGLightTypeSpot; + NSString* x = attributes[@"x"]; + NSString* y = attributes[@"y"]; + NSString* z = attributes[@"z"]; + if (x.length > 0) _lightX = x.doubleValue; + if (y.length > 0) _lightY = y.doubleValue; + if (z.length > 0) _lightZ = z.doubleValue; + NSString* px = attributes[@"pointsAtX"]; + NSString* py = attributes[@"pointsAtY"]; + NSString* pz = attributes[@"pointsAtZ"]; + if (px.length > 0) _pointsAtX = px.doubleValue; + if (py.length > 0) _pointsAtY = py.doubleValue; + if (pz.length > 0) _pointsAtZ = pz.doubleValue; + NSString* se = attributes[@"specularExponent"]; + if (se.length > 0) _spotExponent = se.doubleValue; + NSString* lca = attributes[@"limitingConeAngle"]; + if (lca.length > 0) _limitingConeAngle = lca.doubleValue; + } +} + +- (uint8_t*)renderInputToBitmapWithGraph:(IJSVGFilterGraph*)graph + width:(NSInteger*)outW + height:(NSInteger*)outH +{ + CIImage* input = [graph imageForInput:self.inputName]; + if (input == nil) { + *outW = 0; *outH = 0; + return NULL; + } + + CGRect bounds = input.extent; + if (CGRectIsInfinite(bounds)) bounds = graph.sourceBounds; + + NSInteger w = (NSInteger)CGRectGetWidth(bounds); + NSInteger h = (NSInteger)CGRectGetHeight(bounds); + if (w <= 0 || h <= 0) { + *outW = 0; *outH = 0; + return NULL; + } + + NSInteger bytesPerRow = w * 4; + uint8_t* pixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (pixels == NULL) { + *outW = 0; *outH = 0; + return NULL; + } + + CIContext* ctx = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer: @NO}]; + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB); + [ctx render:input toBitmap:pixels rowBytes:bytesPerRow bounds:bounds + format:kCIFormatRGBA8 colorSpace:cs]; + CGColorSpaceRelease(cs); + + // CIContext renders bottom-up (CG convention). Flip to top-down (SVG convention) + // so that pixel row 0 corresponds to the top of the image. + uint8_t* rowBuf = (uint8_t*)malloc(bytesPerRow); + for (NSInteger top = 0, bot = h - 1; top < bot; top++, bot--) { + memcpy(rowBuf, pixels + top * bytesPerRow, bytesPerRow); + memcpy(pixels + top * bytesPerRow, pixels + bot * bytesPerRow, bytesPerRow); + memcpy(pixels + bot * bytesPerRow, rowBuf, bytesPerRow); + } + free(rowBuf); + + *outW = w; + *outH = h; + return pixels; +} + +// Sobel-based surface normal computation from the alpha channel height map. +// The SVG spec uses a specific kernel for interior, edge, and corner pixels. +- (void)computeNormalAtX:(NSInteger)x y:(NSInteger)y + pixels:(const uint8_t*)pixels + width:(NSInteger)w height:(NSInteger)h + scale:(CGFloat)bitmapScale + nx:(CGFloat*)nx ny:(CGFloat*)ny nz:(CGFloat*)nz +{ + // Helper to get alpha value at (px, py) clamped to bounds + #define ALPHA(px, py) ({ \ + NSInteger cx = MAX(0, MIN((px), w - 1)); \ + NSInteger cy = MAX(0, MIN((py), h - 1)); \ + (CGFloat)pixels[(cy * w + cx) * 4 + 3] / 255.0; \ + }) + + // SVG spec Sobel kernel factors (in filter pixel space). + CGFloat f4 = 1.0 / 4.0; // interior + CGFloat f3 = 1.0 / 3.0; // edge (one direction) + CGFloat f2 = 1.0 / 2.0; // edge (other direction), divided by 3 below + CGFloat fc = 2.0 / 3.0; // corner + + CGFloat dx, dy; + + if (x == 0 && y == 0) { + dx = fc * (ALPHA(1,0) - ALPHA(0,0) + ALPHA(1,1) - ALPHA(0,1)); + dy = fc * (ALPHA(0,1) - ALPHA(0,0) + ALPHA(1,1) - ALPHA(1,0)); + } else if (x == w-1 && y == 0) { + dx = fc * (ALPHA(w-1,0) - ALPHA(w-2,0) + ALPHA(w-1,1) - ALPHA(w-2,1)); + dy = fc * (ALPHA(w-2,1) - ALPHA(w-2,0) + ALPHA(w-1,1) - ALPHA(w-1,0)); + } else if (x == 0 && y == h-1) { + dx = fc * (ALPHA(1,h-2) - ALPHA(0,h-2) + ALPHA(1,h-1) - ALPHA(0,h-1)); + dy = fc * (ALPHA(0,h-1) - ALPHA(0,h-2) + ALPHA(1,h-1) - ALPHA(1,h-2)); + } else if (x == w-1 && y == h-1) { + dx = fc * (ALPHA(w-1,h-2) - ALPHA(w-2,h-2) + ALPHA(w-1,h-1) - ALPHA(w-2,h-1)); + dy = fc * (ALPHA(w-2,h-1) - ALPHA(w-2,h-2) + ALPHA(w-1,h-1) - ALPHA(w-1,h-2)); + } else if (y == 0) { + dx = f3 * (ALPHA(x+1,0) - ALPHA(x-1,0) + ALPHA(x+1,1) - ALPHA(x-1,1)); + dy = f2 * (ALPHA(x-1,1) - ALPHA(x-1,0) + 2*(ALPHA(x,1) - ALPHA(x,0)) + ALPHA(x+1,1) - ALPHA(x+1,0)) / 3.0; + } else if (y == h-1) { + dx = f3 * (ALPHA(x+1,h-2) - ALPHA(x-1,h-2) + ALPHA(x+1,h-1) - ALPHA(x-1,h-1)); + dy = f2 * (ALPHA(x-1,h-1) - ALPHA(x-1,h-2) + 2*(ALPHA(x,h-1) - ALPHA(x,h-2)) + ALPHA(x+1,h-1) - ALPHA(x+1,h-2)) / 3.0; + } else if (x == 0) { + dx = f2 * (ALPHA(1,y-1) - ALPHA(0,y-1) + 2*(ALPHA(1,y) - ALPHA(0,y)) + ALPHA(1,y+1) - ALPHA(0,y+1)) / 3.0; + dy = f3 * (ALPHA(0,y+1) - ALPHA(0,y-1) + ALPHA(1,y+1) - ALPHA(1,y-1)); + } else if (x == w-1) { + dx = f2 * (ALPHA(w-1,y-1) - ALPHA(w-2,y-1) + 2*(ALPHA(w-1,y) - ALPHA(w-2,y)) + ALPHA(w-1,y+1) - ALPHA(w-2,y+1)) / 3.0; + dy = f3 * (ALPHA(w-2,y+1) - ALPHA(w-2,y-1) + ALPHA(w-1,y+1) - ALPHA(w-1,y-1)); + } else { + // Interior pixel — standard Sobel + dx = f4 * ( + -ALPHA(x-1,y-1) + ALPHA(x+1,y-1) + -2*ALPHA(x-1,y) + 2*ALPHA(x+1,y) + -ALPHA(x-1,y+1) + ALPHA(x+1,y+1) + ); + dy = f4 * ( + -ALPHA(x-1,y-1) - 2*ALPHA(x,y-1) - ALPHA(x+1,y-1) + +ALPHA(x-1,y+1) + 2*ALPHA(x,y+1) + ALPHA(x+1,y+1) + ); + } + + #undef ALPHA + + // Normal = (-surfaceScale * dx, -surfaceScale * dy, 1) + CGFloat ssx = -_surfaceScale * dx; + CGFloat ssy = -_surfaceScale * dy; + CGFloat len = sqrt(ssx * ssx + ssy * ssy + 1.0); + *nx = ssx / len; + *ny = ssy / len; + *nz = 1.0 / len; +} + +- (void)lightDirectionAtSvgX:(CGFloat)sx svgY:(CGFloat)sy + surfaceZ:(CGFloat)sz + lx:(CGFloat*)lx ly:(CGFloat*)ly lz:(CGFloat*)lz +{ + switch (_lightType) { + case IJSVGLightTypeDistant: { + CGFloat azRad = _azimuth * M_PI / 180.0; + CGFloat elRad = _elevation * M_PI / 180.0; + *lx = cos(azRad) * cos(elRad); + *ly = sin(azRad) * cos(elRad); + *lz = sin(elRad); + break; + } + case IJSVGLightTypePoint: + case IJSVGLightTypeSpot: { + // Both sx,sy and lightX,lightY are in SVG user-space coordinates + CGFloat dx = _lightX - sx; + CGFloat dy = _lightY - sy; + CGFloat dz = _lightZ - sz; + CGFloat len = sqrt(dx*dx + dy*dy + dz*dz); + if (len > 0.0) { + *lx = dx / len; + *ly = dy / len; + *lz = dz / len; + } else { + *lx = 0; *ly = 0; *lz = 1; + } + break; + } + } +} + +@end + +// --------------------------------------------------------------------------- +#pragma mark - feSpecularLighting +// --------------------------------------------------------------------------- + +@implementation IJSVGFilterEffectSpecularLighting + +- (instancetype)init +{ + self = [super init]; + if (self) { + _specularConstant = 1.0; + _specularExponent = 1.0; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + [super parseEffectAttributes:attributes]; + + NSString* ks = attributes[@"specularConstant"]; + if (ks.length > 0) _specularConstant = ks.doubleValue; + + NSString* se = attributes[@"specularExponent"]; + if (se.length > 0) _specularExponent = se.doubleValue; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + NSInteger w = 0, h = 0; + uint8_t* inputPixels = [self renderInputToBitmapWithGraph:graph width:&w height:&h]; + if (inputPixels == NULL || w <= 0 || h <= 0) { + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + if (inputPixels) free(inputPixels); + return empty; + } + + CIImage* input = [graph imageForInput:self.inputName]; + CGRect bounds = input.extent; + if (CGRectIsInfinite(bounds)) bounds = graph.sourceBounds; + + NSInteger bytesPerRow = w * 4; + uint8_t* outPixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (outPixels == NULL) { + free(inputPixels); + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + return empty; + } + + // Eye vector (SVG spec: always (0, 0, 1) for infinite viewpoint) + CGFloat ex = 0, ey = 0, ez = 1; + + // Convert bitmap pixel positions to SVG user-space coordinates. + // The input CIImage extent may differ from sourceBounds (e.g., blur extends it). + // CG pixel for flipped bitmap (x, y): (bounds.origin.x + x, bounds.origin.y + h - y) + // SVG mapping uses the source graphic's CG space as reference: + // svgX = svgOrigin.x + (bounds.origin.x + x) / scale + // svgY = svgOrigin.y + (sourceBmpH - bounds.origin.y - bounds.size.height + y) / scale + CGFloat scale = graph.scale; + CGPoint svgOrigin = graph.elementSVGOrigin; + CGFloat sourceBmpH = graph.sourceBounds.size.height; + CGFloat bmpOffX = bounds.origin.x; + CGFloat bmpOffY = sourceBmpH - bounds.origin.y - bounds.size.height; + + for (NSInteger y = 0; y < h; y++) { + for (NSInteger x = 0; x < w; x++) { + CGFloat normalX, normalY, normalZ; + [self computeNormalAtX:x y:y pixels:inputPixels width:w height:h + scale:scale nx:&normalX ny:&normalY nz:&normalZ]; + + CGFloat alpha = (CGFloat)inputPixels[(y * w + x) * 4 + 3] / 255.0; + CGFloat surfZ = self.surfaceScale * alpha; + + CGFloat svgX = svgOrigin.x + (bmpOffX + (CGFloat)x) / scale; + CGFloat svgY = svgOrigin.y + (bmpOffY + (CGFloat)y) / scale; + + CGFloat lx, ly, lz; + [self lightDirectionAtSvgX:svgX svgY:svgY + surfaceZ:surfZ lx:&lx ly:&ly lz:&lz]; + + // Half vector H = normalize(L + E) + CGFloat hx = lx + ex; + CGFloat hy = ly + ey; + CGFloat hz = lz + ez; + CGFloat hLen = sqrt(hx*hx + hy*hy + hz*hz); + if (hLen > 0) { hx /= hLen; hy /= hLen; hz /= hLen; } + + // N dot H + CGFloat nDotH = normalX * hx + normalY * hy + normalZ * hz; + nDotH = fmax(0.0, nDotH); + + CGFloat spec = _specularConstant * pow(nDotH, _specularExponent); + spec = fmax(0.0, fmin(1.0, spec)); + NSInteger idx = (y * w + x) * 4; + // SVG spec: output color = ks * (N dot H)^se * lightColor + // Output alpha = max(R, G, B) + CGFloat r = fmin(1.0, spec * self.lightColorR); + CGFloat g = fmin(1.0, spec * self.lightColorG); + CGFloat b = fmin(1.0, spec * self.lightColorB); + CGFloat a = fmax(r, fmax(g, b)); + + outPixels[idx + 0] = (uint8_t)(r * 255.0 + 0.5); + outPixels[idx + 1] = (uint8_t)(g * 255.0 + 0.5); + outPixels[idx + 2] = (uint8_t)(b * 255.0 + 0.5); + outPixels[idx + 3] = (uint8_t)(a * 255.0 + 0.5); + } + } + + free(inputPixels); + + // Flip output back to CG bottom-up convention for CIImage + { + uint8_t* rowBuf = (uint8_t*)malloc(bytesPerRow); + for (NSInteger top = 0, bot = h - 1; top < bot; top++, bot--) { + memcpy(rowBuf, outPixels + top * bytesPerRow, bytesPerRow); + memcpy(outPixels + top * bytesPerRow, outPixels + bot * bytesPerRow, bytesPerRow); + memcpy(outPixels + bot * bytesPerRow, rowBuf, bytesPerRow); + } + free(rowBuf); + } + + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB); + NSData* data = [NSData dataWithBytesNoCopy:outPixels length:bytesPerRow * h freeWhenDone:YES]; + CIImage* output = [CIImage imageWithBitmapData:data bytesPerRow:bytesPerRow + size:CGSizeMake(w, h) + format:kCIFormatRGBA8 colorSpace:cs]; + CGColorSpaceRelease(cs); + + if (bounds.origin.x != 0 || bounds.origin.y != 0) { + output = [output imageByApplyingTransform: + CGAffineTransformMakeTranslation(bounds.origin.x, bounds.origin.y)]; + } + + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end + +// --------------------------------------------------------------------------- +#pragma mark - feDiffuseLighting +// --------------------------------------------------------------------------- + +@implementation IJSVGFilterEffectDiffuseLighting + +- (instancetype)init +{ + self = [super init]; + if (self) { + _diffuseConstant = 1.0; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + [super parseEffectAttributes:attributes]; + + NSString* kd = attributes[@"diffuseConstant"]; + if (kd.length > 0) _diffuseConstant = kd.doubleValue; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + NSInteger w = 0, h = 0; + uint8_t* inputPixels = [self renderInputToBitmapWithGraph:graph width:&w height:&h]; + if (inputPixels == NULL || w <= 0 || h <= 0) { + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + if (inputPixels) free(inputPixels); + return empty; + } + + CIImage* input = [graph imageForInput:self.inputName]; + CGRect bounds = input.extent; + if (CGRectIsInfinite(bounds)) bounds = graph.sourceBounds; + + NSInteger bytesPerRow = w * 4; + uint8_t* outPixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (outPixels == NULL) { + free(inputPixels); + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + return empty; + } + + CGFloat scale = graph.scale; + CGPoint svgOrigin = graph.elementSVGOrigin; + CGFloat sourceBmpH_d = graph.sourceBounds.size.height; + CGFloat bmpOffX_d = bounds.origin.x; + CGFloat bmpOffY_d = sourceBmpH_d - bounds.origin.y - bounds.size.height; + + for (NSInteger y = 0; y < h; y++) { + for (NSInteger x = 0; x < w; x++) { + CGFloat normalX, normalY, normalZ; + [self computeNormalAtX:x y:y pixels:inputPixels width:w height:h + scale:scale nx:&normalX ny:&normalY nz:&normalZ]; + + CGFloat alpha = (CGFloat)inputPixels[(y * w + x) * 4 + 3] / 255.0; + CGFloat surfZ = self.surfaceScale * alpha; + + CGFloat svgX = svgOrigin.x + (bmpOffX_d + (CGFloat)x) / scale; + CGFloat svgY = svgOrigin.y + (bmpOffY_d + (CGFloat)y) / scale; + + CGFloat lx, ly, lz; + [self lightDirectionAtSvgX:svgX svgY:svgY + surfaceZ:surfZ lx:&lx ly:&ly lz:&lz]; + + // N dot L + CGFloat nDotL = normalX * lx + normalY * ly + normalZ * lz; + nDotL = fmax(0.0, nDotL); + + CGFloat diff = _diffuseConstant * nDotL; + diff = fmax(0.0, fmin(1.0, diff)); + + NSInteger idx = (y * w + x) * 4; + // SVG spec: output = kd * (N dot L) * lightColor, alpha = 1 + CGFloat r = fmin(1.0, diff * self.lightColorR); + CGFloat g = fmin(1.0, diff * self.lightColorG); + CGFloat b = fmin(1.0, diff * self.lightColorB); + + outPixels[idx + 0] = (uint8_t)(r * 255.0 + 0.5); + outPixels[idx + 1] = (uint8_t)(g * 255.0 + 0.5); + outPixels[idx + 2] = (uint8_t)(b * 255.0 + 0.5); + outPixels[idx + 3] = 255; // diffuse always opaque + } + } + + free(inputPixels); + + // Flip output back to CG bottom-up convention for CIImage + { + uint8_t* rowBuf = (uint8_t*)malloc(bytesPerRow); + for (NSInteger top = 0, bot = h - 1; top < bot; top++, bot--) { + memcpy(rowBuf, outPixels + top * bytesPerRow, bytesPerRow); + memcpy(outPixels + top * bytesPerRow, outPixels + bot * bytesPerRow, bytesPerRow); + memcpy(outPixels + bot * bytesPerRow, rowBuf, bytesPerRow); + } + free(rowBuf); + } + + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceLinearSRGB); + NSData* data = [NSData dataWithBytesNoCopy:outPixels length:bytesPerRow * h freeWhenDone:YES]; + CIImage* output = [CIImage imageWithBitmapData:data bytesPerRow:bytesPerRow + size:CGSizeMake(w, h) + format:kCIFormatRGBA8 colorSpace:cs]; + CGColorSpaceRelease(cs); + + if (bounds.origin.x != 0 || bounds.origin.y != 0) { + output = [output imageByApplyingTransform: + CGAffineTransformMakeTranslation(bounds.origin.x, bounds.origin.y)]; + } + + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.h new file mode 100644 index 00000000..358cd29d --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.h @@ -0,0 +1,9 @@ +// +// IJSVGFilterEffectMerge.h +// IJSVG +// + +#import + +@interface IJSVGFilterEffectMerge : IJSVGFilterEffect +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.m new file mode 100644 index 00000000..71884877 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectMerge.m @@ -0,0 +1,40 @@ +// +// IJSVGFilterEffectMerge.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectMerge + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + NSString* lowerName = self.name.lowercaseString; + + if([lowerName isEqualToString:@"femergenode"]) { + CIImage* input = [graph imageForInput:self.inputName]; + [graph setImage:input forResult:self.resultName]; + return input; + } + + // feMerge: composite children (feMergeNode) in order + CIImage* result = nil; + for(IJSVGFilterEffect* child in self.children) { + CIImage* childResult = [child processWithGraph:graph]; + if(result == nil) { + result = childResult; + } else { + CIFilter* over = [CIFilter filterWithName:@"CISourceOverCompositing"]; + [over setDefaults]; + [over setValue:childResult forKey:kCIInputImageKey]; + [over setValue:result forKey:kCIInputBackgroundImageKey]; + result = [over valueForKey:kCIOutputImageKey]; + } + } + if(result == nil) result = [graph imageForInput:self.inputName]; + [graph setImage:result forResult:self.resultName]; + return result; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.h new file mode 100644 index 00000000..2a95a03e --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.h @@ -0,0 +1,13 @@ +// +// IJSVGFilterEffectOffset.h +// IJSVG +// + +#import + +@interface IJSVGFilterEffectOffset : IJSVGFilterEffect + +@property (nonatomic, assign) CGFloat dx; +@property (nonatomic, assign) CGFloat dy; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.m new file mode 100644 index 00000000..0404baa9 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectOffset.m @@ -0,0 +1,28 @@ +// +// IJSVGFilterEffectOffset.m +// IJSVG +// + +#import +#import + +@implementation IJSVGFilterEffectOffset + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + NSString* dxStr = attributes[@"dx"]; + if(dxStr != nil) _dx = dxStr.doubleValue; + NSString* dyStr = attributes[@"dy"]; + if(dyStr != nil) _dy = dyStr.doubleValue; +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* input = [graph imageForInput:self.inputName]; + CGFloat s = graph.scale; + CIImage* output = [input imageByApplyingTransform:CGAffineTransformMakeTranslation(_dx * s, _dy * s)]; + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.h b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.h new file mode 100644 index 00000000..3c7331a6 --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.h @@ -0,0 +1,21 @@ +// +// IJSVGFilterEffectTurbulence.h +// IJSVG +// + +#import + +typedef NS_ENUM(NSInteger, IJSVGTurbulenceType) { + IJSVGTurbulenceTypeTurbulence, + IJSVGTurbulenceTypeFractalNoise, +}; + +@interface IJSVGFilterEffectTurbulence : IJSVGFilterEffect + +@property (nonatomic, assign) CGFloat baseFrequencyX; +@property (nonatomic, assign) CGFloat baseFrequencyY; +@property (nonatomic, assign) NSInteger numOctaves; +@property (nonatomic, assign) CGFloat seed; +@property (nonatomic, assign) IJSVGTurbulenceType turbulenceType; + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.m b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.m new file mode 100644 index 00000000..2a23295d --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/Filter Effects/IJSVGFilterEffectTurbulence.m @@ -0,0 +1,297 @@ +// +// IJSVGFilterEffectTurbulence.m +// IJSVG +// + +#import +#import + +// --------------------------------------------------------------------------- +// SVG feTurbulence — Perlin noise per the SVG 1.1 specification. +// Reference: https://www.w3.org/TR/SVG11/filters.html#feTurbulenceElement +// --------------------------------------------------------------------------- + +#define BSIZE 0x100 +#define BM 0xff +#define N 0x1000 + +@implementation IJSVGFilterEffectTurbulence { + int _latticeSelector[BSIZE + BSIZE + 2]; + float _gradient[4][BSIZE + BSIZE + 2][2]; // 4 channels, each entry is a 2D gradient (float to match WebKit) + BOOL _tablesInitialized; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _baseFrequencyX = 0.0; + _baseFrequencyY = 0.0; + _numOctaves = 1; + _seed = 0.0; + _turbulenceType = IJSVGTurbulenceTypeTurbulence; + _tablesInitialized = NO; + } + return self; +} + +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + [super parseEffectAttributes:attributes]; + + NSString* baseFreq = attributes[@"baseFrequency"]; + if (baseFreq.length > 0) { + NSArray* components = [baseFreq componentsSeparatedByCharactersInSet: + [NSCharacterSet characterSetWithCharactersInString:@" ,"]]; + NSMutableArray* values = [NSMutableArray array]; + for (NSString* comp in components) { + NSString* trimmed = [comp stringByTrimmingCharactersInSet: + NSCharacterSet.whitespaceCharacterSet]; + if (trimmed.length > 0) { + [values addObject:trimmed]; + } + } + if (values.count >= 1) { + _baseFrequencyX = [values[0] doubleValue]; + _baseFrequencyY = _baseFrequencyX; + } + if (values.count >= 2) { + _baseFrequencyY = [values[1] doubleValue]; + } + } + + NSString* octaves = attributes[@"numOctaves"]; + if (octaves.length > 0) { + _numOctaves = octaves.integerValue; + if (_numOctaves < 1) _numOctaves = 1; + } + + NSString* seedStr = attributes[@"seed"]; + if (seedStr.length > 0) { + _seed = seedStr.doubleValue; + } + + NSString* type = attributes[@"type"]; + if (type.length > 0) { + if ([type caseInsensitiveCompare:@"fractalNoise"] == NSOrderedSame) { + _turbulenceType = IJSVGTurbulenceTypeFractalNoise; + } else { + _turbulenceType = IJSVGTurbulenceTypeTurbulence; + } + } +} + +// --------------------------------------------------------------------------- +#pragma mark - SVG spec PRNG and table setup +// --------------------------------------------------------------------------- + +// Park-Miller LCG (Lehmer RNG) used by WebKit for feTurbulence. +// Uses Schrage decomposition to avoid overflow: a*seed mod m +// where a=16807, m=2^31-1, q=m/a=127773, r=m%a=2836. +static const long kRandMaximum = 2147483647L; // 2^31 - 1 +static const long kRandAmplitude = 16807L; // 7^5 +static const long kRandQ = 127773L; // m / a +static const long kRandR = 2836L; // m % a + +static long _svgRandom(long lSeed) +{ + long result = kRandAmplitude * (lSeed % kRandQ) - kRandR * (lSeed / kRandQ); + if (result <= 0) + result += kRandMaximum; + return result; +} + +- (void)initializeTables +{ + if (_tablesInitialized) return; + _tablesInitialized = YES; + + long lSeed = (long)_seed; + // Clamp seed to [1, 2^31-2] to match WebKit's Park-Miller PRNG range. + if (lSeed <= 0) lSeed = -(lSeed % (kRandMaximum - 1)) + 1; + if (lSeed > kRandMaximum - 1) lSeed = kRandMaximum - 1; + + // Initialize gradient vectors for all 4 channels (R, G, B, A). + // Per the SVG spec, gradients are generated first, then the + // permutation table is initialized and shuffled once. + for (int k = 0; k < 4; k++) { + for (int i = 0; i < BSIZE; i++) { + lSeed = _svgRandom(lSeed); + _gradient[k][i][0] = (float)((lSeed % (BSIZE + BSIZE)) - BSIZE) / BSIZE; + lSeed = _svgRandom(lSeed); + _gradient[k][i][1] = (float)((lSeed % (BSIZE + BSIZE)) - BSIZE) / BSIZE; + float s = hypotf(_gradient[k][i][0], _gradient[k][i][1]); + if (s > 0.0f) { + _gradient[k][i][0] /= s; + _gradient[k][i][1] /= s; + } + } + } + + // Initialize and shuffle the permutation table (Fisher-Yates, descending). + // WebKit calls random() then uses the result: j = random() % BSize. + for (int i = 0; i < BSIZE; i++) { + _latticeSelector[i] = i; + } + for (int i = BSIZE - 1; i > 0; i--) { + int k = _latticeSelector[i]; + lSeed = _svgRandom(lSeed); + int j = (int)(lSeed % BSIZE); + _latticeSelector[i] = _latticeSelector[j]; + _latticeSelector[j] = k; + } + + // Duplicate for overflow + for (int i = 0; i < BSIZE + 2; i++) { + _latticeSelector[BSIZE + i] = _latticeSelector[i]; + for (int k = 0; k < 4; k++) { + _gradient[k][BSIZE + i][0] = _gradient[k][i][0]; + _gradient[k][BSIZE + i][1] = _gradient[k][i][1]; + } + } +} + +// --------------------------------------------------------------------------- +#pragma mark - Noise evaluation (SVG spec algorithm) +// --------------------------------------------------------------------------- + +static float _sCurve(float t) { return t * t * (3.0f - 2.0f * t); } +static float _lerp(float t, float a, float b) { return a + t * (b - a); } + +- (float)noise2ForChannel:(int)channel x:(float)x y:(float)y +{ + float t = x + N; + int bx0 = ((int)t) & BM; + int bx1 = (bx0 + 1) & BM; + float rx0 = t - (int)t; + float rx1 = rx0 - 1.0f; + + t = y + N; + int by0 = ((int)t) & BM; + int by1 = (by0 + 1) & BM; + float ry0 = t - (int)t; + float ry1 = ry0 - 1.0f; + + int i = _latticeSelector[bx0]; + int j = _latticeSelector[bx1]; + + int b00 = _latticeSelector[i + by0]; + int b10 = _latticeSelector[j + by0]; + int b01 = _latticeSelector[i + by1]; + int b11 = _latticeSelector[j + by1]; + + float sx = _sCurve(rx0); + float sy = _sCurve(ry0); + + float u, v; + + u = rx0 * _gradient[channel][b00][0] + ry0 * _gradient[channel][b00][1]; + v = rx1 * _gradient[channel][b10][0] + ry0 * _gradient[channel][b10][1]; + float a = _lerp(sx, u, v); + + u = rx0 * _gradient[channel][b01][0] + ry1 * _gradient[channel][b01][1]; + v = rx1 * _gradient[channel][b11][0] + ry1 * _gradient[channel][b11][1]; + float b = _lerp(sx, u, v); + + return _lerp(sy, a, b); +} + +- (void)turbulenceAtX:(float)x y:(float)y result:(float[4])result +{ + BOOL isFractalNoise = (_turbulenceType == IJSVGTurbulenceTypeFractalNoise); + + for (int ch = 0; ch < 4; ch++) { + result[ch] = 0.0f; + } + + float ratio = 1.0f; + for (NSInteger oct = 0; oct < _numOctaves; oct++) { + for (int ch = 0; ch < 4; ch++) { + float n = [self noise2ForChannel:ch + x:x * (float)_baseFrequencyX * ratio + y:y * (float)_baseFrequencyY * ratio]; + if (isFractalNoise) { + result[ch] += n / ratio; + } else { + result[ch] += fabsf(n) / ratio; + } + } + ratio *= 2.0f; + } + + // Clamp to [0, 1] + for (int ch = 0; ch < 4; ch++) { + if (isFractalNoise) { + result[ch] = (result[ch] + 1.0f) / 2.0f; + } + result[ch] = fmaxf(0.0f, fminf(1.0f, result[ch])); + } +} + +// --------------------------------------------------------------------------- +#pragma mark - Rendering +// --------------------------------------------------------------------------- + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + [self initializeTables]; + + CGRect bounds = graph.sourceBounds; + CGFloat scale = graph.scale; + + // sourceBounds is the CIImage extent in pixels — use directly without + // multiplying by scale (the bitmap is already at device pixel resolution). + NSInteger w = (NSInteger)CGRectGetWidth(bounds); + NSInteger h = (NSInteger)CGRectGetHeight(bounds); + if (w <= 0 || h <= 0) { + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + return empty; + } + + NSInteger bytesPerRow = w * 4; + uint8_t* pixels = (uint8_t*)calloc(bytesPerRow * h, 1); + if (pixels == NULL) { + CIImage* empty = [CIImage emptyImage]; + [graph setImage:empty forResult:self.resultName]; + return empty; + } + + // Map pixel coordinates to absolute SVG user-space using the element offset. + // CIImage bitmaps are bottom-up (row 0 = bottom), so flip Y to match SVG's + // top-down coordinate system. + CGPoint svgOrigin = graph.elementSVGOrigin; + for (NSInteger py = 0; py < h; py++) { + for (NSInteger px = 0; px < w; px++) { + float svgX = (float)(svgOrigin.x + (bounds.origin.x + (double)px) / scale); + float svgY = (float)(svgOrigin.y + (bounds.origin.y + (double)(h - 1 - py)) / scale); + + float result[4]; + [self turbulenceAtX:svgX y:svgY result:result]; + + NSInteger idx = (py * w + px) * 4; + pixels[idx + 0] = (uint8_t)(result[0] * 255.0f + 0.5f); + pixels[idx + 1] = (uint8_t)(result[1] * 255.0f + 0.5f); + pixels[idx + 2] = (uint8_t)(result[2] * 255.0f + 0.5f); + pixels[idx + 3] = (uint8_t)(result[3] * 255.0f + 0.5f); + } + } + + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + NSData* data = [NSData dataWithBytesNoCopy:pixels length:bytesPerRow * h freeWhenDone:YES]; + CIImage* output = [CIImage imageWithBitmapData:data + bytesPerRow:bytesPerRow + size:CGSizeMake(w, h) + format:kCIFormatRGBA8 + colorSpace:cs]; + CGColorSpaceRelease(cs); + + // The bitmap is already in CG pixel space (same size as sourceBounds). + // No scaling needed — the noise was sampled at SVG coordinates but stored + // at pixel resolution to match other CIImages in the filter pipeline. + + [graph setImage:output forResult:self.resultName]; + return output; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.h index c3eee4d8..d7cde2af 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.h @@ -7,13 +7,17 @@ // #import +#import #import @interface IJSVGFilter : IJSVGGroup - (CGImageRef)newImageByApplyFilterToLayer:(CALayer*)layer - scale:(CGFloat)scale; + scale:(CGFloat)scale + outputFrame:(CGRect*)outFrame; @property (nonatomic, readonly) BOOL valid; +@property (nonatomic, assign) BOOL usesSRGBColorInterpolation; +@property (nonatomic, strong) NSXMLElement* defElement; @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.m index cb078a61..2ecb882c 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilter.m @@ -8,40 +8,365 @@ #import #import +#import +#import #import +#import #import +#import +#import +#import +#import +#include + +#if TARGET_OS_IOS +static const size_t kIJSVGFilterMaxBitmapDimension = 8192; +static const size_t kIJSVGFilterMaxBitmapBytes = 16ull * 1024ull * 1024ull; +#else +static const size_t kIJSVGFilterMaxBitmapDimension = 32768; +static const size_t kIJSVGFilterMaxBitmapBytes = 512ull * 1024ull * 1024ull; +#endif + +static inline BOOL IJSVGFilterValueIsFiniteAndPositive(CGFloat value) +{ + return isfinite(value) && value > 0.f; +} + +static inline BOOL IJSVGFilterRectHasRenderableExtent(CGRect rect) +{ + return isfinite(rect.origin.x) && + isfinite(rect.origin.y) && + IJSVGFilterValueIsFiniteAndPositive(rect.size.width) && + IJSVGFilterValueIsFiniteAndPositive(rect.size.height); +} + +static inline BOOL IJSVGFilterBitmapSizeIsSafe(size_t width, size_t height) +{ + if(width == 0 || height == 0) { + return NO; + } + if(width > kIJSVGFilterMaxBitmapDimension || + height > kIJSVGFilterMaxBitmapDimension) { + return NO; + } + if(width > SIZE_MAX / height) { + return NO; + } + size_t pixelCount = width * height; + if(pixelCount > SIZE_MAX / 4) { + return NO; + } + return pixelCount * 4 <= kIJSVGFilterMaxBitmapBytes; +} + +static inline CGFloat IJSVGFilterClampedScaleForBitmapSize(CGFloat width, + CGFloat height, + CGFloat scale) +{ + if(IJSVGFilterValueIsFiniteAndPositive(width) == NO || + IJSVGFilterValueIsFiniteAndPositive(height) == NO || + IJSVGFilterValueIsFiniteAndPositive(scale) == NO) { + return scale; + } + + CGFloat bitmapBytesAtScaleOne = width * height * 4.f; + if(IJSVGFilterValueIsFiniteAndPositive(bitmapBytesAtScaleOne) == NO) { + return scale; + } + + CGFloat maxScale = sqrt((CGFloat)kIJSVGFilterMaxBitmapBytes / bitmapBytesAtScaleOne); + if(IJSVGFilterValueIsFiniteAndPositive(maxScale) == NO) { + return scale; + } + return MIN(scale, maxScale); +} + +static CGFloat IJSVGFilterEffectiveScaleForLayer(CALayer *layer, + CGFloat backingScale) +{ + CGFloat effectiveScale = backingScale; + IJSVGRootLayer *rootLayer = (IJSVGRootLayer *)[IJSVGLayer rootLayerForLayer:layer]; + if([rootLayer isKindOfClass:IJSVGRootLayer.class] == NO || + rootLayer.rendersWithViewBoxTransform == NO || + rootLayer.viewBox == nil) { + return effectiveScale; + } + + CGRect viewBox = [rootLayer.viewBox computeValue:CGSizeZero]; + CGRect drawingRect = rootLayer.bounds; + if(IJSVGFilterRectHasRenderableExtent(viewBox) == NO || + IJSVGFilterRectHasRenderableExtent(drawingRect) == NO) { + return effectiveScale; + } + + CGAffineTransform transform = IJSVGViewBoxComputeTransform(viewBox, + drawingRect, + rootLayer.viewBoxAlignment, + rootLayer.viewBoxMeetOrSlice); + CGFloat scaleX = hypot(transform.a, transform.c); + CGFloat scaleY = hypot(transform.b, transform.d); + CGFloat viewBoxScale = MAX(MIN(scaleX, scaleY), 0.f); + if(viewBoxScale <= 0.f || isfinite(viewBoxScale) == NO) { + return effectiveScale; + } + return effectiveScale * viewBoxScale; +} @implementation IJSVGFilter +- (void)setDefaults +{ + [super setDefaults]; + self.units = IJSVGUnitObjectBoundingBox; + self.contentUnits = IJSVGUnitUserSpaceOnUse; +} + +- (BOOL)usesSRGBFilterInterpolation +{ + if(self.usesSRGBColorInterpolation == YES) { + return YES; + } + for(IJSVGFilterEffect *effect in self.children) { + if([effect isKindOfClass:IJSVGFilterEffectGaussianBlur.class] == NO) { + continue; + } + IJSVGFilterEffectGaussianBlur *blurEffect = (IJSVGFilterEffectGaussianBlur *)effect; + if(blurEffect.usesSRGBColorInterpolation == YES) { + return YES; + } + } + return NO; +} + - (BOOL)valid { return self.children.count != 0; } +// Resolve a filter region attribute value against the element bbox dimension. +// In objectBoundingBox mode, percentages and plain numbers are relative to the +// element bbox. In userSpaceOnUse, plain numbers are absolute user-space units. +- (CGFloat)filterRegionValue:(IJSVGUnitLength*)unit forDimension:(CGFloat)dim +{ + if(unit.type == IJSVGUnitLengthTypePercentage) { + return [unit computeValue:dim]; + } + if(self.units == IJSVGUnitObjectBoundingBox && + unit.type == IJSVGUnitLengthTypeNumber) { + return unit.value * dim; + } + return [unit computeValue:dim]; +} + - (CGImageRef)newImageByApplyFilterToLayer:(CALayer*)layer scale:(CGFloat)scale + outputFrame:(CGRect*)outFrame { IJSVGFilter* filter = layer.filter; layer.filter = nil; - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGFloat effectiveScale = IJSVGFilterEffectiveScaleForLayer(layer, scale); + if(outFrame != NULL) { + *outFrame = CGRectZero; + } + + // Move layer to origin and clear referencingLayer for bitmap rendering. + // Store the original SVG position so gradient layers can compute + // userSpaceOnUse coordinates correctly via IJSVGFilterElementOffset. + CGRect savedFrame = layer.frame; + CGRect savedOBB = layer.outerBoundingBox; + CALayer* savedRef = layer.referencingLayer; + float savedOpacity = layer.opacity; + IJSVGThreadManager* threadMgr = nil; + CGColorSpaceRef colorSpace = NULL; + CGImageRef originalImage = NULL; + CGImageRef outputImage = NULL; + CIImage* sourceGraphic = nil; + IJSVGFilterGraph* graph = nil; + CIImage* output = nil; + CIContext* context = nil; + CGRect outputExtent = CGRectNull; + BOOL resetLayerState = NO; + BOOL resetThreadState = NO; + + if(effectiveScale <= 0.f || IJSVGFilterRectHasRenderableExtent(savedOBB) == NO) { + goto cleanup; + } + + // Compute the SVG filter region from x/y/width/height attributes. + // Default filterUnits is objectBoundingBox: values are fractions of the + // element bbox. Percentages are resolved by computeValue:, but plain + // numbers (e.g. x="-.16711") must be multiplied by the bbox dimension. + // SVG defaults: x=-10%, y=-10%, width=120%, height=120%. + CGFloat elemW = savedOBB.size.width; + CGFloat elemH = savedOBB.size.height; + CGFloat frx = self.x != nil ? [self filterRegionValue:self.x forDimension:elemW] : -0.1 * elemW; + CGFloat fry = self.y != nil ? [self filterRegionValue:self.y forDimension:elemH] : -0.1 * elemH; + CGFloat frw = self.width != nil ? [self filterRegionValue:self.width forDimension:elemW] : 1.2 * elemW; + CGFloat frh = self.height != nil ? [self filterRegionValue:self.height forDimension:elemH] : 1.2 * elemH; + CGFloat regionMinX = frx; + CGFloat regionMinY = fry; + if(self.units == IJSVGUnitUserSpaceOnUse) { + regionMinX -= savedOBB.origin.x; + regionMinY -= savedOBB.origin.y; + } + CGRect localFilterRegion = CGRectMake(regionMinX, regionMinY, frw, frh); + if(isfinite(frx) == NO || + isfinite(fry) == NO || + IJSVGFilterValueIsFiniteAndPositive(frw) == NO || + IJSVGFilterValueIsFiniteAndPositive(frh) == NO || + IJSVGFilterRectHasRenderableExtent(localFilterRegion) == NO) { + goto cleanup; + } + // Padding must cover the filter region extent beyond the element on all sides, + // and also provide enough room for blur kernels to compute correctly. + CGFloat padLeft = MAX(0, -CGRectGetMinX(localFilterRegion)); + CGFloat padRight = MAX(0, CGRectGetMaxX(localFilterRegion) - elemW); + CGFloat padTop = MAX(0, -CGRectGetMinY(localFilterRegion)); + CGFloat padBottom = MAX(0, CGRectGetMaxY(localFilterRegion) - elemH); + CGFloat unscaledBitmapWidth = elemW + padLeft + padRight; + CGFloat unscaledBitmapHeight = elemH + padTop + padBottom; + effectiveScale = IJSVGFilterClampedScaleForBitmapSize(unscaledBitmapWidth, + unscaledBitmapHeight, + effectiveScale); + CGFloat bitmapWidth = ceilf(unscaledBitmapWidth * effectiveScale); + CGFloat bitmapHeight = ceilf(unscaledBitmapHeight * effectiveScale); + if(IJSVGFilterValueIsFiniteAndPositive(bitmapWidth) == NO || + IJSVGFilterValueIsFiniteAndPositive(bitmapHeight) == NO) { + goto cleanup; + } + size_t bmpW = (size_t)bitmapWidth; + size_t bmpH = (size_t)bitmapHeight; + if(IJSVGFilterBitmapSizeIsSafe(bmpW, bmpH) == NO) { + goto cleanup; + } + + CFStringRef colorSpaceName = self.usesSRGBFilterInterpolation == YES ? + kCGColorSpaceSRGB : kCGColorSpaceLinearSRGB; + colorSpace = CGColorSpaceCreateWithName(colorSpaceName); + if(colorSpace == NULL) { + goto cleanup; + } uint32_t info = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little; - CGImageRef originalImage = [IJSVGLayer newImageForLayer:layer - options:IJSVGLayerDrawingOptionNone - colorSpace:colorSpace - bitmapInfo:info - scale:scale]; - CIImage* image = [CIImage imageWithCGImage:originalImage]; - CGColorSpaceRelease(colorSpace); - CGImageRelease(originalImage); - + + if([layer isKindOfClass:IJSVGImageLayer.class]) { + // Image layers can bypass the generic layer-tree renderer here and + // draw their content directly into the padded filter bitmap. + CGContextRef bmpCtx = CGBitmapContextCreate(NULL, bmpW, bmpH, 8, 0, colorSpace, info); + if(bmpCtx == NULL) { + goto cleanup; + } + CGContextScaleCTM(bmpCtx, effectiveScale, effectiveScale); + CGContextTranslateCTM(bmpCtx, padLeft, padTop); + [(IJSVGImageLayer *)layer drawInContext:bmpCtx]; + originalImage = CGBitmapContextCreateImage(bmpCtx); + CGContextRelease(bmpCtx); + } else { + // Move layer to origin for rendering (standard approach) + layer.frame = CGRectMake(0, 0, savedFrame.size.width, savedFrame.size.height); + layer.outerBoundingBox = CGRectMake(0, 0, savedOBB.size.width, savedOBB.size.height); + layer.referencingLayer = nil; + resetLayerState = YES; + + layer.opacity = 1.0f; + + threadMgr = IJSVGThreadManager.currentManager; + [threadMgr setUserInfoObject:@YES forKey:@"IJSVGFilterRendering"]; + [threadMgr setUserInfoObject:[NSValue valueWithPoint:NSMakePoint(savedOBB.origin.x, savedOBB.origin.y)] + forKey:@"IJSVGFilterElementOffset"]; + resetThreadState = YES; + + CGContextRef bmpCtx = CGBitmapContextCreate(NULL, bmpW, bmpH, 8, 0, colorSpace, info); + if(bmpCtx == NULL) { + goto cleanup; + } + CGContextScaleCTM(bmpCtx, effectiveScale, effectiveScale); + CGContextTranslateCTM(bmpCtx, padLeft, padTop); + [IJSVGLayer renderLayer:layer + inContext:bmpCtx + options:IJSVGLayerDrawingOptionNone]; + originalImage = CGBitmapContextCreateImage(bmpCtx); + CGContextRelease(bmpCtx); + } + + if(originalImage == NULL) { + goto cleanup; + } + + sourceGraphic = [CIImage imageWithCGImage:originalImage]; + if(sourceGraphic == nil) { + goto cleanup; + } + + graph = [[IJSVGFilterGraph alloc] initWithSourceGraphic:sourceGraphic scale:effectiveScale]; + graph.elementSVGOrigin = CGPointMake(savedOBB.origin.x - padLeft, + savedOBB.origin.y - padTop); + for(IJSVGFilterEffect* effect in self.children) { - image = [effect processImage:image]; + [effect processWithGraph:graph]; + } + + output = [graph lastResult]; + CGRect pixelFilterRegion = CGRectMake((padLeft + CGRectGetMinX(localFilterRegion)) * effectiveScale, + (padTop + CGRectGetMinY(localFilterRegion)) * effectiveScale, + frw * effectiveScale, + frh * effectiveScale); + if(IJSVGFilterRectHasRenderableExtent(pixelFilterRegion) == NO) { + output = nil; + } else if(output == nil || output == sourceGraphic) { + output = [sourceGraphic imageByCroppingToRect:pixelFilterRegion]; + } else { + output = [output imageByCroppingToRect:pixelFilterRegion]; + } + + outputExtent = output != nil ? output.extent : CGRectNull; + if(IJSVGFilterRectHasRenderableExtent(outputExtent) == NO) { + goto cleanup; + } + + if(outFrame != NULL) { + // Map the CIImage output extent from pixel space back to user-space + // coordinates. Effects like feOffset shift the extent relative to the + // pixel filter region; we must reflect that shift in the drawing frame + // so the result is positioned correctly. + *outFrame = CGRectMake( + localFilterRegion.origin.x + (outputExtent.origin.x - pixelFilterRegion.origin.x) / effectiveScale, + localFilterRegion.origin.y + (outputExtent.origin.y - pixelFilterRegion.origin.y) / effectiveScale, + outputExtent.size.width / effectiveScale, + outputExtent.size.height / effectiveScale + ); + } + + context = IJSVGThreadManager.currentManager.CIContext; +#if TARGET_OS_IOS + outputImage = [context createCGImage:output + fromRect:outputExtent + format:kCIFormatRGBA8 + colorSpace:colorSpace]; +#else + outputImage = [context createCGImage:output + fromRect:outputExtent]; +#endif + if(outputImage == NULL) { + outputImage = CGImageCreateCopy(originalImage); + } + +cleanup: + if(resetThreadState == YES) { + [threadMgr setUserInfoObject:nil forKey:@"IJSVGFilterRendering"]; + [threadMgr setUserInfoObject:nil forKey:@"IJSVGFilterElementOffset"]; + } + if(resetLayerState == YES) { + layer.frame = savedFrame; + layer.outerBoundingBox = savedOBB; + layer.referencingLayer = savedRef; + layer.opacity = savedOpacity; + } + if(colorSpace != NULL) { + CGColorSpaceRelease(colorSpace); + } + if(originalImage != NULL) { + CGImageRelease(originalImage); } - - IJSVGThreadManager* manager = IJSVGThreadManager.currentManager; - CIContext* context = manager.CIContext; - CGImageRef outputImage = [context createCGImage:image - fromRect:image.extent]; layer.filter = filter; return outputImage; } diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.h index 99be8ac3..de986a4a 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.h @@ -8,6 +8,8 @@ #import +@class IJSVGFilterGraph; + typedef NS_ENUM(NSInteger, IJSVGFilterEffectSource) { IJSVGFilterEffectSourceGraphic, IJSVGFilterEffectSourceAlpha, @@ -31,10 +33,18 @@ typedef NS_ENUM(NSInteger, IJSVGFilterEffectEdgeMode) { @property (nonatomic, assign) IJSVGFilterEffectEdgeMode edgeMode; @property (nonatomic, copy) NSString* primitiveReference; +// Graph-based input/output routing +@property (nonatomic, copy) NSString* inputName; // "in" attribute +@property (nonatomic, copy) NSString* inputName2; // "in2" attribute +@property (nonatomic, copy) NSString* resultName; // "result" attribute + + (Class)effectClassForElementName:(NSString*)name; ++ (BOOL)isElementNameSupported:(NSString*)name; + (IJSVGFilterEffectSource)sourceForString:(NSString*)string; + (IJSVGFilterEffectEdgeMode)edgeModeForString:(NSString*)string; +- (void)parseEffectAttributes:(NSDictionary*)attributes; +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph; - (CIImage*)processImage:(CIImage*)image; @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.m index 58c70669..44b87723 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterEffect.m @@ -7,7 +7,17 @@ // #import +#import #import +#import +#import +#import +#import +#import +#import +#import +#import +#import #import @implementation IJSVGFilterEffect @@ -17,7 +27,18 @@ @implementation IJSVGFilterEffect + (void)load { _elementClassMap = @{ - @"fegaussianblur": IJSVGFilterEffectGaussianBlur.class + @"fegaussianblur": IJSVGFilterEffectGaussianBlur.class, + @"feflood": IJSVGFilterEffectFlood.class, + @"feoffset": IJSVGFilterEffectOffset.class, + @"fecomposite": IJSVGFilterEffectComposite.class, + @"femerge": IJSVGFilterEffectMerge.class, + @"femergenode": IJSVGFilterEffectMerge.class, + @"fecolormatrix": IJSVGFilterEffectColorMatrix.class, + @"feblend": IJSVGFilterEffectBlend.class, + @"feturbulence": IJSVGFilterEffectTurbulence.class, + @"fedisplacementmap": IJSVGFilterEffectDisplacementMap.class, + @"fespecularlighting": IJSVGFilterEffectSpecularLighting.class, + @"fediffuselighting": IJSVGFilterEffectDiffuseLighting.class, }; } @@ -27,51 +48,48 @@ + (Class)effectClassForElementName:(NSString*)name return _elementClassMap[key] ?: IJSVGFilterEffect.class; } ++ (BOOL)isElementNameSupported:(NSString*)name +{ + NSString* key = name.lowercaseString; + return _elementClassMap[key] != nil; +} + + (IJSVGFilterEffectSource)sourceForString:(NSString*)string { const char* name = string.UTF8String; - if(name == NULL) { - return IJSVGFilterEffectSourceGraphic; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "sourcegraphic") == YES) { - return IJSVGFilterEffectSourceGraphic; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "sourcealpha") == YES) { - return IJSVGFilterEffectSourceAlpha; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "backgroundimage") == YES) { - return IJSVGFilterEffectSourceBackgroundImage; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "backgroundalpha") == YES) { - return IJSVGFilterEffectSourceBackgroundAlpha; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "fillpaint") == YES) { - return IJSVGFilterEffectSourceFillPaint; - } - if(IJSVGCharBufferCaseInsensitiveCompare(name, "strokepain") == YES) { - return IJSVGFilterEffectSourceStrokePaint; - } + if(name == NULL) return IJSVGFilterEffectSourceGraphic; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "sourcegraphic") == YES) return IJSVGFilterEffectSourceGraphic; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "sourcealpha") == YES) return IJSVGFilterEffectSourceAlpha; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "backgroundimage") == YES) return IJSVGFilterEffectSourceBackgroundImage; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "backgroundalpha") == YES) return IJSVGFilterEffectSourceBackgroundAlpha; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "fillpaint") == YES) return IJSVGFilterEffectSourceFillPaint; + if(IJSVGCharBufferCaseInsensitiveCompare(name, "strokepaint") == YES) return IJSVGFilterEffectSourceStrokePaint; return IJSVGFilterEffectSourcePrimitiveReference; } + (IJSVGFilterEffectEdgeMode)edgeModeForString:(NSString*)string { const char* name = string.lowercaseString.UTF8String; - if(name == NULL) { - return IJSVGFilterEffectEdgeModeNone; - } - if(IJSVGCharBufferCompare(name, "none") == YES) { - return IJSVGFilterEffectEdgeModeNone; - } - if(IJSVGCharBufferCompare(name, "wrap") == YES) { - return IJSVGFilterEffectEdgeModeWrap; - } - if(IJSVGCharBufferCompare(name, "duplicate") == YES) { - return IJSVGFilterEffectEdgeModeDuplicate; - } + if(name == NULL) return IJSVGFilterEffectEdgeModeNone; + if(IJSVGCharBufferCompare(name, "none") == YES) return IJSVGFilterEffectEdgeModeNone; + if(IJSVGCharBufferCompare(name, "wrap") == YES) return IJSVGFilterEffectEdgeModeWrap; + if(IJSVGCharBufferCompare(name, "duplicate") == YES) return IJSVGFilterEffectEdgeModeDuplicate; return IJSVGFilterEffectEdgeModeNone; } +- (void)parseEffectAttributes:(NSDictionary*)attributes +{ + // Subclasses override +} + +- (CIImage*)processWithGraph:(IJSVGFilterGraph*)graph +{ + CIImage* input = [graph imageForInput:self.inputName]; + CIImage* output = [self processImage:input]; + [graph setImage:output forResult:self.resultName]; + return output; +} + - (CIImage*)processImage:(CIImage*)image { return image; diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.h new file mode 100644 index 00000000..ec26954e --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.h @@ -0,0 +1,24 @@ +// +// IJSVGFilterGraph.h +// IJSVG +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IJSVGFilterGraph : NSObject + +@property (nonatomic, readonly) CGRect sourceBounds; +@property (nonatomic, readonly) CGFloat scale; +@property (nonatomic, assign) CGPoint elementSVGOrigin; // SVG-space position of bitmap top-left + +- (instancetype)initWithSourceGraphic:(CIImage*)sourceGraphic scale:(CGFloat)scale; +- (CIImage*)imageForInput:(nullable NSString*)inputName; +- (void)setImage:(CIImage*)image forResult:(nullable NSString*)resultName; +- (CIImage*)lastResult; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.m new file mode 100644 index 00000000..510f58fb --- /dev/null +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGFilterGraph.m @@ -0,0 +1,71 @@ +// +// IJSVGFilterGraph.m +// IJSVG +// + +#import + +@implementation IJSVGFilterGraph { + NSMutableDictionary* _buffers; + CIImage* _lastResult; +} + +- (instancetype)initWithSourceGraphic:(CIImage*)sourceGraphic scale:(CGFloat)scale +{ + if((self = [super init]) != nil) { + _sourceBounds = sourceGraphic.extent; + _scale = scale; + _buffers = [NSMutableDictionary dictionary]; + _buffers[@"SourceGraphic"] = sourceGraphic; + _buffers[@"SourceAlpha"] = [self extractAlphaFromImage:sourceGraphic]; + _lastResult = sourceGraphic; + } + return self; +} + +- (CIImage*)extractAlphaFromImage:(CIImage*)image +{ + CIFilter* colorMatrix = [CIFilter filterWithName:@"CIColorMatrix"]; + [colorMatrix setDefaults]; + [colorMatrix setValue:image forKey:kCIInputImageKey]; + [colorMatrix setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputRVector"]; + [colorMatrix setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputGVector"]; + [colorMatrix setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputBVector"]; + [colorMatrix setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:1] forKey:@"inputAVector"]; + [colorMatrix setValue:[CIVector vectorWithX:0 Y:0 Z:0 W:0] forKey:@"inputBiasVector"]; + return [colorMatrix valueForKey:kCIOutputImageKey]; +} + +- (CIImage*)imageForInput:(NSString*)inputName +{ + if(inputName == nil || inputName.length == 0) { + return _lastResult; + } + NSString* lower = inputName.lowercaseString; + if([lower isEqualToString:@"sourcegraphic"]) { + return _buffers[@"SourceGraphic"]; + } + if([lower isEqualToString:@"sourcealpha"]) { + return _buffers[@"SourceAlpha"]; + } + CIImage* result = _buffers[inputName]; + if(result != nil) { + return result; + } + return _lastResult; +} + +- (void)setImage:(CIImage*)image forResult:(NSString*)resultName +{ + _lastResult = image; + if(resultName != nil && resultName.length > 0) { + _buffers[resultName] = image; + } +} + +- (CIImage*)lastResult +{ + return _lastResult; +} + +@end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.h index abbd546d..77ccc7d2 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.h @@ -11,8 +11,15 @@ #import #import +typedef NS_ENUM(NSInteger, IJSVGSpreadMethod) { + IJSVGSpreadMethodPad, // default: extend last color + IJSVGSpreadMethodReflect, // mirror the gradient + IJSVGSpreadMethodRepeat, // tile the gradient +}; + @interface IJSVGGradient : IJSVGGroup +@property (nonatomic, assign) IJSVGSpreadMethod spreadMethod; @property (nonatomic, strong) NSArray* colors; @property (nonatomic, assign) CGFloat* locations; @property (nonatomic, assign) NSUInteger numberOfStops; @@ -22,8 +29,6 @@ @property (nonatomic, strong) IJSVGUnitLength* y1; @property (nonatomic, strong) IJSVGUnitLength* y2; -@property (nonatomic, readonly) NSArray* stops; - + (CGFloat*)computeColorStops:(IJSVGGradient*)gradient colors:(NSArray**)someColors; diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.m index 812f2709..f4886e82 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGradient.m @@ -40,6 +40,7 @@ - (id)copyWithZone:(NSZone*)zone - (void)applyPropertiesFromNode:(IJSVGGradient*)node { [super applyPropertiesFromNode:node]; + self.spreadMethod = node.spreadMethod; self.numberOfStops = node.numberOfStops; self.colors = node.colors.copy; size_t length = sizeof(CGFloat)*node.numberOfStops; @@ -62,7 +63,7 @@ - (void)setLocations:(CGFloat*)locations + (CGFloat*)computeColorStops:(IJSVGGradient*)gradient colors:(NSArray**)someColors { - NSArray* stops = [gradient childrenOfType:IJSVGNodeTypeStop]; + NSArray* stops = gradient.children; NSMutableArray* colors = [[NSMutableArray alloc] initWithCapacity:stops.count]; CGFloat* stopsParams = (CGFloat*)malloc(stops.count * sizeof(CGFloat)); @@ -99,8 +100,16 @@ - (CGGradientRef)CGGradient for (NSColor* color in _colors) { CFArrayAppendValue(colors, color.CGColor); } - CGGradientRef result = CGGradientCreateWithColors(IJSVGColor.defaultColorSpace.CGColorSpace, - colors, _locations); + CGGradientRef result = NULL; +#if TARGET_OS_IOS + result = CGGradientCreateWithColors(IJSVGColor.defaultColorSpace, + colors, + _locations); +#else + result = CGGradientCreateWithColors(IJSVGColor.defaultColorSpace.CGColorSpace, + colors, + _locations); +#endif CFRelease(colors); return _CGGradient = result; } diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.h index 5a1f5f2c..ab6ffc70 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.h @@ -25,7 +25,6 @@ - (BOOL)childrenMatchTraits:(IJSVGNodeTraits)traits; - (BOOL)containsNodesMatchingTraits:(IJSVGNodeTraits)traits; - (NSArray*)nodesMatchingTraits:(IJSVGNodeTraits)traits; -- (NSSet*)childSetOfType:(IJSVGNodeType)type; -- (NSArray*)childrenOfType:(IJSVGNodeType)type; +- (NSSet*)childrenOfType:(IJSVGNodeType)type; @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.m index 52f03057..e0822d6f 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGGroup.m @@ -88,17 +88,7 @@ - (BOOL)childrenMatchTraits:(IJSVGNodeTraits)traits return YES; } -- (NSArray*)childrenOfType:(IJSVGNodeType)type { - NSMutableArray* nodes = [[NSMutableArray alloc] init]; - for(IJSVGNode* node in self.children) { - if(node.type == type) { - [nodes addObject:node]; - } - } - return nodes; -} - -- (NSSet*)childSetOfType:(IJSVGNodeType)type +- (NSSet*)childrenOfType:(IJSVGNodeType)type { NSMutableSet* nodes = [[NSMutableSet alloc] init]; for(IJSVGNode* node in self.children) { diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGImage.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGImage.m index 6216874c..09a91c65 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGImage.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGImage.m @@ -52,28 +52,15 @@ - (void)loadFromString:(NSString*)encodedString - (void)loadFromURL:(NSURL*)aURL { - // If we are not a data URL, lets check what its trying to reach is actually - // reachable, if not, just return as cant load it. - if (![aURL.scheme isEqualToString:@"data"] && - ![aURL checkResourceIsReachableAndReturnError:nil]) { -#if DEBUG - NSLog(@"<%@> references: \"%@\", which cannot be reached.", - NSStringFromClass(self.class), aURL); -#endif - return; - } - - // Convert to data, if its nil, just return, nothing more can do. NSData* data = [NSData dataWithContentsOfURL:aURL]; + + // no data, just ignore...invalid probably if(data == nil) { return; } - // set the image against the container — only if it was created from the data. + // set the image against the container NSImage* anImage = [[NSImage alloc] initWithData:data]; - if (anImage == nil) { - return; - } [self setImage:anImage]; } @@ -87,12 +74,16 @@ - (void)setImage:(NSImage*)anImage CGImage = nil; } +#if TARGET_OS_IOS + CGImage = [_image CGImage]; +#else CGRect rect = CGRectMake(0.f, 0.f, _intrinsicSize.width, _intrinsicSize.height); CGImage = [_image CGImageForProposedRect:&rect context:nil hints:nil]; +#endif CGImageRetain(CGImage); } diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGLinearGradient.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGLinearGradient.m index 19acc016..de49ff1c 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGLinearGradient.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGLinearGradient.m @@ -7,6 +7,7 @@ // #import +#import #import #import @@ -26,24 +27,24 @@ + (IJSVGBitFlags*)allowedAttributes + (void)parseGradient:(NSXMLElement*)element gradient:(IJSVGLinearGradient*)aGradient { - // Work out x1, x2, y1, y2 - NSDictionary *dict = @{ - IJSVGAttributeX1: @"0", - IJSVGAttributeX2: @"100%", - IJSVGAttributeY1: @"0", - IJSVGAttributeY2: @"0", - }; + // just ask unit for the value + NSString* x1 = ([element attributeForName:IJSVGAttributeX1].stringValue ?: @"0"); + NSString* x2 = ([element attributeForName:IJSVGAttributeX2].stringValue ?: @"100%"); + NSString* y1 = ([element attributeForName:IJSVGAttributeY1].stringValue ?: @"0"); + NSString* y2 = ([element attributeForName:IJSVGAttributeY2].stringValue ?: @"0"); + aGradient.x1 = [IJSVGGradientUnitLength unitWithString:x1 fromUnitType:aGradient.units]; + aGradient.x2 = [IJSVGGradientUnitLength unitWithString:x2 fromUnitType:aGradient.units]; + aGradient.y1 = [IJSVGGradientUnitLength unitWithString:y1 fromUnitType:aGradient.units]; + aGradient.y2 = [IJSVGGradientUnitLength unitWithString:y2 fromUnitType:aGradient.units]; - for (NSString* key in dict) { - NSString *value = [element attributeForName:key].stringValue ?: dict[key]; - IJSVGUnitLength *length = [IJSVGUnitLength unitWithString:value - fromUnitType:aGradient.units]; - length = length ?: [IJSVGUnitLength unitWithString:dict[key] - fromUnitType:aGradient.units]; - [aGradient setValue:length - forKey:key]; + // parse spreadMethod + NSString* spreadMethod = [element attributeForName:@"spreadMethod"].stringValue; + if([spreadMethod isEqualToString:@"reflect"]) { + aGradient.spreadMethod = IJSVGSpreadMethodReflect; + } else if([spreadMethod isEqualToString:@"repeat"]) { + aGradient.spreadMethod = IJSVGSpreadMethodRepeat; } - + // compute the color stops and colours NSArray* colors = nil; CGFloat* stopsParams = [self.class computeColorStops:aGradient @@ -86,10 +87,72 @@ - (void)drawInContextRef:(CGContextRef)ctx IJSVGConcatTransformsCTM(ctx, self.transforms); // draw the gradient - CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + if(self.spreadMethod == IJSVGSpreadMethodPad) { + // Default: extend with last color before/after + CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawLinearGradient(ctx, self.CGGradient, gradientStartPoint, + gradientEndPoint, options); + } else { + // Reflect or Repeat: tile the gradient by drawing it multiple times + // with reflected/repeated start/end points + CGFloat dx = gradientEndPoint.x - gradientStartPoint.x; + CGFloat dy = gradientEndPoint.y - gradientStartPoint.y; + CGFloat gradLen = sqrt(dx * dx + dy * dy); + if(gradLen < 0.001) { + CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; + CGContextDrawLinearGradient(ctx, self.CGGradient, gradientStartPoint, + gradientEndPoint, options); + } else { + // Determine how many tiles we need to cover the clip bounds + CGRect clipBounds = CGContextGetClipBoundingBox(ctx); + CGFloat maxDim = MAX(CGRectGetWidth(clipBounds), CGRectGetHeight(clipBounds)); + int tiles = (int)(maxDim / gradLen) + 2; + + CGGradientRef grad = self.CGGradient; + CGGradientRef reversedGrad = NULL; + + if(self.spreadMethod == IJSVGSpreadMethodReflect) { + // Create a reversed gradient for alternating tiles + NSUInteger nStops = self.numberOfStops; + CGFloat* revLocations = (CGFloat*)malloc(nStops * sizeof(CGFloat)); + CGFloat* revComponents = (CGFloat*)malloc(nStops * 4 * sizeof(CGFloat)); + for(NSUInteger i = 0; i < nStops; i++) { + revLocations[i] = 1.0 - self.locations[nStops - 1 - i]; + NSColor* color = [IJSVGColor computeColorSpace:self.colors[nStops - 1 - i]]; + CGFloat r = 0.f; + CGFloat g = 0.f; + CGFloat b = 0.f; + CGFloat a = 0.f; + IJSVGColorGetRGBAComponents(color, &r, &g, &b, &a); + revComponents[i * 4 + 0] = r; + revComponents[i * 4 + 1] = g; + revComponents[i * 4 + 2] = b; + revComponents[i * 4 + 3] = a; + } + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + reversedGrad = CGGradientCreateWithColorComponents(cs, revComponents, revLocations, nStops); + CGColorSpaceRelease(cs); + free(revLocations); + free(revComponents); + } - CGContextDrawLinearGradient(ctx, self.CGGradient, gradientStartPoint, - gradientEndPoint, options); + for(int t = -tiles; t <= tiles; t++) { + CGPoint tileStart = CGPointMake(gradientStartPoint.x + dx * t, + gradientStartPoint.y + dy * t); + CGPoint tileEnd = CGPointMake(gradientStartPoint.x + dx * (t + 1), + gradientStartPoint.y + dy * (t + 1)); + + BOOL useReverse = (self.spreadMethod == IJSVGSpreadMethodReflect) && (abs(t) % 2 == 1); + CGGradientRef tileGrad = useReverse ? reversedGrad : grad; + + CGContextDrawLinearGradient(ctx, tileGrad, tileStart, tileEnd, 0); + } + + if(reversedGrad != NULL) { + CGGradientRelease(reversedGrad); + } + } + } } @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGMask.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGMask.h index ee57864a..c01d89ce 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGMask.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGMask.h @@ -10,4 +10,6 @@ @interface IJSVGMask : IJSVGGroup +@property (nonatomic, assign) BOOL usesAlphaMask; + @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.h index 1fa0a64b..762481c7 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.h @@ -10,8 +10,8 @@ #import #import #import -#import -#import +#import +#import @class IJSVGNode; @class IJSVG; @@ -92,6 +92,8 @@ typedef NS_ENUM(NSInteger, IJSVGNodeAttribute) { IJSVGNodeAttributeFilter, IJSVGNodeAttributeStdDeviation, IJSVGNodeAttributeIn, + IJSVGNodeAttributeIn2, + IJSVGNodeAttributeResult, IJSVGNodeAttributeEdgeMode, IJSVGNodeAttributeMarker }; diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.m index b4c87ce5..058de8a3 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGNode.m @@ -121,7 +121,20 @@ + (IJSVGNodeType)typeForString:(NSString*)string if(IJSVGCharBufferCaseInsensitiveCompare(nodeType, "filter") == YES) { return IJSVGNodeTypeFilter; } - if(IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fegaussianblur") == YES) { + if(IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fegaussianblur") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fecolormatrix") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "feflood") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "feoffset") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fecomposite") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "femerge") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "femergenode") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "feblend") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "femorphology") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fecomponenttransfer") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "feturbulence") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fedisplacementmap") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fespecularlighting") == YES || + IJSVGCharBufferCaseInsensitiveCompare(nodeType, "fediffuselighting") == YES) { return IJSVGNodeTypeFilterEffect; } return IJSVGNodeTypeUnknown; diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGPath.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGPath.m index 11d4863f..1f56b8e7 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGPath.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGPath.m @@ -8,6 +8,7 @@ #import #import +#import @implementation IJSVGPath @@ -38,18 +39,27 @@ + (void)recursivelyAddPathedNodesPaths:(NSArray*)nodes transform:(CGAffineTransform)transform toPath:(CGMutablePathRef)mutPath { - for(IJSVGPath* pathNode in nodes) { + for(IJSVGNode* pathNode in nodes) { + // Compute effective transform including the node's own transforms + // (important for inside ) + CGAffineTransform nodeTransform = transform; + if(pathNode.transforms.count > 0) { + for(IJSVGTransform* t in pathNode.transforms) { + nodeTransform = CGAffineTransformConcat(t.CGAffineTransform, nodeTransform); + } + } + // just a pathed node if([pathNode matchesTraits:IJSVGNodeTraitPathed] == YES) { - CGPathAddPath(mutPath, &transform, pathNode.path); + CGPathAddPath(mutPath, &nodeTransform, ((IJSVGPath*)pathNode).path); continue; } - + // could be a use group if([pathNode isKindOfClass:IJSVGGroup.class] == YES) { IJSVGGroup* useGroup = (IJSVGGroup*)pathNode; [self recursivelyAddPathedNodesPaths:useGroup.children - transform:transform + transform:nodeTransform toPath:mutPath]; } } @@ -130,7 +140,7 @@ - (void)computeTraits // component to then remove the trait if its 0.f if([self.stroke isKindOfClass:IJSVGColorNode.class] == YES) { IJSVGColorNode* strokeColor = (IJSVGColorNode*)self.stroke; - if(strokeColor.color.alphaComponent == 0.f || + if(IJSVGColorAlphaComponent(strokeColor.color) == 0.f || strokeColor.isNoneOrTransparent == YES) { [self removeTraits:IJSVGNodeTraitStroked]; } diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRadialGradient.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRadialGradient.m index f66ebf20..56515926 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRadialGradient.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRadialGradient.m @@ -51,9 +51,10 @@ + (void)parseGradient:(NSXMLElement*)element if(str != nil) { unit = [IJSVGUnitLength unitWithString:str fromUnitType:gradient.units]; + } else { + // spec says to say 50% for missing property default + unit = [IJSVGUnitLength unitWithPercentageFloat:.5f]; } - // spec says to say 50% for missing property default - unit = unit ?: [IJSVGUnitLength unitWithPercentageFloat:.5f]; [gradient setValue:unit forKey:kv[key]]; } @@ -63,9 +64,9 @@ + (void)parseGradient:(NSXMLElement*)element if(fr != nil) { gradient.fr = [IJSVGUnitLength unitWithString:fr fromUnitType:gradient.units]; + } else { + gradient.fr = [IJSVGUnitLength unitWithPercentageFloat:0.f]; } - - gradient.fr = gradient.fr ?: [IJSVGUnitLength unitWithPercentageFloat:0.f]; // fx and fy are the same unless specified otherwise gradient.fx = gradient.cx; @@ -75,13 +76,20 @@ + (void)parseGradient:(NSXMLElement*)element NSString* fx = [element attributeForName:IJSVGAttributeFX].stringValue; if(fx != nil) { gradient.fx = [IJSVGUnitLength unitWithString:fx - fromUnitType:gradient.units] ?: gradient.fx; + fromUnitType:gradient.units]; } NSString* fy = [element attributeForName:IJSVGAttributeFY].stringValue; if(fy != nil) { gradient.fy = [IJSVGUnitLength unitWithString:fy - fromUnitType:gradient.units] ?: gradient.fy; + fromUnitType:gradient.units]; + } + + NSString* spreadMethod = [element attributeForName:@"spreadMethod"].stringValue; + if([spreadMethod isEqualToString:@"reflect"]) { + gradient.spreadMethod = IJSVGSpreadMethodReflect; + } else if([spreadMethod isEqualToString:@"repeat"]) { + gradient.spreadMethod = IJSVGSpreadMethodRepeat; } NSArray* colors = nil; @@ -140,12 +148,14 @@ - (void)drawInContextRef:(CGContextRef)ctx // concat the gradient transform into the context IJSVGConcatTransformsCTM(ctx, self.transforms); - // draw the gradient + // WebKit currently pads radial gradients even when spreadMethod is + // `reflect` or `repeat`. The renderer comparisons in this target use + // WebKit as the reference image, so match that behavior here. CGGradientDrawingOptions options = kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation; CGContextDrawRadialGradient(ctx, self.CGGradient, - gradientEndPoint, focalRadius, - gradientStartPoint, - radius, options); + gradientEndPoint, focalRadius, + gradientStartPoint, + radius, options); CGContextRestoreGState(ctx); } diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.h b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.h index 1e8ea0ab..72394e3c 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.h +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.h @@ -12,12 +12,9 @@ @interface IJSVGRootNode : IJSVGGroup -@property (nonatomic, assign) CGSize clientSize; -@property (nonatomic, assign) BOOL viewBoxContainsRelativeUnits; @property (nonatomic, assign) IJSVGIntrinsicDimensions intrinsicDimensions; @property (nonatomic, strong) IJSVGUnitSize* intrinsicSize; +@property (nonatomic, assign) BOOL hasExplicitViewBox; @property (nonatomic, readonly) CGRect bounds; -- (void)inferViewBoxIfRequired; - @end diff --git a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.m b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.m index 66dadfbb..468d9d8e 100644 --- a/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.m +++ b/Framework/IJSVG/IJSVG/Source/Nodes/IJSVGRootNode.m @@ -38,26 +38,6 @@ - (instancetype)init return self; } -- (void)inferViewBoxIfRequired -{ - // Already have a viewBox, no need to do anything - if(self.viewBox != nil) { - return; - } - - // We only need to do the following if the FF is enabled. - if(IJSVGThreadManager.currentManager.featureFlags.inferViewBoxes.enabled == NO) { - return; - } - - // We can just union our child bounds together and use that. - CGRect rect = [super bounds]; - if(CGRectIsInfinite(rect) || CGRectIsEmpty(rect)) { - return; - } - self.viewBox = [IJSVGUnitRect rectWithCGRect:rect]; -} - - (CGRect)bounds { return [self.viewBox computeValue:CGSizeZero]; @@ -66,7 +46,7 @@ - (CGRect)bounds - (IJSVGRootNode *)rootNode { IJSVGRootNode* rootNode = nil; - if((rootNode = super.rootNode) == self) { + if((rootNode = [super rootNode]) == self) { IJSVGNode* parent = self.parentNode; if([parent isKindOfClass:IJSVGRootNode.class]) { return (IJSVGRootNode*)parent; diff --git a/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.h b/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.h index 494a213e..5c47e65f 100644 --- a/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.h +++ b/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.h @@ -25,10 +25,10 @@ #import #import #import +#import #import #import -#import -#import +#import typedef void (^IJSVGNodeParserPostProcessBlock)(void); @@ -127,17 +127,13 @@ void IJSVGParserMallocBuffersFree(IJSVGParserMallocBuffers* buffers); IJSVGStyleSheet* _styleSheet; NSMutableDictionary* _detachedReferences; IJSVGThreadManager* _threadManager; - CGSize _rootSize; - IJSVGRootNode* _rootNode; - NSURL* _fileURL; } -@property (nonatomic, assign) CGSize defaultSize; +@property (nonatomic, strong, readonly) IJSVGRootNode* rootNode; + (BOOL)isDataSVG:(NSData*)data; - (id)initWithSVGString:(NSString*)string - fileURL:(NSURL*)fileURL error:(NSError**)error; - (id)initWithFileURL:(NSURL*)aURL @@ -146,6 +142,4 @@ void IJSVGParserMallocBuffersFree(IJSVGParserMallocBuffers* buffers); + (IJSVGParser*)parserForFileURL:(NSURL*)aURL error:(NSError**)error; -- (IJSVGRootNode*)rootNodeWithSize:(CGSize)size; - @end diff --git a/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.m b/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.m index 9dec1434..1105016f 100644 --- a/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.m +++ b/Framework/IJSVG/IJSVG/Source/Parsing/IJSVGParser.m @@ -11,6 +11,8 @@ #import #import #import +#import + NSString* const IJSVGStringObjectBoundingBox = @"objectBoundingBox"; NSString* const IJSVGStringUserSpaceOnUse = @"userSpaceOnUse"; @@ -83,10 +85,278 @@ NSString* const IJSVGAttributeHref = @"href"; NSString* const IJSVGAttributeOverflow = @"overflow"; NSString* const IJSVGAttributeFilter = @"filter"; +NSString* const IJSVGAttributeFilterUnits = @"filterUnits"; NSString* const IJSVGAttributeStdDeviation = @"stdDeviation"; NSString* const IJSVGAttributeIn = @"in"; NSString* const IJSVGAttributeEdgeMode = @"edgeMode"; NSString* const IJSVGAttributeMarker = @"marker"; +NSString* const IJSVGAttributePrimitiveUnits = @"primitiveUnits"; +NSString* const IJSVGAttributeColorInterpolationFilters = @"color-interpolation-filters"; + +static const CGFloat kIJSVGDefaultRootViewportWidth = 300.f; +static const CGFloat kIJSVGDefaultRootViewportHeight = 150.f; +NSString* const IJSVGAttributeIn2 = @"in2"; +NSString* const IJSVGAttributeResult = @"result"; + +static NSString* IJSVGTextPropertyForElement(NSXMLElement* element, NSString* propertyName) +{ + NSString* value = [element attributeForName:propertyName].stringValue; + NSString* styleString = [element attributeForName:IJSVGAttributeStyle].stringValue; + if(styleString.length != 0) { + IJSVGStyleSheetStyle* style = [IJSVGStyleSheetStyle parseStyleString:styleString]; + NSString* styleValue = [style property:propertyName]; + if(styleValue.length != 0) { + value = styleValue; + } + } + return value; +} + +static CGFloat IJSVGTextFirstFloat(NSString* string, CGFloat fallback) +{ + if(string.length == 0) { + return fallback; + } + NSScanner* scanner = [NSScanner scannerWithString:string]; + double value = 0.f; + if([scanner scanDouble:&value] == NO) { + return fallback; + } + return (CGFloat)value; +} + +static BOOL IJSVGTextStringIsItalic(NSString* string) +{ + NSString* value = string.lowercaseString; + return [value containsString:@"italic"] || [value containsString:@"oblique"]; +} + +static BOOL IJSVGTextStringIsBold(NSString* string) +{ + NSString* value = string.lowercaseString; + if([value isEqualToString:@"bold"] || [value isEqualToString:@"bolder"]) { + return YES; + } + if(value.length != 0 && value.floatValue >= 600.f) { + return YES; + } + return NO; +} + +static NSArray* IJSVGTextFontNamesForFamily(NSString* family, CGFloat fontSize) +{ + if(family.length == 0) { + return @[]; + } + + NSMutableArray* names = [[NSMutableArray alloc] init]; + NSArray* parts = [family componentsSeparatedByString:@","]; + for(NSString* part in parts) { + NSString* trimmed = [part stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if(trimmed.length == 0) { + continue; + } + + if(([trimmed hasPrefix:@"\""] && [trimmed hasSuffix:@"\""]) || + ([trimmed hasPrefix:@"'"] && [trimmed hasSuffix:@"'"])) { + trimmed = [trimmed substringWithRange:NSMakeRange(1, trimmed.length - 2)]; + } + + if(trimmed.length == 0) { + continue; + } + + NSString* lower = trimmed.lowercaseString; + if([lower isEqualToString:@"serif"]) { + [names addObject:@"Times New Roman"]; + [names addObject:@"Times"]; + continue; + } + if([lower isEqualToString:@"sans-serif"]) { + [names addObject:[NSFont systemFontOfSize:fontSize].fontName]; + [names addObject:@"Helvetica"]; + continue; + } + if([lower isEqualToString:@"monospace"]) { + if(@available(macOS 10.15, *)) { +#if TARGET_OS_IOS + CGFloat monospacedWeight = UIFontWeightRegular; +#else + CGFloat monospacedWeight = NSFontWeightRegular; +#endif + [names addObject:[NSFont monospacedSystemFontOfSize:fontSize + weight:monospacedWeight].fontName]; + } +#if !TARGET_OS_IOS + else { + [names addObject:[NSFont userFixedPitchFontOfSize:fontSize].fontName]; + } +#endif + [names addObject:@"Menlo"]; + [names addObject:@"Courier"]; + continue; + } + if([lower isEqualToString:@"system-ui"]) { + [names addObject:[NSFont systemFontOfSize:fontSize].fontName]; + continue; + } + + [names addObject:trimmed]; + } + return names; +} + +static CTFontRef IJSVGTextCreateFontForElement(NSXMLElement* element) CF_RETURNS_RETAINED +{ + CGFloat fontSize = IJSVGTextFirstFloat(IJSVGTextPropertyForElement(element, @"font-size"), 16.f); + if(fontSize <= 0.f) { + fontSize = 16.f; + } + + NSString* family = IJSVGTextPropertyForElement(element, @"font-family"); + NSString* fontStyle = IJSVGTextPropertyForElement(element, @"font-style"); + NSString* fontWeight = IJSVGTextPropertyForElement(element, @"font-weight"); + + CTFontRef font = NULL; + if(family.length != 0) { + for(NSString* fontName in IJSVGTextFontNamesForFamily(family, fontSize)) { + font = CTFontCreateWithName((CFStringRef)fontName, fontSize, NULL); + if(font != NULL) { + break; + } + } + } + if(font == NULL) { + NSFont* systemFont = [NSFont systemFontOfSize:fontSize]; + font = CTFontCreateWithName((CFStringRef)systemFont.fontName, fontSize, NULL); + } + + CTFontSymbolicTraits traits = 0; + if(IJSVGTextStringIsItalic(fontStyle) == YES) { + traits |= kCTFontItalicTrait; + } + if(IJSVGTextStringIsBold(fontWeight) == YES) { + traits |= kCTFontBoldTrait; + } + if(traits != 0 && font != NULL) { + CTFontRef traitedFont = CTFontCreateCopyWithSymbolicTraits(font, + fontSize, + NULL, + traits, + traits); + if(traitedFont != NULL) { + CFRelease(font); + font = traitedFont; + } + } + return font; +} + +static NSString* IJSVGTextContentForElement(NSXMLElement* element) +{ + NSMutableString* text = [[NSMutableString alloc] init]; + for(NSXMLNode* child in element.children) { + if(child.kind == NSXMLTextKind) { + if(child.stringValue.length != 0) { + [text appendString:child.stringValue]; + } + continue; + } + if(child.kind == NSXMLElementKind) { + NSString* childText = IJSVGTextContentForElement((NSXMLElement*)child); + if(childText.length != 0) { + [text appendString:childText]; + } + } + } + if(text.length != 0) { + return text; + } + return element.stringValue ?: @""; +} + +static CGMutablePathRef IJSVGTextCreatePathForElement(NSXMLElement* element, NSString* text) CF_RETURNS_RETAINED +{ + if(text.length == 0) { + return nil; + } + CTFontRef font = IJSVGTextCreateFontForElement(element); + if(font == NULL) { + return nil; + } + + // x/y are applied by the existing IJSVG transform pipeline (node.x/node.y), + // so keep glyph paths in local coordinates to avoid double translation. + CGFloat x = 0.f; + CGFloat y = 0.f; + + NSDictionary* attributes = @{ + (__bridge id)kCTFontAttributeName: (__bridge id)font + }; + NSAttributedString* attributedString = nil; + attributedString = [[NSAttributedString alloc] initWithString:text + attributes:attributes]; + CTLineRef line = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)attributedString); + CGMutablePathRef textPath = CGPathCreateMutable(); + + CFArrayRef runs = CTLineGetGlyphRuns(line); + CFIndex runCount = CFArrayGetCount(runs); + for(CFIndex runIndex = 0; runIndex < runCount; runIndex++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, runIndex); + CFIndex glyphCount = CTRunGetGlyphCount(run); + if(glyphCount == 0) { + continue; + } + + CGGlyph* glyphs = (CGGlyph*)calloc((size_t)glyphCount, sizeof(CGGlyph)); + CGPoint* positions = (CGPoint*)calloc((size_t)glyphCount, sizeof(CGPoint)); + if(glyphs == NULL || positions == NULL) { + if(glyphs != NULL) { + (void)free(glyphs), glyphs = NULL; + } + if(positions != NULL) { + (void)free(positions), positions = NULL; + } + continue; + } + + CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CTRunGetPositions(run, CFRangeMake(0, 0), positions); + + NSDictionary* runAttributes = (__bridge NSDictionary*)CTRunGetAttributes(run); + CTFontRef runFont = (__bridge CTFontRef)runAttributes[(__bridge id)kCTFontAttributeName]; + if(runFont == NULL) { + runFont = font; + } + + for(CFIndex glyphIndex = 0; glyphIndex < glyphCount; glyphIndex++) { + CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyphs[glyphIndex], NULL); + if(glyphPath == NULL) { + continue; + } + CGAffineTransform transform = CGAffineTransformMake(1.f, + 0.f, + 0.f, + -1.f, + x + positions[glyphIndex].x, + y + positions[glyphIndex].y); + CGPathAddPath(textPath, &transform, glyphPath); + CGPathRelease(glyphPath); + } + + (void)free(glyphs), glyphs = NULL; + (void)free(positions), positions = NULL; + } + + CFRelease(line); + CFRelease(font); + + if(CGPathIsEmpty(textPath) == YES) { + CGPathRelease(textPath); + return nil; + } + return textPath; +} @implementation IJSVGParser @@ -118,13 +388,9 @@ + (IJSVGParser*)parserForFileURL:(NSURL*)aURL } - (id)initWithSVGString:(NSString*)string - fileURL:(NSURL*)aURL error:(NSError**)error { if((self = [super init]) != nil) { - // just some generic value to get it up n running. - _fileURL = aURL; - _defaultSize = CGSizeMake(200.f, 200.f); // use NSXMLDocument as its the easiest thing to do on OSX NSError* anError = nil; @@ -142,12 +408,19 @@ - (id)initWithSVGString:(NSString*)string error:error]; } + // attempt to parse the file + [self begin]; + // check the actual parsed SVG anError = nil; if([self _validateParse:&anError] == NO) { *error = anError; return nil; } + + // we have actually finished with the document at this point + // so just get rid of it + _document = nil; } return self; } @@ -181,7 +454,6 @@ - (id)initWithFileURL:(NSURL*)aURL } return [self initWithSVGString:str - fileURL:aURL error:error]; } @@ -209,36 +481,7 @@ - (BOOL)_validateParse:(NSError**)error return YES; } -- (IJSVGRootNode*)rootNodeWithSize:(CGSize)size -{ - __weak IJSVGParser* weakSelf = self; - [self beginWithSetup:^{ - // if we have passed in a value that is not zero, we can just set it to that - // else we need to compute it, as we can treat zero as auto. - IJSVGParser* strongSelf = weakSelf; - CGSize computeSize = size; - if(!CGSizeEqualToSize(CGSizeZero, computeSize)) { - strongSelf->_rootSize = size; - return; - } - - // compute the value, if the value is still a nil size, we just need to - // fallback to some generic value, which is against this object. - IJSVGRootNode* node = [self rootNode:NO]; - if(node.viewBox == nil) { - strongSelf->_rootSize = strongSelf->_defaultSize; - return; - } - - // at this point we can just compute it again from the viewBox size. - computeSize = [node.viewBox.size computeValue:CGSizeZero]; - strongSelf->_rootSize = CGSizeEqualToSize(CGSizeZero, computeSize) ? - strongSelf->_defaultSize : computeSize; - }]; - return _rootNode; -} - -- (void)beginWithSetup:(dispatch_block_t __nullable)setup +- (void)begin { // setup basics to begin with _styleSheet = [[IJSVGStyleSheet alloc] init]; @@ -246,17 +489,17 @@ - (void)beginWithSetup:(dispatch_block_t __nullable)setup _threadManager = manager; _commandDataStream = manager.pathDataStream; _detachedReferences = [[NSMutableDictionary alloc] init]; - if(setup != nil) { - setup(); - } + + // Pre-scan all elements for IDs to support forward references + // (e.g., in referencing elements defined after ) + [self prescanElementIDs:_document.rootElement]; + _rootNode = [[IJSVGRootNode alloc] init]; - _rootNode.clientSize = _rootSize; IJSVGNodeParserPostProcessBlock postProcessBlock = nil; [self parseSVGElement:_document.rootElement ontoNode:_rootNode parentNode:nil - postProcessBlock:&postProcessBlock - recursive:YES]; + postProcessBlock:&postProcessBlock]; if(postProcessBlock != nil) { postProcessBlock(); } @@ -264,23 +507,6 @@ - (void)beginWithSetup:(dispatch_block_t __nullable)setup _detachedReferences = nil; } -- (IJSVGRootNode*)rootNode:(BOOL)recursive -{ - IJSVGNodeParserPostProcessBlock postProcessBlock = nil; - IJSVGRootNode* node = [[IJSVGRootNode alloc] init]; - node.clientSize = _rootSize; - [self parseSVGElement:_document.rootElement - ontoNode:node - parentNode:nil - postProcessBlock:&postProcessBlock - recursive:recursive]; - if(postProcessBlock != nil) { - postProcessBlock(); - } - [node postProcess]; - return node; -} - - (void)computeDefsForElement:(NSXMLElement*)element parentNode:(IJSVGNode*)parentNode { @@ -298,32 +524,23 @@ - (void)computeDefsForElement:(NSXMLElement*)element - (void)computeViewBoxForRootNode:(IJSVGRootNode*)node { - if(node.viewBox == nil) { - IJSVGUnitLength* width = node.width; - IJSVGUnitLength* height = node.height; - - CGFloat cw = [width computeValue:_rootSize.width]; - CGFloat ch = [height computeValue:_rootSize.height]; - - if(ch == 0.f && cw != 0.f) { - ch = cw; - } else if(cw == 0.f && ch != 0.f) { - cw = ch; + if(node.hasExplicitViewBox == NO) { + CGFloat width = node.width.value; + CGFloat height = node.height.value; + + if(height == 0.f && width != 0.f) { + height = kIJSVGDefaultRootViewportHeight; + } else if(width == 0.f && height != 0.f) { + width = kIJSVGDefaultRootViewportWidth; } - - // Lets infer the size if the width and height are zero. - if(cw == 0.f && ch == 0.f) { - [node inferViewBoxIfRequired]; - } else { - IJSVGUnitSize* size = [IJSVGUnitSize sizeWithWidth:width - height:height]; - node.viewBox = [IJSVGUnitRect rectWithOrigin:IJSVGUnitPoint.zeroPoint - size:size]; + + if(width == 0.f && height == 0.f) { + width = kIJSVGDefaultRootViewportWidth; + height = kIJSVGDefaultRootViewportHeight; } - } - - if(node.viewBox == nil) { - return; + node.viewBox = [IJSVGUnitRect rectWithX:0.f y:0.f + width:width + height:height]; } IJSVGIntrinsicDimensions dimensions = IJSVGIntrinsicDimensionNone; @@ -337,25 +554,11 @@ - (void)computeViewBoxForRootNode:(IJSVGRootNode*)node dimensions |= IJSVGIntrinsicDimensionHeight; hl = node.height; } - - // make note if we are using relative units for the width or height. - if(wl.type == IJSVGUnitLengthTypePercentage || - hl.type == IJSVGUnitLengthTypePercentage) { - node.viewBoxContainsRelativeUnits = YES; - } // store the width and height node.intrinsicDimensions = dimensions; - - // compute the new width and height based on the passed in size as the fall - // back for all the percentage values. - CGSize computedSize = CGSizeMake([wl computeValue:_rootSize.width], - [hl computeValue:_rootSize.height]); - node.intrinsicSize = [IJSVGUnitSize sizeWithCGSize:computedSize]; - - // compute the viewbox - CGRect computedViewBox = [node.viewBox computeValue:_rootSize]; - node.viewBox.size = [IJSVGUnitSize sizeWithCGSize:computedViewBox.size]; + node.intrinsicSize = [IJSVGUnitSize sizeWithWidth:wl + height:hl]; } - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)element @@ -393,8 +596,14 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e [ignoringAttributes bitIsSet:IJSVGNodeAttributeID] == NO) { IJSVGAttributeParse(IJSVGAttributeID, ^(NSString* value) { node.identifier = value; - [self detachElement:element - withIdentifier:value]; + // Preserve the original source element for detached references. + // Merged xlink elements can carry the same id on a different tag + // name (for example, a radialGradient merged from a linearGradient + // template), and overwriting the map corrupts subsequent lookups. + if([self detachedElementWithIdentifier:value] == nil) { + [self detachElement:element + withIdentifier:value]; + } }); } @@ -505,10 +714,18 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e NSString* identifier = [IJSVGUtils defURL:value]; if(identifier != nil) { node.mask = (id)[self computeDetachedNodeWithIdentifier:identifier - referencingNode:node - element:element]; + referencingNode:node]; } }); + + if(node.mask == nil) { + NSString* cssMaskValue = [nodeStyle property:@"mask-image"] ?: [nodeStyle property:@"mask"]; + NSString* identifier = cssMaskValue.length != 0 ? [IJSVGUtils defURL:cssMaskValue] : nil; + if(identifier != nil) { + node.mask = (id)[self computeDetachedNodeWithIdentifier:identifier + referencingNode:node]; + } + } } // clip path @@ -518,8 +735,7 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e NSString* identifier = [IJSVGUtils defURL:value]; if(identifier != nil) { node.clipPath = (id)[self computeDetachedNodeWithIdentifier:identifier - referencingNode:node - element:element]; + referencingNode:node]; } }); } @@ -650,8 +866,7 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e NSString* fillIdentifier = [IJSVGUtils defURL:value]; if(fillIdentifier != nil) { IJSVGNode* object = [self computeDetachedNodeWithIdentifier:fillIdentifier - referencingNode:node - element:element]; + referencingNode:node]; node.stroke = object; return; } @@ -689,8 +904,7 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e NSString* fillIdentifier = [IJSVGUtils defURL:value]; if(fillIdentifier != nil) { IJSVGNode* object = [self computeDetachedNodeWithIdentifier:fillIdentifier - referencingNode:node - element:element]; + referencingNode:node]; node.fill = object; return; } @@ -799,6 +1013,9 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e y:floats[1] width:floats[2] height:floats[3]]; + if([node isKindOfClass:IJSVGRootNode.class] == YES) { + ((IJSVGRootNode *)node).hasExplicitViewBox = YES; + } ((void)free(floats)), floats = NULL; }); } @@ -820,8 +1037,7 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e NSString* filterIdentifier = [IJSVGUtils defURL:value]; if(filterIdentifier != nil) { IJSVGNode* filter = [self computeDetachedNodeWithIdentifier:filterIdentifier - referencingNode:node - element:element]; + referencingNode:node]; node.filter = (IJSVGFilter*)filter; } }); @@ -829,20 +1045,31 @@ - (IJSVGNodeParserPostProcessBlock)computeAttributesFromElement:(NSXMLElement*)e if(_threadManager.featureFlags.filters.enabled == YES && node.type == IJSVGNodeTypeFilterEffect) { IJSVGFilterEffect* effect = (IJSVGFilterEffect*)node; - - // in + + // in (store as inputName for graph routing) IJSVGAttributeParse(IJSVGAttributeIn, ^(NSString* value) { + effect.inputName = value; effect.source = [IJSVGFilterEffect sourceForString:value]; if(effect.source == IJSVGFilterEffectSourcePrimitiveReference) { effect.primitiveReference = value; } }); - + + // in2 + IJSVGAttributeParse(IJSVGAttributeIn2, ^(NSString* value) { + effect.inputName2 = value; + }); + + // result + IJSVGAttributeParse(IJSVGAttributeResult, ^(NSString* value) { + effect.resultName = value; + }); + // edge mode IJSVGAttributeParse(IJSVGAttributeEdgeMode, ^(NSString* value) { effect.edgeMode = [IJSVGFilterEffect edgeModeForString:value]; }); - + // deviation IJSVGAttributeParse(IJSVGAttributeStdDeviation, ^(NSString* value) { effect.stdDeviation = [IJSVGUnitLength unitWithString:value]; @@ -997,6 +1224,13 @@ - (IJSVGNode*)parseElement:(NSXMLElement*)element postProcessBlock:&postProcessBlock]; break; } + case IJSVGNodeTypeText: + case IJSVGNodeTypeTextSpan: { + computedNode = [self parseTextElement:element + parentNode:node + postProcessBlock:&postProcessBlock]; + break; + } case IJSVGNodeTypeFilter: { if(_threadManager.featureFlags.filters.enabled == YES) { computedNode = [self parseFilterElement:element @@ -1045,6 +1279,22 @@ - (void)computeElement:(NSXMLElement*)element } #pragma mark Detaching nodes +- (void)prescanElementIDs:(NSXMLElement*)element +{ + // Recursively scan all elements and store ID → element mappings + // so forward references (e.g., in referencing content elements) work. + if(element == nil) return; + NSXMLNode* idAttr = [element attributeForName:@"id"]; + if(idAttr != nil && idAttr.stringValue.length > 0) { + _detachedReferences[idAttr.stringValue] = element; + } + for(NSXMLElement* child in element.children) { + if([child isKindOfClass:[NSXMLElement class]]) { + [self prescanElementIDs:child]; + } + } +} + - (void)detachElement:(NSXMLElement*)element withIdentifier:(NSString*)identifier { @@ -1061,49 +1311,17 @@ - (NSXMLElement*)detachedElementWithIdentifier:(NSString*)identifier - (IJSVGNode*)computeDetachedNodeWithIdentifier:(NSString*)identifier referencingNode:(IJSVGNode*)node - element:(NSXMLElement*)element { NSXMLElement* detachedElement = [self detachedElementWithIdentifier:identifier]; if(detachedElement == nil) { return nil; } - - // if we are recursive, we must return nil to prevent crashing. - if([self isElement:element decedentOf:detachedElement]) { - [self recursionDetectedOn:element - decendentOf:detachedElement - identifier:identifier]; - return nil; - } - + detachedElement = detachedElement.copy; // we need to make sure once we are done, we detach this from its parent // or it can cause recursion down the line - return [self parseElement:detachedElement - parentNode:node].detach; -} - -- (void)recursionDetectedOn:(NSXMLElement*)element - decendentOf:(NSXMLElement*)parent - identifier:(NSString*)identifier -{ - // For now, we only want to log these for debug builds whilst we fix any - // SVG's that are problematic. -#if DEBUG - NSLog(@"<%@> Recursion detected in file: \"%@\", with identifer: \"%@\"", - self.className, _fileURL ?: @"Unknown", identifier); -#endif -} - -- (BOOL)isElement:(NSXMLElement*)element - decedentOf:(NSXMLElement*)parentElement { - NSXMLElement* parent = (NSXMLElement*)element.parent; - while(parent != nil) { - if(parentElement == parent) { - return YES; - } - parent = (NSXMLElement*)parent.parent; - } - return NO; + IJSVGNode *detachedNode = [self parseElement:detachedElement + parentNode:node].detach; + return detachedNode; } - (NSXMLElement*)mergedElement:(NSXMLElement*)element @@ -1115,7 +1333,7 @@ - (NSXMLElement*)mergedElement:(NSXMLElement*)element attribute = attribute.copy; [copy addAttribute:attribute]; } - + // if we merge an element, we need to also maintain its children, if the // reference element has children and the referencing element does not, // use those else use the referencing element children. @@ -1124,12 +1342,13 @@ - (NSXMLElement*)mergedElement:(NSXMLElement*)element for(__strong NSXMLElement* child in copy.children) { [copy removeChildAtIndex:child.index]; } - + // add the new ones from the copy for(__strong NSXMLElement* child in element.children) { [copy addChild:child.copy]; } } + return copy; } @@ -1152,7 +1371,24 @@ - (IJSVGNode*)parseFilterElement:(NSXMLElement*)element *postProcessBlock = [self computeAttributesFromElement:element onNode:node ignoredAttributes:nil]; - + + NSString* filterUnits = [element attributeForName:IJSVGAttributeFilterUnits].stringValue; + if(filterUnits.length != 0) { + node.units = [IJSVGUtils unitTypeForString:filterUnits]; + } + + NSString* primitiveUnits = [element attributeForName:IJSVGAttributePrimitiveUnits].stringValue; + if(primitiveUnits.length != 0) { + node.contentUnits = [IJSVGUtils unitTypeForString:primitiveUnits]; + } + + NSString* colorInterpolationFilters = [element attributeForName:IJSVGAttributeColorInterpolationFilters].stringValue; + if([colorInterpolationFilters caseInsensitiveCompare:@"sRGB"] == NSOrderedSame) { + node.usesSRGBColorInterpolation = YES; + } + + node.defElement = (NSXMLElement*)[element copy]; + [self computeElement:element parentNode:node]; return node; @@ -1171,11 +1407,44 @@ - (IJSVGNode*)parseFilterEffectElement:(NSXMLElement*)element IJSVGGroup* group = (IJSVGGroup*)parentNode; [group addChild:node]; } - + *postProcessBlock = [self computeAttributesFromElement:element onNode:node ignoredAttributes:nil]; - + + // Let the effect subclass parse its own specific attributes + NSMutableDictionary* attrs = [NSMutableDictionary dictionary]; + for(NSXMLNode* attr in element.attributes) { + if(attr.name != nil && attr.stringValue != nil) { + attrs[attr.name] = attr.stringValue; + } + } + [node parseEffectAttributes:attrs]; + + // Parse light source child elements (fePointLight, feDistantLight, feSpotLight) + // and forward their attributes to the effect via parseLightSourceElement:attributes: + for(NSXMLElement* child in element.children) { + if(child.kind != NSXMLElementKind) continue; + NSString* childName = child.localName.lowercaseString; + if([childName isEqualToString:@"fepointlight"] || + [childName isEqualToString:@"fedistantlight"] || + [childName isEqualToString:@"fespotlight"]) { + NSMutableDictionary* childAttrs = [NSMutableDictionary dictionary]; + for(NSXMLNode* attr in child.attributes) { + if(attr.name != nil && attr.stringValue != nil) { + childAttrs[attr.name] = attr.stringValue; + } + } + SEL lightSel = NSSelectorFromString(@"parseLightSourceElement:attributes:"); + if([node respondsToSelector:lightSel]) { + // Forward light source element data to lighting effects + typedef void (*LightParseFn)(id, SEL, NSString*, NSDictionary*); + LightParseFn fn = (LightParseFn)[node methodForSelector:lightSel]; + fn(node, lightSel, childName, childAttrs); + } + } + } + [self computeElement:element parentNode:node]; return node; @@ -1250,6 +1519,9 @@ - (IJSVGNode*)parseStopElement:(NSXMLElement*)element *postProcessBlock = [self computeAttributesFromElement:element onNode:node ignoredAttributes:nil]; + if(node.fill == nil) { + node.fill = (id)[IJSVGColorNode colorNodeWithColor:NSColor.blackColor]; + } return node; } @@ -1282,6 +1554,9 @@ - (IJSVGNode*)parsePathElement:(NSXMLElement*)element *postProcessBlock = [self computeAttributesFromElement:element onNode:node ignoredAttributes:nil]; + if(node.fill == nil) { + node.fill = (id)[IJSVGColorNode colorNodeWithColor:NSColor.blackColor]; + } return node; } @@ -1483,6 +1758,39 @@ - (IJSVGNode*)parseCircleElement:(NSXMLElement*)element return node; } +- (IJSVGNode*)parseTextElement:(NSXMLElement*)element + parentNode:(IJSVGNode*)parentNode + postProcessBlock:(IJSVGNodeParserPostProcessBlock*)postProcessBlock +{ + NSString* text = IJSVGTextContentForElement(element); + if(text.length == 0) { + return nil; + } + + CGMutablePathRef textPath = IJSVGTextCreatePathForElement(element, text); + if(textPath == NULL) { + return nil; + } + + IJSVGPath* node = [[IJSVGPath alloc] init]; + node.type = [element.localName.lowercaseString isEqualToString:@"tspan"] ? IJSVGNodeTypeTextSpan : IJSVGNodeTypeText; + node.primitiveType = kIJSVGPrimitivePathTypePath; + node.name = element.localName; + node.parentNode = parentNode; + node.path = textPath; + CGPathRelease(textPath); + + if([parentNode isKindOfClass:IJSVGGroup.class] == YES) { + IJSVGGroup* group = (IJSVGGroup*)parentNode; + [group addChild:node]; + } + + *postProcessBlock = [self computeAttributesFromElement:element + onNode:node + ignoredAttributes:nil]; + return node; +} + - (IJSVGNode*)parseGroupElement:(NSXMLElement*)element parentNode:(IJSVGNode*)parentNode nodeType:(IJSVGNodeType)nodeType @@ -1511,7 +1819,6 @@ - (void)parseSVGElement:(NSXMLElement*)element ontoNode:(IJSVGRootNode*)node parentNode:(IJSVGNode*)parentNode postProcessBlock:(IJSVGNodeParserPostProcessBlock*)postProcessBlock - recursive:(BOOL)recursive { node.type = IJSVGNodeTypeSVG; node.name = element.localName; @@ -1537,14 +1844,15 @@ - (void)parseSVGElement:(NSXMLElement*)element onNode:node ignoredAttributes:ignored]; + // make sure we compute the viewbox + [self computeViewBoxForRootNode:node]; // recursively compute children - if(recursive == YES) { - [self computeElement:element - parentNode:node]; - } - - // make sure we compute the viewbox + [self computeElement:element + parentNode:node]; + + // Recompute fallback sizing after children are available so roots without + // an explicit viewBox can derive their missing dimension from content. [self computeViewBoxForRootNode:node]; } @@ -1556,8 +1864,7 @@ - (IJSVGNode*)parseSVGElement:(NSXMLElement*)element [self parseSVGElement:element ontoNode:node parentNode:parentNode - postProcessBlock:postProcessBlock - recursive:YES]; + postProcessBlock:postProcessBlock]; return node; } @@ -1663,7 +1970,12 @@ - (IJSVGNode*)parseImageElement:(NSXMLElement*)element // load image from base64 NSXMLNode* dataNode = [self resolveXLinkAttributeForElement:element]; - if(units == IJSVGUnitObjectBoundingBox) { + // Referenced images inside keep their own intrinsic/user-space + // geometry. The transform maps them into the parent coordinate + // system; multiplying width/height by the referencing object's bounding + // box here inflates the image and distorts the final result. + if(units == IJSVGUnitObjectBoundingBox && + parentNode.type != IJSVGNodeTypeUse) { node.width = [IJSVGUnitLength unitWithFloat:node.width.value*bounds.size.width]; node.height = [IJSVGUnitLength unitWithFloat:node.height.value*bounds.size.height]; } @@ -1684,20 +1996,11 @@ - (IJSVGNode*)parseUseElement:(NSXMLElement*)element // its important that we remove the xlink attribute or hell breaks loose NSXMLElement* detachedElement = [self detachedElementWithIdentifier:xlinkID]; - - // We are trying to use an element that is a decedent of itself. - if([self isElement:element decedentOf:detachedElement]) { - [self recursionDetectedOn:element - decendentOf:detachedElement - identifier:xlinkID]; - return nil; - } - IJSVGGroup* node = (IJSVGGroup*)[self parseGroupElement:element parentNode:parentNode nodeType:IJSVGNodeTypeUse postProcessBlock:postProcessBlock]; - + IJSVGNode* shadowNode = [self parseElement:detachedElement parentNode:node]; if(shadowNode != nil) { @@ -1777,6 +2080,17 @@ - (IJSVGNode*)parseMaskElement:(NSXMLElement*)element *postProcessBlock = [self computeAttributesFromElement:element onNode:node ignoredAttributes:nil]; + + NSString* maskType = [element attributeForName:@"mask-type"].stringValue; + if(maskType.length == 0) { + NSString* styleString = [element attributeForName:IJSVGAttributeStyle].stringValue; + if(styleString.length != 0) { + IJSVGStyleSheetStyle* style = [IJSVGStyleSheetStyle parseStyleString:styleString]; + maskType = [style property:@"mask-type"]; + } + } + node.usesAlphaMask = maskType.length != 0 && + [maskType caseInsensitiveCompare:@"alpha"] == NSOrderedSame; [self computeElement:element parentNode:node]; diff --git a/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGLayerTree.m b/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGLayerTree.m index fc1e98e2..a378c01c 100644 --- a/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGLayerTree.m +++ b/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGLayerTree.m @@ -42,14 +42,14 @@ - (id)init - (void)pushViewPort:(CGRect)viewPort { - NSValue* value = [NSValue valueWithRect:NSRectFromCGRect(viewPort)]; + NSValue* value = [NSValue valueWithRect:viewPort]; [_viewPortStack addObject:value]; } - (CGRect)viewPort { NSValue* value = _viewPortStack.lastObject; - return (CGRect)NSRectToCGRect(value.rectValue); + return value.rectValue; } - (void)popViewPort @@ -422,6 +422,10 @@ - (NSColor*)colorForColor:(NSColor*)color { IJSVGRootLayer* layer = [IJSVGRootLayer layer]; layer.viewBox = node.viewBox; + layer.hasExplicitViewBox = node.hasExplicitViewBox; + layer.rendersWithViewBoxTransform = + node.hasExplicitViewBox == YES || + node.intrinsicDimensions == (IJSVGIntrinsicDimensionWidth | IJSVGIntrinsicDimensionHeight); layer.intrinsicSize = node.intrinsicSize; layer.viewBoxAlignment = node.viewBoxAlignment; layer.viewBoxMeetOrSlice = node.viewBoxMeetOrSlice; @@ -450,8 +454,25 @@ - (NSColor*)colorForColor:(NSColor*)color sublayers:(NSArray*>*)sublayers { IJSVGGroupLayer* layer = [IJSVGGroupLayer layer]; - layer.boundingBox = [IJSVGLayer calculateFrameForSublayers:sublayers]; - layer.outerBoundingBox = layer.boundingBox; + CGRect bbox = [IJSVGLayer calculateFrameForSublayers:sublayers]; + layer.frame = bbox; + layer.boundingBox = bbox; + layer.outerBoundingBox = bbox; + + // Make sublayer FRAME positions RELATIVE to the group's origin. + // The outerBoundingBox stays at absolute position (used for gradient + // coordinate chain calculations via userSpaceTransformForLayer:). + // The frame is used by renderLayerTree: for positioning sublayers. + if(bbox.origin.x != 0 || bbox.origin.y != 0) { + for(CALayer* sublayer in sublayers) { + CGRect sf = sublayer.frame; + sublayer.frame = CGRectMake(sf.origin.x - bbox.origin.x, + sf.origin.y - bbox.origin.y, + sf.size.width, sf.size.height); + // DON'T shift outerBoundingBox — it's used for absolute coordinate chains + } + } + layer.sublayers = sublayers; return layer; } @@ -477,12 +498,17 @@ - (IJSVGGradientLayer*)drawableBasicGradientLayerForLayer:(CALayer / + // objectBoundingBox cases. + maskingBounds = CGRectApplyAffineTransform(maskBounds, userSpaceTransform); // we need to move all the layers back if they are into the userSpace // coordinate system for(CALayer *childLayer in maskLayer.sublayers) { - CGRect innerBoundingBox = childLayer.innerBoundingBox; - CGAffineTransform innerTransform = CGAffineTransformMakeTranslation(-innerBoundingBox.origin.x, - -innerBoundingBox.origin.y); - childLayer.frame = CGRectApplyAffineTransform(childLayer.frame, userSpaceTransform); - childLayer.frame = CGRectApplyAffineTransform(childLayer.frame, innerTransform); + CGRect innerBoundingBox = childLayer.innerBoundingBox; + CGAffineTransform innerTransform = CGAffineTransformMakeTranslation(-innerBoundingBox.origin.x, + -innerBoundingBox.origin.y); + childLayer.frame = CGRectApplyAffineTransform(childLayer.frame, userSpaceTransform); + childLayer.frame = CGRectApplyAffineTransform(childLayer.frame, innerTransform); } + } if(maskNode.units == IJSVGUnitUserSpaceOnUse) { @@ -620,6 +649,7 @@ - (IJSVGPatternLayer*)drawableBasicPatternLayerForLayer:(CALayer*)layer + clipPath:(IJSVGClipPath*)clipPath + clipRule:(IJSVGWindingRule)clipRule +{ + if([node isKindOfClass:IJSVGGroup.class] == NO || + clipPath.clipPath != nil || + clipRule == IJSVGWindingRuleEvenOdd) { + return CGFLOAT_MAX; + } + IJSVGGroup *group = (IJSVGGroup *)node; + if(group.children.count == 0 || + group.children.count != clipPath.children.count || + group.children.count != layer.sublayers.count || + group.children.count != 2) { + return CGFLOAT_MAX; + } + for(NSUInteger idx = 0; idx < group.children.count; idx++) { + IJSVGNode *groupChild = group.children[idx]; + IJSVGNode *clipChild = clipPath.children[idx]; + if([self _nodeIsSafeForMatchingClipSeam:groupChild] == NO || + [self _nodeIsSafeForMatchingClipSeam:clipChild] == NO) { + return CGFLOAT_MAX; + } + if(groupChild.windingRule == IJSVGWindingRuleEvenOdd || + clipChild.windingRule == IJSVGWindingRuleEvenOdd) { + return CGFLOAT_MAX; + } + if([self _pathNode:(IJSVGPath *)groupChild + matchesClipPathNode:(IJSVGPath *)clipChild] == NO) { + return CGFLOAT_MAX; + } + } + CALayer *leftLayer = (CALayer *)layer.sublayers[0]; + CALayer *rightLayer = (CALayer *)layer.sublayers[1]; + CGFloat leftEdge = CGRectGetMaxX(leftLayer.frame); + CGFloat rightEdge = CGRectGetMinX(rightLayer.frame); + if(fabs(leftEdge - rightEdge) > 1.f) { + return CGFLOAT_MAX; + } + return (leftEdge + rightEdge) * .5f; +} + +- (CALayer*)_matchingClipSeamLayerForNode:(IJSVGNode*)node + layer:(CALayer*)layer + clipPath:(IJSVGClipPath*)clipPath + clipRule:(IJSVGWindingRule)clipRule +{ + CGFloat seamX = [self _matchingClipSeamXForNode:node + layer:layer + clipPath:clipPath + clipRule:clipRule]; + if(isfinite(seamX) == NO || seamX == CGFLOAT_MAX) { + return nil; + } + IJSVGShapeLayer *seamLayer = [IJSVGShapeLayer layer]; + CGMutablePathRef path = CGPathCreateMutable(); + CGRect bounds = layer.bounds; + CGPathMoveToPoint(path, NULL, seamX, CGRectGetMinY(bounds)); + CGPathAddLineToPoint(path, NULL, seamX, CGRectGetMaxY(bounds)); + seamLayer.path = path; + seamLayer.frame = bounds; + seamLayer.outerBoundingBox = bounds; + seamLayer.boundingBox = bounds; + seamLayer.fillColor = nil; + seamLayer.strokeColor = NSColor.whiteColor.CGColor; + seamLayer.lineWidth = 1.f; + seamLayer.opacity = 21.f / 255.f; + CGPathRelease(path); + return seamLayer; +} + #pragma mark Defaults - (void)applyDefaultsToLayer:(CALayer*)layer @@ -692,19 +827,45 @@ - (void)applyDefaultsToLayer:(CALayer*)layer // add the clip mask if any if(node.clipPath != nil) { IJSVGClipPath* clipPath = node.clipPath; - CGPathRef path = [self newClipPathFromNode:clipPath - fromLayer:layer]; - IJSVGWindingRule clipRule = node.clipRule; if(clipRule == IJSVGWindingRuleInherit) { clipRule = clipPath.computedClipRule; } - - layer.clipPath = path; - layer.clipRule = [IJSVGUtils CGFillRuleForWindingRule:clipRule]; - CGPathRelease(path); + // Chained clip paths need sequential intersection semantics, and + // multi-child clip paths only need layered composition when the + // children are not simple path nodes (for example -based + // composition like the clippy repro). Direct path children can + // still be flattened into one CGPath. + BOOL requiresLayeredClipComposition = clipPath.clipPath != nil; + if(requiresLayeredClipComposition == NO && clipPath.children.count > 1) { + for(IJSVGNode *child in clipPath.children) { + if([child isKindOfClass:IJSVGPath.class] == NO) { + requiresLayeredClipComposition = YES; + break; + } + } + } + if(requiresLayeredClipComposition == YES) { + layer.clipLayers = [self clipLayersFromNode:clipPath + referencingLayer:layer + fromLayer:nil]; + } else { + CGPathRef path = [self newClipPathFromNode:clipPath + fromLayer:layer]; + layer.clipPath = path; + layer.clipRule = [IJSVGUtils CGFillRuleForWindingRule:clipRule]; + CGPathRelease(path); + } + if(node.filter == nil) { + CALayer *seamLayer = [self _matchingClipSeamLayerForNode:node + layer:layer + clipPath:clipPath + clipRule:clipRule]; + if(seamLayer != nil) { + [layer addSublayer:seamLayer]; + } + } } - // setup the opacity CGFloat opacity = node.opacity.value; if(opacity != 1.f) { @@ -735,8 +896,77 @@ - (void)applyDefaultsToLayer:(CALayer*)layer IJSVGFilterLayer* filterLayer = [IJSVGFilterLayer layer]; filterLayer.filter = filter; filterLayer.frame = layer.frame; + filterLayer.outerBoundingBox = layer.outerBoundingBox; filterLayer.sublayer = layer; + // SVG compositing applies clipping and masking after filter effects. + // Move the built clip/mask state to the filter wrapper so the filtered + // image is clipped, instead of blurring already-clipped source content. + filterLayer.maskLayer = layer.maskLayer; + if(filterLayer.maskLayer != nil) { + filterLayer.maskLayer.referencingLayer = filterLayer; + layer.maskLayer = nil; + } + filterLayer.clipRule = layer.clipRule; + filterLayer.clipPath = layer.clipPath; + layer.clipPath = nil; + filterLayer.clipLayers = layer.clipLayers; + filterLayer.clippingTransform = layer.clippingTransform; + filterLayer.clippingBoundingBox = layer.clippingBoundingBox; + if(filterLayer.clipLayers.count != 0) { + for(CALayer *clipLayer in filterLayer.clipLayers) { + clipLayer.referencingLayer = filterLayer; + } + layer.clipLayers = nil; + layer.clippingTransform = CGAffineTransformIdentity; + layer.clippingBoundingBox = CGRectZero; + } + // SVG spec: filter is applied before opacity. Transfer the element's + // opacity to the filter layer so it composites AFTER filtering. + filterLayer.opacity = layer.opacity; + layer.opacity = 1.0f; + filterLayer.referencingLayer = layer.referencingLayer; layer.referencingLayer = filterLayer; + CALayer *seamLayer = nil; + if(node.clipPath != nil) { + IJSVGWindingRule clipRule = node.clipRule; + if(clipRule == IJSVGWindingRuleInherit) { + clipRule = node.clipPath.computedClipRule; + } + seamLayer = [self _matchingClipSeamLayerForNode:node + layer:layer + clipPath:node.clipPath + clipRule:clipRule]; + } + if(seamLayer != nil) { + IJSVGGroupLayer *wrapperLayer = [IJSVGGroupLayer layer]; + CGRect frame = filterLayer.frame; + wrapperLayer.frame = frame; + wrapperLayer.boundingBox = frame; + wrapperLayer.outerBoundingBox = frame; + wrapperLayer.referencingLayer = filterLayer.referencingLayer; + wrapperLayer.maskLayer = filterLayer.maskLayer; + wrapperLayer.clipRule = filterLayer.clipRule; + wrapperLayer.clipPath = filterLayer.clipPath; + wrapperLayer.clipLayers = filterLayer.clipLayers; + wrapperLayer.clippingTransform = filterLayer.clippingTransform; + wrapperLayer.clippingBoundingBox = filterLayer.clippingBoundingBox; + wrapperLayer.opacity = filterLayer.opacity; + filterLayer.maskLayer = nil; + filterLayer.clipPath = nil; + filterLayer.clipLayers = nil; + filterLayer.clippingTransform = CGAffineTransformIdentity; + filterLayer.clippingBoundingBox = CGRectZero; + filterLayer.opacity = 1.f; + filterLayer.frame = CGRectMake(0.f, 0.f, frame.size.width, frame.size.height); + seamLayer.frame = CGRectMake(0.f, 0.f, frame.size.width, frame.size.height); + seamLayer.outerBoundingBox = filterLayer.frame; + seamLayer.boundingBox = filterLayer.frame; + seamLayer.referencingLayer = wrapperLayer; + filterLayer.referencingLayer = wrapperLayer; + [wrapperLayer addSublayer:filterLayer]; + [wrapperLayer addSublayer:seamLayer]; + return wrapperLayer; + } return filterLayer; } @@ -776,6 +1006,77 @@ - (void)applyDefaultsToLayer:(CALayer*)layer identity = CGAffineTransformConcat(identity, transform.CGAffineTransform); } + + // For raster image content behind a pure scale/translate transform, + // absorb the transform into the image viewport instead of scaling the + // already-rasterized layer. That preserves the image's own + // preserveAspectRatio behavior instead of squashing the bitmap. + CALayer* imageContainer = layer; + NSMutableArray*>* imageContainerChain = [[NSMutableArray alloc] init]; + IJSVGImageLayer* imageLayer = nil; + while([imageContainer isKindOfClass:IJSVGGroupLayer.class] == YES && + imageContainer.sublayers.count == 1) { + [imageContainerChain addObject:imageContainer]; + CALayer* childLayer = (CALayer*)imageContainer.sublayers.firstObject; + if([childLayer isKindOfClass:IJSVGImageLayer.class] == YES) { + imageLayer = (IJSVGImageLayer*)childLayer; + break; + } + imageContainer = childLayer; + } + if(identity.b == 0.f && + identity.c == 0.f && + identity.a > 0.f && + identity.d > 0.f && + imageLayer != nil) { + CGAffineTransform imageTransform = CGAffineTransformIdentity; + if(x != 0.f || y != 0.f) { + imageTransform = CGAffineTransformTranslate(imageTransform, x, y); + } + for(IJSVGTransform* transform in transforms.reverseObjectEnumerator) { + CGAffineTransform transformAffine = transform.CGAffineTransform; + if(transform.appliedContentUnits == IJSVGUnitObjectBoundingBox) { + CGRect appliedBounds = transform.appliedBounds; + if(transform.command == IJSVGTransformCommandTranslate || + transform.command == IJSVGTransformCommandTranslateX || + transform.command == IJSVGTransformCommandTranslateY || + transform.command == IJSVGTransformCommandMatrix) { + if(isfinite(CGRectGetWidth(appliedBounds)) && + CGRectGetWidth(appliedBounds) != 0.f) { + transformAffine.tx /= CGRectGetWidth(appliedBounds); + } + if(isfinite(CGRectGetHeight(appliedBounds)) && + CGRectGetHeight(appliedBounds) != 0.f) { + transformAffine.ty /= CGRectGetHeight(appliedBounds); + } + } + } + imageTransform = CGAffineTransformConcat(imageTransform, transformAffine); + } + CGRect imageFrame = CGRectApplyAffineTransform(imageLayer.frame, imageTransform); + imageLayer.frame = imageFrame; + imageLayer.boundingBox = imageFrame; + imageLayer.outerBoundingBox = imageFrame; + // Only force the image to fill its new frame when the absorbed + // transform has non-uniform scale — that's the case where the + // author's transform was meant to stretch the raster (so a + // meet-fit would letterbox the wrong way). For pure translate or + // uniform scale the frame's aspect ratio is unchanged and the + // image's own preserveAspectRatio still produces the right result, + // matching WebKit's -of- semantics. + if(fabs(imageTransform.a) != fabs(imageTransform.d)) { + imageLayer.image.viewBoxAlignment = IJSVGViewBoxAlignmentNone; + imageLayer.image.viewBoxMeetOrSlice = IJSVGViewBoxMeetOrSliceSlice; + } + for(CALayer* containerLayer in imageContainerChain.reverseObjectEnumerator) { + CGRect groupFrame = [IJSVGLayer calculateFrameForSublayers:containerLayer.sublayers]; + containerLayer.frame = groupFrame; + containerLayer.boundingBox = groupFrame; + containerLayer.outerBoundingBox = groupFrame; + } + return layer; + } + parentLayer.affineTransform = identity; [parentLayer addSublayer:layer]; parentLayer.outerBoundingBox = [IJSVGLayer calculateFrameForSublayers:parentLayer.sublayers]; @@ -787,13 +1088,16 @@ - (void)applyDefaultsToLayer:(CALayer*)layer - (IJSVGLayer*)drawableLayerForImageNode:(IJSVGImage*)image { IJSVGImageLayer* layer = [[IJSVGImageLayer alloc] initWithImage:image]; - // make sure we set the width and height correctly, - // as this may not be exactly the same as the size of the - // given image + // Use the image's declared width/height as-is. Rounding up via ceilf + // here corrupts fractional OBB sizes (e.g. width="1.05" inflates to + // 2.0), which then double-stretches raster content when a with + // a non-uniform scale is absorbed into the frame. CGRect frame = layer.frame; - frame.size.width = ceilf(image.width.value); - frame.size.height = ceilf(image.height.value); + frame.size.width = image.width.value; + frame.size.height = image.height.value; layer.frame = frame; + layer.boundingBox = frame; + layer.outerBoundingBox = frame; [layer setNeedsLayout]; return (IJSVGLayer*)layer; } diff --git a/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGStyle.h b/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGStyle.h index 0d470ba1..f139bcc5 100644 --- a/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGStyle.h +++ b/Framework/IJSVG/IJSVG/Source/Rendering/IJSVGStyle.h @@ -8,9 +8,8 @@ #import #import -#import -#import #import +#import @interface IJSVGStyle : NSObject diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.h index e6e8a478..73b0bd0b 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.h @@ -21,7 +21,5 @@ - (void)addBits:(IJSVGBitFlags*)storage; - (BOOL)bitIsSet:(int)bit; - (void)setBit:(int)bit; -- (void)unsetBit:(int)bit; -- (void)setAllBits; @end diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.m index b1757857..7e7efeaa 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags.m @@ -48,14 +48,6 @@ - (void)setBit:(int)bit *(_storage + bit) = 1; } -- (void)unsetBit:(int)bit -{ - *(_storage + bit) = 0; -} -- (void)setAllBits -{ - memset(_storage, 1, _length); -} @end diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags64.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags64.m index 812a4fe6..84ccf3ba 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags64.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGBitFlags64.m @@ -37,14 +37,4 @@ - (void)setBit:(int)bit _storage64 |= (1ULL << bit); } -- (void)unsetBit:(int)bit -{ - _storage64 &= ~(1ULL << bit); -} - -- (void)setAllBits -{ - _storage64 = 1ULL; -} - @end diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.h index b87ba5c4..9da1b1fb 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.h @@ -15,7 +15,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly, strong) IJSVGFeatureFlag* filters; @property (nonatomic, readonly, strong) IJSVGFeatureFlag* viewBoxNormalization; -@property (nonatomic, readonly, strong) IJSVGFeatureFlag* inferViewBoxes; @end diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.m index 297229d1..100125e9 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGFeatureFlags.m @@ -14,13 +14,10 @@ - (instancetype)init { if((self = [super init]) != nil) { // filters - _filters = [IJSVGFeatureFlag featureFlagWithEnabled:NO]; + _filters = [IJSVGFeatureFlag featureFlagWithEnabled:YES]; // viewBox normalization _viewBoxNormalization = [IJSVGFeatureFlag featureFlagWithEnabled:YES]; - - // Inferring of viewBoxes - _inferViewBoxes = [IJSVGFeatureFlag featureFlagWithEnabled:YES]; } return self; } diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.h index 0a3a81eb..cd571eb8 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.h @@ -29,6 +29,7 @@ + (IJSVGThreadManager*)managerForThread:(NSThread*)thread; + (IJSVGThreadManager*)managerForSVG:(IJSVG*)svg; + (IJSVGThreadManager*)currentManager; ++ (void)clearAllCIContextCaches; - (void)adopt:(IJSVG*)svg; - (void)remove:(IJSVG*)svg; diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.m index b364a771..67d3e338 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGThreadManager.m @@ -7,6 +7,7 @@ // #import +#import @implementation IJSVGThreadManager @@ -59,6 +60,18 @@ + (IJSVGThreadManager *)currentManager return [self managerForThread:NSThread.currentThread]; } ++ (void)clearAllCIContextCaches +{ + NSMapTable* map = [self mapTable]; + @synchronized (map) { + NSEnumerator* enumerator = map.objectEnumerator; + IJSVGThreadManager* manager = nil; + while((manager = [enumerator nextObject]) != nil) { + [manager->_CIContext clearCaches]; + } + } +} + - (void)dealloc { if(_pathDataStream != NULL) { @@ -142,9 +155,25 @@ - (CIContext*)CIContext if(_CIContext == nil) { // for high performance we can disable the color // management +#if TARGET_OS_IOS + NSMutableDictionary *options = [NSMutableDictionary dictionaryWithDictionary:@{ + kCIImageColorSpace: NSNull.null, + kCIContextWorkingFormat: @(kCIFormatRGBA8) + }]; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + if(colorSpace != NULL) { + options[kCIContextWorkingColorSpace] = (__bridge id)colorSpace; + options[kCIContextOutputColorSpace] = (__bridge id)colorSpace; + } + _CIContext = [CIContext contextWithOptions:options]; + if(colorSpace != NULL) { + CGColorSpaceRelease(colorSpace); + } +#else _CIContext = [CIContext contextWithOptions:@{ kCIImageColorSpace: NSNull.null }]; +#endif } return _CIContext; } diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransaction.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransaction.m index bfeec931..8a8cf0e8 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransaction.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransaction.m @@ -8,7 +8,8 @@ #import #import -#import +#import +#import BOOL IJSVGIsMainThread(void) { return IJSVGThreadManager.currentManager.thread.isMainThread; @@ -16,6 +17,11 @@ BOOL IJSVGIsMainThread(void) { BOOL IJSVGBeginTransaction(void) { +#if TARGET_OS_IOS + if([NSThread isMainThread] == NO) { + return NO; + } +#endif if(IJSVGIsMainThread() == YES) { return NO; } diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.h index 65c4ab86..7bf31053 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.h @@ -33,6 +33,8 @@ typedef NS_ENUM(NSInteger, IJSVGTransformCommand) { @property (nonatomic, assign) CGFloat* parameters; @property (nonatomic, assign) NSInteger parameterCount; @property (nonatomic, assign) NSInteger sort; +@property (nonatomic, assign) CGRect appliedBounds; +@property (nonatomic, assign) IJSVGUnitType appliedContentUnits; void IJSVGApplyTransform(NSArray* transforms, IJSVGTransformApplyBlock block); BOOL IJSVGAffineTransformScalesAndTranslates(CGAffineTransform affineTransform); diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.m index d0a8d9e3..6d9e1104 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGTransform.m @@ -24,6 +24,8 @@ - (id)copyWithZone:(NSZone*)zone trans.parameters = (CGFloat*)malloc(sizeof(CGFloat) * _parameterCount); trans.sort = _sort; trans.parameterCount = _parameterCount; + trans.appliedBounds = _appliedBounds; + trans.appliedContentUnits = _appliedContentUnits; memcpy(trans.parameters, _parameters, sizeof(CGFloat) * _parameterCount); return trans; } @@ -229,6 +231,8 @@ - (IJSVGTransform*)transformByApplyingUnits:(IJSVGUnitType)units - (void)applyBounds:(CGRect)bounds withContentUnits:(IJSVGUnitType)contentUnits { + _appliedBounds = bounds; + _appliedContentUnits = contentUnits; if(contentUnits != IJSVGUnitObjectBoundingBox) { return; } diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitPoint.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitPoint.m index d04e5c21..3d5d5960 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitPoint.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitPoint.m @@ -7,6 +7,7 @@ // #import +#import @implementation IJSVGUnitPoint diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitRect.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitRect.m index 2979071e..f017dba9 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitRect.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitRect.m @@ -7,6 +7,7 @@ // #import +#import @implementation IJSVGUnitRect diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitSize.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitSize.m index c7a0a8ee..b99c31a2 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitSize.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUnitSize.m @@ -7,6 +7,7 @@ // #import +#import @implementation IJSVGUnitSize diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.h index 346248a2..b55b3668 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.h @@ -9,7 +9,7 @@ #import #import #import -#import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -24,7 +24,6 @@ CGFloat IJSVGRatio(CGPoint a, CGPoint b); CGFloat IJSVGAngle(CGPoint a, CGPoint b); CGFloat IJSVGRadiansToDegrees(CGFloat radians); CGFloat IJSVGDegreesToRadians(CGFloat degrees); -BOOL IJSVGIsValidContextSize(CGSize size); char IJSVGCharToLower(char c); BOOL IJSVGCharBufferCaseInsensitiveCompare(const char* str1, const char* str2); @@ -64,8 +63,6 @@ BOOL IJSVGIsSVGLayer(CALayer* layer); + (NSString* _Nullable)mixBlendingModeForBlendMode:(IJSVGBlendMode)blendMode; + (NSRange)rangeOfParentheses:(NSString*)string; -+ (void)logParameters:(CGFloat*)param - count:(NSInteger)count; + (CGFloat)floatValue:(NSString*)string; + (CGFloat)angleBetweenPointA:(NSPoint)point pointb:(NSPoint)point; diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.m index f073b859..8b3790d9 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGUtils.m @@ -25,10 +25,6 @@ @implementation IJSVGUtils .height = -1.23f }; -BOOL IJSVGIsValidContextSize(CGSize size) { - return size.width >= 1.f && size.height >= 1.f; -} - BOOL IJSVGCharBufferIsHEX(char* buffer) { char c; while((c = *buffer++)) { @@ -592,16 +588,6 @@ + (CGFloat)floatValue:(NSString*)string return val; } -+ (void)logParameters:(CGFloat*)param - count:(NSInteger)count -{ - NSMutableString* str = [[NSMutableString alloc] init]; - for (NSInteger i = 0; i < count; i++) { - [str appendFormat:@"%f ", param[i]]; - } - NSLog(@"%@", str); -} - + (CGFloat)floatValue:(NSString*)string { if([string isEqualToString:IJSVGStringInherit]) { @@ -696,6 +682,13 @@ + (CGLineJoin)CGLineJoinForCALineJoin:(CAShapeLayerLineCap)lineJoin + (NSImage*)resizeImage:(NSImage*)anImage toSize:(CGSize)size { +#if TARGET_OS_IOS + UIGraphicsBeginImageContextWithOptions(size, NO, 0.f); + [anImage drawInRect:CGRectMake(0.f, 0.f, size.width, size.height)]; + NSImage* image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; +#else NSImage* image = [[NSImage alloc] initWithSize:size]; [image lockFocus]; [anImage drawInRect:NSMakeRect(0.f, 0.f, size.width, size.height) @@ -704,6 +697,7 @@ + (NSImage*)resizeImage:(NSImage*)anImage fraction:1.f]; [image unlockFocus]; return image; +#endif } @end diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.h b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.h index a607d63c..940a8c78 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.h +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.h @@ -7,7 +7,7 @@ // #import -#import +#import typedef NS_ENUM(NSInteger, IJSVGViewBoxAlignment) { IJSVGViewBoxAlignmentUnknown, diff --git a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.m b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.m index 064296c9..2f0d9440 100644 --- a/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.m +++ b/Framework/IJSVG/IJSVG/Source/Utils/IJSVGViewBox.m @@ -340,12 +340,11 @@ CGAffineTransform IJSVGViewBoxComputeRectNone(CGRect viewBox, CGRect drawingRect CGAffineTransform transform = CGAffineTransformIdentity; transform = CGAffineTransformConcat(transform, CGAffineTransformMakeScale(width, height)); - // translate it - CGAffineTransform translate = CGAffineTransformMakeTranslation(drawingRect.size.width / 2.f - ((viewBox.size.width * width)) / 2.f, - drawingRect.size.height - (viewBox.size.height * height)); - transform = CGAffineTransformConcat(transform, translate); - translate = CGAffineTransformMakeTranslation(-(viewBox.origin.x * width), - -(viewBox.origin.y * height)); + // preserveAspectRatio="none" maps the viewBox directly to the drawing + // rect with independent X/Y scaling. There is no additional centering or + // bottom-alignment step. + CGAffineTransform translate = CGAffineTransformMakeTranslation(-(viewBox.origin.x * width), + -(viewBox.origin.y * height)); transform = CGAffineTransformConcat(transform, translate); return transform; } diff --git a/IJSVGExample/IJSVGExample.xcodeproj/project.pbxproj b/IJSVGExample/IJSVGExample.xcodeproj/project.pbxproj index c48c793c..ea179ebb 100644 --- a/IJSVGExample/IJSVGExample.xcodeproj/project.pbxproj +++ b/IJSVGExample/IJSVGExample.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 072DC6760DCF445DBD512112 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C310F21548684F6181BD5579 /* WebKit.framework */; }; 5901B73D282818CA00290525 /* contour_hatching.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5901B730282818CA00290525 /* contour_hatching.svg */; }; 5901B73E282818CA00290525 /* canonical.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5901B731282818CA00290525 /* canonical.svg */; }; 5901B73F282818CA00290525 /* clipping.svg in Resources */ = {isa = PBXBuildFile; fileRef = 5901B732282818CA00290525 /* clipping.svg */; }; @@ -68,6 +69,13 @@ 59F799E219B880CE00096CB7 /* htc_one.svg in Resources */ = {isa = PBXBuildFile; fileRef = 59F799E119B880CE00096CB7 /* htc_one.svg */; }; 59FB54BE280B03E600D148FA /* IJSVG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59FB54B8280B038200D148FA /* IJSVG.framework */; }; 59FB54C0280B03F400D148FA /* IJSVG.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 59FB54B8280B038200D148FA /* IJSVG.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + AE0DB59A2FB9B10F00DA70FD /* Group.svg in Resources */ = {isa = PBXBuildFile; fileRef = AE0DB5972FB9B10F00DA70FD /* Group.svg */; }; + AE0DB59B2FB9B10F00DA70FD /* Rectangle.svg in Resources */ = {isa = PBXBuildFile; fileRef = AE0DB5992FB9B10F00DA70FD /* Rectangle.svg */; }; + AE0DB59C2FB9B10F00DA70FD /* NewTux.svg in Resources */ = {isa = PBXBuildFile; fileRef = AE0DB5982FB9B10F00DA70FD /* NewTux.svg */; }; + AE0DB59D2FB9B10F00DA70FD /* AJ_Digital_Camera.svg in Resources */ = {isa = PBXBuildFile; fileRef = AE0DB5962FB9B10F00DA70FD /* AJ_Digital_Camera.svg */; }; + E6C4BA913FFD4E098562C475 /* IJSVGWebKitPSNRTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FCC83ED38A1C4FC3BA5D4E13 /* IJSVGWebKitPSNRTests.m */; }; + 07198EA402F3443187A14F1C /* IJSVG.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59FB54B8280B038200D148FA /* IJSVG.framework */; }; + EC2F13BF2DF44CD088469013 /* IJSVG.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 59FB54B8280B038200D148FA /* IJSVG.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -105,6 +113,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E93DB705D0414EA6A57AC38E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + EC2F13BF2DF44CD088469013 /* IJSVG.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -182,6 +201,12 @@ 59D6B1A6284F698F0058DE6B /* sh.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sh.svg; sourceTree = ""; }; 59F799E119B880CE00096CB7 /* htc_one.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = htc_one.svg; sourceTree = ""; }; 59FB54B3280B038200D148FA /* IJSVG.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = IJSVG.xcodeproj; path = ../Framework/IJSVG/IJSVG.xcodeproj; sourceTree = ""; }; + AE0DB5962FB9B10F00DA70FD /* AJ_Digital_Camera.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = AJ_Digital_Camera.svg; sourceTree = ""; }; + AE0DB5972FB9B10F00DA70FD /* Group.svg */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Group.svg; sourceTree = ""; }; + AE0DB5982FB9B10F00DA70FD /* NewTux.svg */ = {isa = PBXFileReference; lastKnownFileType = text; path = NewTux.svg; sourceTree = ""; }; + AE0DB5992FB9B10F00DA70FD /* Rectangle.svg */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Rectangle.svg; sourceTree = ""; }; + C310F21548684F6181BD5579 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; + FCC83ED38A1C4FC3BA5D4E13 /* IJSVGWebKitPSNRTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IJSVGWebKitPSNRTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -197,6 +222,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 072DC6760DCF445DBD512112 /* WebKit.framework in Frameworks */, + 07198EA402F3443187A14F1C /* IJSVG.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -311,6 +338,11 @@ isa = PBXGroup; children = ( 5956659019B62F4700D805FF /* IJSVGExampleTests.m */, + FCC83ED38A1C4FC3BA5D4E13 /* IJSVGWebKitPSNRTests.m */, + AE0DB5962FB9B10F00DA70FD /* AJ_Digital_Camera.svg */, + AE0DB5972FB9B10F00DA70FD /* Group.svg */, + AE0DB5982FB9B10F00DA70FD /* NewTux.svg */, + AE0DB5992FB9B10F00DA70FD /* Rectangle.svg */, 5956658E19B62F4700D805FF /* Supporting Files */, ); path = IJSVGExampleTests; @@ -335,6 +367,7 @@ 59FB54BD280B03E600D148FA /* Frameworks */ = { isa = PBXGroup; children = ( + C310F21548684F6181BD5579 /* WebKit.framework */, ); name = Frameworks; sourceTree = ""; @@ -368,6 +401,7 @@ 5956658619B62F4700D805FF /* Sources */, 5956658719B62F4700D805FF /* Frameworks */, 5956658819B62F4700D805FF /* Resources */, + E93DB705D0414EA6A57AC38E /* Embed Frameworks */, ); buildRules = ( ); @@ -494,6 +528,10 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + AE0DB59A2FB9B10F00DA70FD /* Group.svg in Resources */, + AE0DB59B2FB9B10F00DA70FD /* Rectangle.svg in Resources */, + AE0DB59C2FB9B10F00DA70FD /* NewTux.svg in Resources */, + AE0DB59D2FB9B10F00DA70FD /* AJ_Digital_Camera.svg in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -521,6 +559,7 @@ buildActionMask = 2147483647; files = ( 5956659119B62F4700D805FF /* IJSVGExampleTests.m in Sources */, + E6C4BA913FFD4E098562C475 /* IJSVGWebKitPSNRTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -658,7 +697,6 @@ 5956659819B62F4700D805FF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -672,14 +710,12 @@ INFOPLIST_FILE = IJSVGExampleTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IJSVGExample.app/Contents/MacOS/IJSVGExample"; }; name = Debug; }; 5956659919B62F4700D805FF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ENABLE_OBJC_ARC = YES; COMBINE_HIDPI_IMAGES = YES; FRAMEWORK_SEARCH_PATHS = ( @@ -689,7 +725,6 @@ INFOPLIST_FILE = IJSVGExampleTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/IJSVGExample.app/Contents/MacOS/IJSVGExample"; }; name = Release; }; diff --git a/IJSVGExample/IJSVGExample.xcodeproj/project.xcworkspace/xcuserdata/duncan.xcuserdatad/UserInterfaceState.xcuserstate b/IJSVGExample/IJSVGExample.xcodeproj/project.xcworkspace/xcuserdata/duncan.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..507c84567b405bb9a5417c789cd03ecbab3e0582 GIT binary patch literal 9236 zcmbVR34Bvk)<5?)DNUL*FI&@WrO*n9O-Z`f4Z1=tl+q2#7Sran4Wvm)($XS>*98z% zTybO6E(nN>yW)zVIPRdJgQzI(+l=G5>kPwp-g`~jD9&%@YktkkyYJj{?mhRM|5;vZ zx8E0zIGwKp0SXBq134tZI8ZLg>g7XWUog;;{WuR#(hni?XW2+znp+}n^0DlkDBn85)#Fb$mGf?UXhd?hDzOnenLth>lgJrl8p$K&q=HnEDl(JIBGu$PQbU?ZGnr2oki{fG zdPqO{16e~ZBx}iKWCPhqHj%5y)#Q3|BN--J$sJ@H*-joH50Zz-!(=yklI$VRkXOh7 za*!M)Z;`jjJLGHf5AqH9mV8IPCqIxM$xq~Ga)J^nr-^hT&7@iMG&+e+rc>zYbSgcA zW>W{vq4~6cPN(Ixg3hAVbPk*y8qN_q|5 zOt;WK(%b1n^kKT2K0+U*kI~2J6ZA>ChdxF3(tY%4`V8IQlojal`wxQ}Qa}S7=pl7} zZ9~p-zCXMWuOlYVoYm&{gu_Fi1s$U-VF(OhWHNReo8acMic9ib6~&cRj)LO+d`G^s zve;2lT2SaHtjwz_brzLX6}U>=oT0j+a(*zhJlySR<12z~Jzabt5*2KLOvv5=R3qo=hz7>I;|em@^_msJ#3xLnREM`d|Ikt4sXtjtlGS6JZ4 zEi26}EH5l8sw{Lajj^o?dAfKv=fHtkp70!BM+jZO+wAf8@ZqXZu&cJAo)1TYAy2EH z7u}=66Y;pYNihZ}yv!4t?+G{i!agLL?~8Oc_yQe%9-p|m@km(O7C}dhsVjcDoOzU0 zFz9dfgsP*g++5n|Gf_!PZVW=MZ8E zgNxu|H)lb`3!YFXgr9o8JH&_4?>)jQ*km?ou9oz)Mne;!2{L)2)rN;Q;gNHy#*y&SaGT)7b-^%NSul`S`rNUE&t%6AY#XQ6>W$_@NeZQ^ zvD*{rY(Yri`)2t9k)h<|#ntCEG|#N;^K^Cld5#<+DH?7m-`DL42!T)(BXT-!Iqy~u zkco~POhzgLH!^(AOdL#28%j0_WYL+c6A21dqX! z@ERP3H{pHw06xZ`_Ye35PLM>26(!OY;Wh4$Xe3Kn3+oV>D*L+q5?;;iiTHg1zS+mG zl;XT_bR$+b4C~=C=>4zh-CV1?F*38f*;QE4+&C%&JutU$^mdHM#{OY+q6zuheF)vsHa5cLZcbm@FsCOXEO^w)hhd{&=l=x&M9q8?Tn*WS za1}da5auI%pbMXNYVZ)TA*Ue{>S>Ggg!s%E-7`cTM>*X9H$(Oi+z2@h5l$vUkGWa1Y!I z_rd+}06d6h_b}{cg{+7bvl4bDJByvo&SB@W={w<3w8+Qd3AD;R@D%KYeeg7!!Ae;f zt7UW9Qns4i%x+9aGIyw+n5j!G+xh@?0II}zC>q6{`h7d8W^~R$QW(v{AyhmgsE(CS1M_8-i z+dV!0h$u!1QyC)yaMXvWAh?Nh*>#Rbs94)$1Y|kPBN~ilKY#)N7@D{6NH7qa6;25Gp>M*8%h5_6#JIV#*|@>5G^#KTBTzXRGK-XMdHC>(*84|);}?7{0mONzu_bS zLI@=Z7zv`O;_P6X7&VrTu2)j2F&4z@gTV-fN?sbrnOz=VV3gdAiIZ>=bK9dlN1w6z zY#v+4masb3?B?=f9xsJA#1ui-ITFX5YE*~Mo8H0WY7>5 zQInK~qmr7$HLwQO$eI?`@+(K|ikl(2CeabSo3o*+M)4ORMgTp@h=as1nj+S5H=z|p zbre0rA(A0%;Z%<>bs!T-CdpbTL~Uv7Y%yEp=3LVvUER}s%fh`Kj_%O3u~9efCZ|ux z^}0jBWzs4plPQaE4N=1wCa06B&^vYrZL`kPww(70hEs`ULBBLJn>gIu6g-PKD;PbP zct9BL9;qh{ z%+LDS1p=lHN9T)q`0-ChU4Z4(#Lux@{F!>G|tB|9@fWt*h&^aRj$I{ zVb=RA@Fd+M;2A+o3yxq~NYYoYps$szdla2k3FyRD{0fZ5zz0KhUPP?q5@Hq_1FfDg zzX-1(-j8V9%QIjRwm@>1b!2^0Z3AM<5LqX9!sNDKSB|F}gQet>Illuh=-M&770NTo zprV*@YP21RJLGb5g;e>Bfl_Qq3T|d?#!N~`h%u(~D+N>t*LzkqtjxVlp04Oau+-&y3R+)W0<^7eT;m9%_#CNd5;_??~@P6hvXyjF}sFc%dTVBv(4-V zb|bqCMOZ{V)R24Fnt&^Fk&~9(geuHE)1T##hKyEaOFFl8O24y zPvItCx`|ScqXx*{MU$w4DrquRQ8i7W8p=^E)logWoei)-HpGV6R(1#5#`u0G z7u-TqsR?~Oi(2sCM(s46jz_w6H9%nDIw4^dVGE_@+tNE#}bCC+iQ8=%X=}BM)6(h)e2>obmonn=pNpg9-%Xf{4COQp-2sM z;6egYO;#Of58^T6nuIJ=(x92=xOdyGwy2s<&!773%3G4<1Kj1aVo2568y!JcG$hUp3#qG9$FdxkwH)T`+z z{x|j|c|%z2;TD{VKY&M#W`5WN(7qiomH4FcQC*#1Gf zkv;osw#al7y^3Dlf^5U$0kA*2xoju?6=PE6z-vK%esQU@yrR_UbXB^$JZ-^npLB71 zLS2RVlK5-sb?AMuZj*GF!}MBuz1V-2?xTykf!-u6>PGhbAibHrAh_NCCQ{VKt@Pp% zExggko{sV!pgSS;PBe@mI!w3HJLopLo$jD_vX|J)>=kx^9b~Vv*VrL;_)eHicOkgm zP4A)i();NB2(PK^b%fP7*k9NY_9i=u_Y)TS!j*nshwusqVFwH34j#d&*Vl&4ZchYp z5DSjfvBTB)76cP#3~|`kkRCKt)QD+R0NY1#ETW|{7E2Q4SR8GMO~w>*tOOxt^q1q; zZzLm3;)}2d-z=R|lX!7+6TQ6=uefH19~&%w2j=YEJ@~@nO!2leh}J9~=0a&+ZedP7 zw(i=>g2CmbzL3kAjZH^TyYvd?8-%Kj$%PWFTBC)qEuf62$o^W>HCdif&x61iLM zk+;eH@_@Ws9+F4oz49yNgYw7ZZ^%E8e<=S*{)znW@?R1Y61hZUVtQgpVpU>wVohRg zVqIc=Vpn2s;&q9e6K_o7lFUhEN!3ZAq$5eklYU4#p~z4;6i!91qCiokC{cJ6%M>dW zs}*Y$OtDt6Ua>)Ox#A(kQN;&}4;3FPzEb?4_@`2)lq<(6IpuieB;_>a*~)p!24$0S zzOqHRNV!DmR(h0eN?y52xmvkK$&?o>FIBEnUZ&iryh6E2dA0I(<;%+B$}f|($)_hz zPi{?Ko4g}=XY#J(N0XmO-jlp9`I+QrlaD5Uko;-#=gD6t|1J3!6;UOqgbTcT^%`E@IFTXY9?@92){-qjt~eW};!t@>Pjk-kFeXqVx zf06zY{aXEc{RaKz`s?+(^t<)@^au1u^hfn?>yPQ*(|@M_LjPC&SNgB@-xxT9!%%1N z8F~%2ThTVoehP{TT4f_o*8D23QG`wawYXiTP~vx#k(>i1{M(W#*0M zE6khBo6R?xZ#Hi+-)i1z-etbqe6NLCG#0HzZ!uarED=kOrPtDDS!-EmS#Q~3xxsRy zmxEXEj)@ zR=ah)b%J$@b*eSnnqzfZ=UF#c@3%f>ecHO;`keIz>r2)*t?yeuw|-;&&U(Tovngz; zHnYuYOSfg%CfcUiifv`K3R~E=+P2BI*>;=lF5BIXV@p&v+R@XQ|wdi+4kx7QhT|*(mvB(ZLhJmL>oI literal 0 HcmV?d00001 diff --git a/IJSVGExample/IJSVGExample.xcodeproj/xcshareddata/xcschemes/IJSVGExample.xcscheme b/IJSVGExample/IJSVGExample.xcodeproj/xcshareddata/xcschemes/IJSVGExample.xcscheme new file mode 100644 index 00000000..67608cbf --- /dev/null +++ b/IJSVGExample/IJSVGExample.xcodeproj/xcshareddata/xcschemes/IJSVGExample.xcscheme @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/IJSVGExample/IJSVGExampleTests/AJ_Digital_Camera.svg b/IJSVGExample/IJSVGExampleTests/AJ_Digital_Camera.svg new file mode 100644 index 00000000..d3eeb4bf --- /dev/null +++ b/IJSVGExample/IJSVGExampleTests/AJ_Digital_Camera.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/IJSVGExample/IJSVGExampleTests/Group.svg b/IJSVGExample/IJSVGExampleTests/Group.svg new file mode 100644 index 00000000..435ffa46 --- /dev/null +++ b/IJSVGExample/IJSVGExampleTests/Group.svg @@ -0,0 +1,13 @@ + + + + Group + Created with Sketch. + + + + + + + + \ No newline at end of file diff --git a/IJSVGExample/IJSVGExampleTests/IJSVGWebKitPSNRTests.m b/IJSVGExample/IJSVGExampleTests/IJSVGWebKitPSNRTests.m new file mode 100644 index 00000000..4fa03eae --- /dev/null +++ b/IJSVGExample/IJSVGExampleTests/IJSVGWebKitPSNRTests.m @@ -0,0 +1,555 @@ +// +// IJSVGWebKitPSNRTests.m +// IJSVGExampleTests +// +// Compares IJSVG drawInRect:context: output against WebKit WKWebView snapshots +// using PSNR (Peak Signal-to-Noise Ratio) as the quality metric. +// +// Adapted from Sitely's TestIJSVGFilters.m — only the comparison +// infrastructure is kept here, plus a few example test cases that use SVGs +// already bundled in the IJSVGExample target. +// + +#import +#import +#import +#import +#import + +static const CGFloat kDefaultPSNRThreshold = 30.0; +static NSString * const kOutputFolder = @"IJSVGWebKitPSNRResults"; + + +#pragma mark - WebKit snapshot helper (navigation delegate) + +@interface _IJSVGWebKitSnapshotHelper : NSObject +@property (nonatomic, copy) void (^completionBlock)(void); +@property (nonatomic, assign) BOOL loaded; +@end + +@implementation _IJSVGWebKitSnapshotHelper + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + self.loaded = YES; + if (self.completionBlock) { + self.completionBlock(); + } +} + +- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + NSLog(@"WebKit navigation failed: %@", error); + self.loaded = YES; + if (self.completionBlock) { + self.completionBlock(); + } +} + +@end + + +#pragma mark - Test class + +@interface IJSVGWebKitPSNRTests : XCTestCase +@end + +@implementation IJSVGWebKitPSNRTests + + +#pragma mark - Output directory + ++ (NSString *)outputDirectory { + NSString *override = NSProcessInfo.processInfo.environment[@"IJSVG_OUTPUT_DIR"]; + if (override.length != 0) { + [[NSFileManager defaultManager] createDirectoryAtPath:override + withIntermediateDirectories:YES + attributes:nil + error:nil]; + return override; + } + NSString *desktop = [NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) firstObject]; + NSString *dir = [desktop stringByAppendingPathComponent:kOutputFolder]; + [[NSFileManager defaultManager] createDirectoryAtPath:dir + withIntermediateDirectories:YES + attributes:nil + error:nil]; + return dir; +} + ++ (NSString *)webKitSandboxHomeDirectory { + NSString *dir = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ijsvg-webkit-home"]; + NSFileManager *fileManager = [NSFileManager defaultManager]; + [fileManager createDirectoryAtPath:[dir stringByAppendingPathComponent:@"Library/Caches/com.apple.dt.xctest.tool/WebKit"] + withIntermediateDirectories:YES + attributes:nil + error:nil]; + [fileManager createDirectoryAtPath:[dir stringByAppendingPathComponent:@"Library/WebKit"] + withIntermediateDirectories:YES + attributes:nil + error:nil]; + return dir; +} + ++ (NSTimeInterval)webKitRenderSettleDelay { + NSString *override = NSProcessInfo.processInfo.environment[@"IJSVG_WEBKIT_SETTLE_DELAY"]; + if (override.length != 0) { + double parsed = override.doubleValue; + if (parsed >= 0.0) { + return parsed; + } + } + return 2.0; +} + + +#pragma mark - SVG loading + +- (NSString *)svgStringForBundledResource:(NSString *)name { + NSBundle *bundle = [NSBundle bundleForClass:[self class]]; + NSString *path = [bundle pathForResource:name ofType:@"svg"]; + if (path == nil) { + XCTFail(@"Could not find bundled SVG resource '%@.svg'", name); + return nil; + } + NSError *error = nil; + NSString *svg = [NSString stringWithContentsOfFile:path + encoding:NSUTF8StringEncoding + error:&error]; + if (svg == nil) { + NSLog(@"Failed to load SVG %@: %@", path, error); + } + return svg; +} + + +#pragma mark - HTML / data URL helpers + +- (NSString *)webKitDataURLForSVGString:(NSString *)svgString { + NSString *normalized = [IJSVG normalizedSVGStringForEmbedding:svgString]; + NSData *data = [normalized dataUsingEncoding:NSUTF8StringEncoding]; + NSString *base64 = [data base64EncodedStringWithOptions:0]; + return [NSString stringWithFormat:@"data:image/svg+xml;charset=utf-8;base64,%@", base64]; +} + +- (NSString *)webKitHTMLForImageSource:(NSString *)imageSource size:(CGSize)size { + return [NSString stringWithFormat: + @"" + @"" + @"
svg
", + (int)size.width, (int)size.height, + (int)size.width, (int)size.height, imageSource]; +} + +- (NSString *)webKitHTMLForInlineSVGString:(NSString *)svgString size:(CGSize)size { + NSString *normalized = [IJSVG normalizedSVGStringForEmbedding:svgString]; + return [NSString stringWithFormat: + @"" + @"" + @"
%@
", + (int)size.width, (int)size.height, + (int)size.width, (int)size.height, normalized]; +} + +- (BOOL)webKitImageElementLoaded:(WKWebView *)webView { + __block BOOL loaded = NO; + XCTestExpectation *jsExp = [self expectationWithDescription:@"WebKit image state"]; + NSString *script = @"(function(){var img=document.querySelector('img'); return !!img && img.complete && img.naturalWidth > 0 && img.naturalHeight > 0;})();"; + [webView evaluateJavaScript:script completionHandler:^(id _Nullable value, NSError * _Nullable error) { + if([value isKindOfClass:NSNumber.class]) { + loaded = ((NSNumber *)value).boolValue; + } + [jsExp fulfill]; + }]; + [self waitForExpectations:@[jsExp] timeout:10.0]; + return loaded; +} + + +#pragma mark - IJSVG rendering (2x bitmap with white background) + +- (CGImageRef)renderIJSVG:(NSString *)svgString size:(CGSize)size CF_RETURNS_RETAINED { + IJSVG *svg = [[IJSVG alloc] initWithSVGString:svgString]; + XCTAssertNotNil(svg, @"IJSVG failed to parse SVG string"); + if (!svg) return NULL; + + svg.renderingBackingScaleHelper = ^CGFloat { + return 2.0; + }; + + NSUInteger pixelW = (NSUInteger)(size.width * 2); + NSUInteger pixelH = (NSUInteger)(size.height * 2); + + CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + CGContextRef ctx = CGBitmapContextCreate(NULL, pixelW, pixelH, 8, + pixelW * 4, + cs, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease(cs); + if (!ctx) return NULL; + + CGContextSetRGBFillColor(ctx, 1, 1, 1, 1); + CGContextFillRect(ctx, CGRectMake(0, 0, pixelW, pixelH)); + + CGContextTranslateCTM(ctx, 0, pixelH); + CGContextScaleCTM(ctx, 2.0, -2.0); + + [svg drawInRect:CGRectMake(0, 0, size.width, size.height) context:ctx]; + + CGImageRef image = CGBitmapContextCreateImage(ctx); + CGContextRelease(ctx); + return image; +} + + +#pragma mark - WebKit rendering (snapshot with white background) + +- (CGImageRef)renderWebKit:(NSString *)svgString size:(CGSize)size CF_RETURNS_RETAINED { + __block CGImageRef resultImage = NULL; + NSString *svgDataURLString = [self webKitDataURLForSVGString:svgString]; + + void (^work)(void) = ^{ + static dispatch_once_t onceToken; + static NSWindow *window = nil; + static WKWebView *webView = nil; + static _IJSVGWebKitSnapshotHelper *delegate = nil; + + dispatch_once(&onceToken, ^{ + NSString *webKitHome = [IJSVGWebKitPSNRTests webKitSandboxHomeDirectory]; + setenv("CFFIXED_USER_HOME", webKitHome.fileSystemRepresentation, 1); + setenv("HOME", webKitHome.fileSystemRepresentation, 1); + + NSRect windowRect = NSMakeRect(-10000, -10000, 256, 256); + window = [[NSWindow alloc] initWithContentRect:windowRect + styleMask:NSWindowStyleMaskBorderless + backing:NSBackingStoreBuffered + defer:NO]; + [window setBackgroundColor:[NSColor whiteColor]]; + + WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; + config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; + webView = [[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 256, 256) + configuration:config]; + [[window contentView] addSubview:webView]; + [window orderBack:nil]; + + delegate = [[_IJSVGWebKitSnapshotHelper alloc] init]; + webView.navigationDelegate = delegate; + }); + + [window setFrame:NSMakeRect(-10000, -10000, size.width, size.height) display:NO]; + webView.frame = NSMakeRect(0, 0, size.width, size.height); + + // Load the SVG via an data URL so WebKit uses its SVG parser + // rather than HTML inline-SVG parsing. + NSString *html = [self webKitHTMLForImageSource:svgDataURLString + size:size]; + + XCTestExpectation *loadExp = [self expectationWithDescription:@"WebKit page loaded"]; + delegate.loaded = NO; + delegate.completionBlock = ^{ + [loadExp fulfill]; + }; + + [webView loadHTMLString:html baseURL:nil]; + [self waitForExpectations:@[loadExp] timeout:10.0]; + + if([self webKitImageElementLoaded:webView] == NO) { + // Fall back to inline SVG if didn't load. + NSString *fallbackHTML = [self webKitHTMLForInlineSVGString:svgString + size:size]; + XCTestExpectation *fallbackExp = [self expectationWithDescription:@"WebKit fallback loaded"]; + delegate.loaded = NO; + delegate.completionBlock = ^{ + [fallbackExp fulfill]; + }; + [webView loadHTMLString:fallbackHTML baseURL:nil]; + [self waitForExpectations:@[fallbackExp] timeout:10.0]; + } + + NSTimeInterval settleDelay = [IJSVGWebKitPSNRTests webKitRenderSettleDelay]; + if (settleDelay > 0.0) { + XCTestExpectation *renderDelay = [self expectationWithDescription:@"render settle"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(settleDelay * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [renderDelay fulfill]; + }); + [self waitForExpectations:@[renderDelay] timeout:MAX(10.0, settleDelay + 1.0)]; + } + + WKSnapshotConfiguration *snapConfig = [[WKSnapshotConfiguration alloc] init]; + snapConfig.snapshotWidth = @(size.width); + + XCTestExpectation *snapExp = [self expectationWithDescription:@"WebKit snapshot"]; + + [webView takeSnapshotWithConfiguration:snapConfig + completionHandler:^(NSImage * _Nullable snapshotImage, + NSError * _Nullable error) { + if (error) { + NSLog(@"Snapshot error: %@", error); + } + if (snapshotImage) { + NSUInteger targetW = (NSUInteger)(size.width * 2); + NSUInteger targetH = (NSUInteger)(size.height * 2); + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef bmpCtx = CGBitmapContextCreate(NULL, targetW, targetH, 8, + targetW * 4, cs, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease(cs); + + if (bmpCtx) { + CGContextSetRGBFillColor(bmpCtx, 1, 1, 1, 1); + CGContextFillRect(bmpCtx, CGRectMake(0, 0, targetW, targetH)); + + NSGraphicsContext *nsCtx = [NSGraphicsContext graphicsContextWithCGContext:bmpCtx flipped:NO]; + [NSGraphicsContext saveGraphicsState]; + [NSGraphicsContext setCurrentContext:nsCtx]; + [snapshotImage drawInRect:NSMakeRect(0, 0, targetW, targetH) + fromRect:NSZeroRect + operation:NSCompositingOperationSourceOver + fraction:1.0]; + [NSGraphicsContext restoreGraphicsState]; + + resultImage = CGBitmapContextCreateImage(bmpCtx); + CGContextRelease(bmpCtx); + } + } + [snapExp fulfill]; + }]; + + [self waitForExpectations:@[snapExp] timeout:10.0]; + }; + + if ([NSThread isMainThread]) { + work(); + } else { + dispatch_sync(dispatch_get_main_queue(), work); + } + + return resultImage; +} + + +#pragma mark - PSNR computation + +- (double)psnrBetweenImageA:(CGImageRef)imageA imageB:(CGImageRef)imageB { + if (!imageA || !imageB) return 0.0; + + size_t wA = CGImageGetWidth(imageA); + size_t hA = CGImageGetHeight(imageA); + size_t wB = CGImageGetWidth(imageB); + size_t hB = CGImageGetHeight(imageB); + + size_t w = MIN(wA, wB); + size_t h = MIN(hA, hB); + + size_t bytesPerRow = w * 4; + size_t bufSize = bytesPerRow * h; + + uint8_t *bufA = (uint8_t *)calloc(1, bufSize); + uint8_t *bufB = (uint8_t *)calloc(1, bufSize); + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctxA = CGBitmapContextCreate(bufA, w, h, 8, bytesPerRow, cs, + kCGImageAlphaPremultipliedLast); + CGContextRef ctxB = CGBitmapContextCreate(bufB, w, h, 8, bytesPerRow, cs, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease(cs); + + CGContextDrawImage(ctxA, CGRectMake(0, 0, w, h), imageA); + CGContextDrawImage(ctxB, CGRectMake(0, 0, w, h), imageB); + + CGContextRelease(ctxA); + CGContextRelease(ctxB); + + double sumSqDiff = 0.0; + NSUInteger pixelCount = 0; + + for (size_t y = 0; y < h; y++) { + for (size_t x = 0; x < w; x++) { + size_t idx = (y * bytesPerRow) + (x * 4); + uint8_t rA = bufA[idx], gA = bufA[idx+1], bA = bufA[idx+2], aA = bufA[idx+3]; + uint8_t rB = bufB[idx], gB = bufB[idx+1], bB = bufB[idx+2], aB = bufB[idx+3]; + + // Only compare pixels where at least one image has alpha > 0 + if (aA > 0 || aB > 0) { + double dr = (double)rA - (double)rB; + double dg = (double)gA - (double)gB; + double db = (double)bA - (double)bB; + double da = (double)aA - (double)aB; + sumSqDiff += dr*dr + dg*dg + db*db + da*da; + pixelCount++; + } + } + } + + free(bufA); + free(bufB); + + if (pixelCount == 0) return 100.0; + + double mse = sumSqDiff / (pixelCount * 4.0); + if (mse == 0.0) return 100.0; + + double psnr = 10.0 * log10((255.0 * 255.0) / mse); + return psnr; +} + + +#pragma mark - Diff image generation + +- (CGImageRef)diffImageBetweenA:(CGImageRef)imageA B:(CGImageRef)imageB CF_RETURNS_RETAINED { + if (!imageA || !imageB) return NULL; + + size_t w = MIN(CGImageGetWidth(imageA), CGImageGetWidth(imageB)); + size_t h = MIN(CGImageGetHeight(imageA), CGImageGetHeight(imageB)); + size_t bytesPerRow = w * 4; + size_t bufSize = bytesPerRow * h; + + uint8_t *bufA = (uint8_t *)calloc(1, bufSize); + uint8_t *bufB = (uint8_t *)calloc(1, bufSize); + uint8_t *bufD = (uint8_t *)calloc(1, bufSize); + + CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB(); + CGContextRef ctxA = CGBitmapContextCreate(bufA, w, h, 8, bytesPerRow, cs, + kCGImageAlphaPremultipliedLast); + CGContextRef ctxB = CGBitmapContextCreate(bufB, w, h, 8, bytesPerRow, cs, + kCGImageAlphaPremultipliedLast); + CGContextRef ctxD = CGBitmapContextCreate(bufD, w, h, 8, bytesPerRow, cs, + kCGImageAlphaPremultipliedLast); + CGColorSpaceRelease(cs); + + CGContextDrawImage(ctxA, CGRectMake(0, 0, w, h), imageA); + CGContextDrawImage(ctxB, CGRectMake(0, 0, w, h), imageB); + + CGContextRelease(ctxA); + CGContextRelease(ctxB); + + for (size_t i = 0; i < bufSize; i += 4) { + int dr = abs((int)bufA[i] - (int)bufB[i]); + int dg = abs((int)bufA[i+1] - (int)bufB[i+1]); + int db = abs((int)bufA[i+2] - (int)bufB[i+2]); + // Amplify differences for visibility (x4) + bufD[i] = (uint8_t)MIN(255, dr * 4); + bufD[i+1] = (uint8_t)MIN(255, dg * 4); + bufD[i+2] = (uint8_t)MIN(255, db * 4); + bufD[i+3] = 255; + } + + CGImageRef diffImage = CGBitmapContextCreateImage(ctxD); + CGContextRelease(ctxD); + + free(bufA); + free(bufB); + free(bufD); + + return diffImage; +} + + +#pragma mark - PNG saving + +- (void)saveCGImage:(CGImageRef)image toPath:(NSString *)path { + if (!image) return; + NSURL *url = [NSURL fileURLWithPath:path]; + CGImageDestinationRef dest = CGImageDestinationCreateWithURL((__bridge CFURLRef)url, + CFSTR("public.png"), 1, NULL); + if (dest) { + CGImageDestinationAddImage(dest, image, NULL); + CGImageDestinationFinalize(dest); + CFRelease(dest); + } +} + + +#pragma mark - Core comparison + +- (void)compareSVGString:(NSString *)svgString + name:(NSString *)name + size:(CGSize)size + psnrThreshold:(double)threshold { + CGImageRef ijsvgImage = [self renderIJSVG:svgString size:size]; + XCTAssertTrue(ijsvgImage != NULL, @"IJSVG render failed for %@", name); + if (ijsvgImage == NULL) { + return; + } + + CGImageRef webkitImage = [self renderWebKit:svgString size:size]; + XCTAssertTrue(webkitImage != NULL, @"WebKit render failed for %@", name); + if (webkitImage == NULL) { + CGImageRelease(ijsvgImage); + return; + } + + NSString *outDir = [IJSVGWebKitPSNRTests outputDirectory]; + [self saveCGImage:ijsvgImage toPath:[outDir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@-ijsvg.png", name]]]; + [self saveCGImage:webkitImage toPath:[outDir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@-webkit.png", name]]]; + + CGImageRef diffImage = [self diffImageBetweenA:ijsvgImage B:webkitImage]; + if (diffImage) { + [self saveCGImage:diffImage toPath:[outDir stringByAppendingPathComponent: + [NSString stringWithFormat:@"%@-diff.png", name]]]; + CGImageRelease(diffImage); + } + + double psnr = [self psnrBetweenImageA:ijsvgImage imageB:webkitImage]; + NSLog(@"[%@] PSNR = %.2f dB IJSVG: %zux%zu WebKit: %zux%zu", + name, psnr, + CGImageGetWidth(ijsvgImage), CGImageGetHeight(ijsvgImage), + CGImageGetWidth(webkitImage), CGImageGetHeight(webkitImage)); + + NSString *psnrLine = [NSString stringWithFormat:@"[%@] PSNR = %.2f dB\n", name, psnr]; + NSString *logPath = [outDir stringByAppendingPathComponent:@"psnr-results.txt"]; + NSFileHandle *fh = [NSFileHandle fileHandleForWritingAtPath:logPath]; + if (fh == nil) { + [psnrLine writeToFile:logPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; + } else { + [fh seekToEndOfFile]; + [fh writeData:[psnrLine dataUsingEncoding:NSUTF8StringEncoding]]; + [fh closeFile]; + } + + CGImageRelease(ijsvgImage); + CGImageRelease(webkitImage); + + XCTAssertGreaterThanOrEqual(psnr, threshold, + @"%@: PSNR %.2f dB is below threshold %.2f dB", + name, psnr, threshold); +} + +- (void)compareBundledSVG:(NSString *)resourceName + size:(CGSize)size + psnrThreshold:(double)threshold { + NSString *svg = [self svgStringForBundledResource:resourceName]; + if (svg == nil) { + return; + } + [self compareSVGString:svg name:resourceName size:size psnrThreshold:threshold]; +} + + +#pragma mark - Example test methods (use SVGs already bundled in IJSVGExample) + +- (void)testRectangle { + [self compareBundledSVG:@"Rectangle" size:CGSizeMake(256, 256) psnrThreshold:kDefaultPSNRThreshold]; +} + +- (void)testGroup { + [self compareBundledSVG:@"Group" size:CGSizeMake(256, 256) psnrThreshold:kDefaultPSNRThreshold]; +} + +- (void)testAJDigitalCamera { + [self compareBundledSVG:@"AJ_Digital_Camera" size:CGSizeMake(256, 256) psnrThreshold:kDefaultPSNRThreshold]; +} + +- (void)testNewTux { + [self compareBundledSVG:@"NewTux" size:CGSizeMake(256, 256) psnrThreshold:kDefaultPSNRThreshold]; +} + +@end diff --git a/IJSVGExample/IJSVGExampleTests/NewTux.svg b/IJSVGExample/IJSVGExampleTests/NewTux.svg new file mode 100644 index 00000000..0718b1a5 --- /dev/null +++ b/IJSVGExample/IJSVGExampleTests/NewTux.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/IJSVGExample/IJSVGExampleTests/Rectangle.svg b/IJSVGExample/IJSVGExampleTests/Rectangle.svg new file mode 100644 index 00000000..5cb0304d --- /dev/null +++ b/IJSVGExample/IJSVGExampleTests/Rectangle.svg @@ -0,0 +1,19 @@ + + + + Rectangle + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file