From b9959c779ccc3370c67050bba0586a2d81ecece9 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 28 Dec 2017 05:23:38 +0200 Subject: [PATCH 01/14] Implement tailor made data structure and logic for text on a path rendering. Remove postinstall script and dependencies on PerformanceBezier & QuartzBookPack. --- ios/RNSVG.xcodeproj/project.pbxproj | 114 ++----------- ios/Text/RNSVGTSpan.h | 3 +- ios/Text/RNSVGTSpan.m | 66 +++++-- ios/Utils/BezierElement.h | 26 +++ ios/Utils/BezierElement.m | 62 +++++++ ios/Utils/UIBezierPath+TextRendering.h | 12 ++ ios/Utils/UIBezierPath+TextRendering.m | 227 +++++++++++++++++++++++++ package.json | 3 +- scripts/install.js | 20 --- 9 files changed, 396 insertions(+), 137 deletions(-) create mode 100644 ios/Utils/BezierElement.h create mode 100644 ios/Utils/BezierElement.m create mode 100644 ios/Utils/UIBezierPath+TextRendering.h create mode 100644 ios/Utils/UIBezierPath+TextRendering.m delete mode 100644 scripts/install.js diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index 3044c51e..a5dca387 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -49,14 +49,12 @@ 7F08CE9B1E23476900650F83 /* RNSVGTSpanManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F08CE991E23476900650F83 /* RNSVGTSpanManager.m */; }; 7F08CEA01E23479700650F83 /* RNSVGTextPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F08CE9D1E23479700650F83 /* RNSVGTextPath.m */; }; 7F08CEA11E23479700650F83 /* RNSVGTSpan.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F08CE9F1E23479700650F83 /* RNSVGTSpan.m */; }; - 7F4BB50A1FB1E50000663D5F /* QuartzBookPack.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F4BB5051FB1DEC300663D5F /* QuartzBookPack.framework */; }; 7F9CDAFA1E1F809C00E0C805 /* RNSVGPathParser.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */; }; 7FC260CE1E3499BC00A39833 /* RNSVGViewBox.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FC260CD1E3499BC00A39833 /* RNSVGViewBox.m */; }; 7FC260D11E34A12000A39833 /* RNSVGSymbol.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FC260D01E34A12000A39833 /* RNSVGSymbol.m */; }; 7FC260D41E34A12A00A39833 /* RNSVGSymbolManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 7FC260D31E34A12A00A39833 /* RNSVGSymbolManager.m */; }; 945A8AF81F4CE3E8004BBF6B /* AlignmentBaseline.m in Sources */ = {isa = PBXBuildFile; fileRef = 945A8AF71F4CE3E8004BBF6B /* AlignmentBaseline.m */; }; 945A8AF91F4CE3E8004BBF6B /* AlignmentBaseline.m in Sources */ = {isa = PBXBuildFile; fileRef = 945A8AF71F4CE3E8004BBF6B /* AlignmentBaseline.m */; }; - 9494C47A1F47116800D5BCFD /* PerformanceBezier.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4771F4710FE00D5BCFD /* PerformanceBezier.framework */; }; 9494C4D81F473BA700D5BCFD /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */; }; 9494C4DA1F473BCB00D5BCFD /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4D91F473BCB00D5BCFD /* CoreText.framework */; }; 9494C4DC1F473BD900D5BCFD /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */; }; @@ -89,6 +87,10 @@ 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */; }; 9494C5481F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; + 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; + 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; + 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; + 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; A361E76E1EB0C33D00646005 /* RNSVGTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D331CE74E3100887C2B /* RNSVGTextManager.m */; }; A361E76F1EB0C33D00646005 /* RNSVGImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1039D2841CE71EB7001E90A8 /* RNSVGImage.m */; }; A361E7701EB0C33D00646005 /* RNSVGRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D471CE74E3D00887C2B /* RNSVGRect.m */; }; @@ -137,37 +139,6 @@ A361E79D1EB0C33D00646005 /* RNSVGDefsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1023B48C1D3DDCCE0051496D /* RNSVGDefsManager.m */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - 7F4BB5041FB1DEC300663D5F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 7F917B141FB1D5E900A75AA4; - remoteInfo = QuartzBookPack; - }; - 7F4BB5061FB1DEC300663D5F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 7F917B1D1FB1D5E900A75AA4; - remoteInfo = QuartzBookPackTests; - }; - 9494C4761F4710FE00D5BCFD /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 66F2EBE31A8DC05100D536E9; - remoteInfo = PerformanceBezier; - }; - 9494C4781F4710FE00D5BCFD /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 66B9D28C1A8D5FDE00CAC341; - remoteInfo = PerformanceBezierTests; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 0CF68ABF1AF0540F00FF9E5C /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; @@ -281,7 +252,6 @@ 7F08CE9E1E23479700650F83 /* RNSVGTSpan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGTSpan.h; path = Text/RNSVGTSpan.h; sourceTree = ""; }; 7F08CE9F1E23479700650F83 /* RNSVGTSpan.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGTSpan.m; path = Text/RNSVGTSpan.m; sourceTree = ""; }; 7F08CEA31E23481F00650F83 /* RNSVGTextAnchor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGTextAnchor.h; path = Utils/RNSVGTextAnchor.h; sourceTree = ""; }; - 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = QuartzBookPack.xcodeproj; path = QuartzBookPack/QuartzBookPack.xcodeproj; sourceTree = ""; }; 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGUnits.h; path = Utils/RNSVGUnits.h; sourceTree = ""; }; 7F9CDAF81E1F809C00E0C805 /* RNSVGPathParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGPathParser.h; path = Utils/RNSVGPathParser.h; sourceTree = ""; }; 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGPathParser.m; path = Utils/RNSVGPathParser.m; sourceTree = ""; }; @@ -293,7 +263,6 @@ 7FC260D31E34A12A00A39833 /* RNSVGSymbolManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNSVGSymbolManager.m; sourceTree = ""; }; 945A8AF71F4CE3E8004BBF6B /* AlignmentBaseline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AlignmentBaseline.m; path = Text/AlignmentBaseline.m; sourceTree = ""; }; 945A8AFA1F4CE41E004BBF6B /* AlignmentBaseline.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = AlignmentBaseline.h; path = Text/AlignmentBaseline.h; sourceTree = ""; }; - 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = PerformanceBezier.xcodeproj; path = PerformanceBezier/PerformanceBezier.xcodeproj; sourceTree = ""; }; 9494C4D71F473BA700D5BCFD /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; 9494C4D91F473BCB00D5BCFD /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 9494C4DB1F473BD900D5BCFD /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -327,6 +296,10 @@ 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSide.m; path = Text/TextPathSide.m; sourceTree = ""; }; 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSpacing.m; path = Text/TextPathSpacing.m; sourceTree = ""; }; 94DDAC5C1F3D024300EED511 /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRNSVG-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIBezierPath+TextRendering.h"; path = "Utils/UIBezierPath+TextRendering.h"; sourceTree = ""; }; + 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIBezierPath+TextRendering.m"; path = "Utils/UIBezierPath+TextRendering.m"; sourceTree = ""; }; + 94EB936B1FF4916F00C0B251 /* BezierElement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BezierElement.m; path = Utils/BezierElement.m; sourceTree = ""; }; + 94EB93701FF4918D00C0B251 /* BezierElement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BezierElement.h; path = Utils/BezierElement.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -334,14 +307,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7F4BB50A1FB1E50000663D5F /* QuartzBookPack.framework in Frameworks */, 9494C4E01F473BED00D5BCFD /* Accelerate.framework in Frameworks */, 9494C4D81F473BA700D5BCFD /* QuartzCore.framework in Frameworks */, 9494C4DA1F473BCB00D5BCFD /* CoreText.framework in Frameworks */, 9494C4DC1F473BD900D5BCFD /* CoreGraphics.framework in Frameworks */, 9494C4DE1F473BDD00D5BCFD /* UIKit.framework in Frameworks */, 9494C4E21F473BF500D5BCFD /* Foundation.framework in Frameworks */, - 9494C47A1F47116800D5BCFD /* PerformanceBezier.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -358,8 +329,6 @@ 0CF68AB81AF0540F00FF9E5C = { isa = PBXGroup; children = ( - 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */, - 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */, 1039D29A1CE7212C001E90A8 /* Utils */, 1039D2801CE71DCF001E90A8 /* Elements */, 1039D27F1CE71D9B001E90A8 /* Text */, @@ -545,19 +514,14 @@ 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */, 1039D29B1CE72177001E90A8 /* RCTConvert+RNSVG.h */, 1039D29C1CE72177001E90A8 /* RCTConvert+RNSVG.m */, + 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */, + 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */, + 94EB936B1FF4916F00C0B251 /* BezierElement.m */, + 94EB93701FF4918D00C0B251 /* BezierElement.h */, ); name = Utils; sourceTree = ""; }; - 7F4BB5001FB1DEC300663D5F /* Products */ = { - isa = PBXGroup; - children = ( - 7F4BB5051FB1DEC300663D5F /* QuartzBookPack.framework */, - 7F4BB5071FB1DEC300663D5F /* QuartzBookPackTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; 9494C2B31F46139600D5BCFD /* Frameworks */ = { isa = PBXGroup; children = ( @@ -571,15 +535,6 @@ name = Frameworks; sourceTree = ""; }; - 9494C4721F4710FE00D5BCFD /* Products */ = { - isa = PBXGroup; - children = ( - 9494C4771F4710FE00D5BCFD /* PerformanceBezier.framework */, - 9494C4791F4710FE00D5BCFD /* PerformanceBezierTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -640,16 +595,6 @@ mainGroup = 0CF68AB81AF0540F00FF9E5C; productRefGroup = 0CF68AC21AF0540F00FF9E5C /* Products */; projectDirPath = ""; - projectReferences = ( - { - ProductGroup = 9494C4721F4710FE00D5BCFD /* Products */; - ProjectRef = 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */; - }, - { - ProductGroup = 7F4BB5001FB1DEC300663D5F /* Products */; - ProjectRef = 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */; - }, - ); projectRoot = ""; targets = ( 0CF68AC01AF0540F00FF9E5C /* RNSVG */, @@ -658,37 +603,6 @@ }; /* End PBXProject section */ -/* Begin PBXReferenceProxy section */ - 7F4BB5051FB1DEC300663D5F /* QuartzBookPack.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework; - path = QuartzBookPack.framework; - remoteRef = 7F4BB5041FB1DEC300663D5F /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 7F4BB5071FB1DEC300663D5F /* QuartzBookPackTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = QuartzBookPackTests.xctest; - remoteRef = 7F4BB5061FB1DEC300663D5F /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9494C4771F4710FE00D5BCFD /* PerformanceBezier.framework */ = { - isa = PBXReferenceProxy; - fileType = wrapper.framework.static; - path = PerformanceBezier.framework; - remoteRef = 9494C4761F4710FE00D5BCFD /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; - 9494C4791F4710FE00D5BCFD /* PerformanceBezierTests.xctest */ = { - isa = PBXReferenceProxy; - fileType = wrapper.cfbundle; - path = PerformanceBezierTests.xctest; - remoteRef = 9494C4781F4710FE00D5BCFD /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; -/* End PBXReferenceProxy section */ - /* Begin PBXSourcesBuildPhase section */ 0CF68ABD1AF0540F00FF9E5C /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -700,6 +614,7 @@ 10BA0D341CE74E3100887C2B /* RNSVGCircleManager.m in Sources */, 10BEC1BC1D3F66F500FDCB19 /* RNSVGLinearGradient.m in Sources */, 9494C5461F4C44DD00D5BCFD /* TextPathSide.m in Sources */, + 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 1039D2B01CE72F27001E90A8 /* RNSVGPercentageConverter.m in Sources */, 9494C53C1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 10BA0D491CE74E3D00887C2B /* RNSVGEllipse.m in Sources */, @@ -734,6 +649,7 @@ 9494C5251F4B605F00D5BCFD /* GlyphContext.m in Sources */, 10BA0D481CE74E3D00887C2B /* RNSVGCircle.m in Sources */, 9494C5401F4C44DD00D5BCFD /* TextLengthAdjust.m in Sources */, + 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */, 10BA0D351CE74E3100887C2B /* RNSVGEllipseManager.m in Sources */, 1039D2A01CE72177001E90A8 /* RCTConvert+RNSVG.m in Sources */, 9494C4FF1F4B5BE800D5BCFD /* FontData.m in Sources */, @@ -767,6 +683,7 @@ A361E7711EB0C33D00646005 /* RNSVGCircleManager.m in Sources */, A361E7721EB0C33D00646005 /* RNSVGLinearGradient.m in Sources */, A361E7731EB0C33D00646005 /* RNSVGPercentageConverter.m in Sources */, + 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 9494C53F1F4C44DD00D5BCFD /* TextDecoration.m in Sources */, A361E7751EB0C33D00646005 /* RNSVGEllipse.m in Sources */, A361E7761EB0C33D00646005 /* RNSVGPath.m in Sources */, @@ -801,6 +718,7 @@ 9494C53B1F4C44DD00D5BCFD /* FontVariantLigatures.m in Sources */, 9494C5001F4B5BE800D5BCFD /* FontData.m in Sources */, 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */, + 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */, A361E78D1EB0C33D00646005 /* RNSVGLineManager.m in Sources */, 9494C53D1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */, diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h index 0f15645b..0b1c7c9e 100644 --- a/ios/Text/RNSVGTSpan.h +++ b/ios/Text/RNSVGTSpan.h @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ #import -#import #import #import #import "RNSVGText.h" @@ -16,6 +15,8 @@ #import "TextPathSpacing.h" #import "TextLengthAdjust.h" #import "AlignmentBaseline.h" +#import "UIBezierPath+TextRendering.h" + @interface RNSVGTSpan : RNSVGText diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index d23da576..ed52be91 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -5,8 +5,6 @@ * This source code is licensed under the MIT-style license found in the * LICENSE file in the root directory of this source tree. */ -#import -#import #import "RNSVGTSpan.h" #import "RNSVGText.h" #import "RNSVGTextPath.h" @@ -17,12 +15,13 @@ NSCharacterSet *separators = nil; @implementation RNSVGTSpan { CGFloat startOffset; - UIBezierPath *_bezierPath; CGPathRef _cache; CGFloat _pathLength; - RNSVGTextPath * textPath; - RNSVGPath * textPathPath; - bool isClosed; + RNSVGTextPath *textPath; + NSMutableArray *lengths; + NSMutableArray *lines; + NSInteger lineCount; + BOOL isClosed; } - (id)init @@ -295,7 +294,7 @@ NSCharacterSet *separators = nil; double textMeasure = CGRectGetWidth(textBounds); double offset = getTextAnchorOffset(textAnchor, textMeasure); - bool hasTextPath = _bezierPath != nil; + bool hasTextPath = textPath != nil; int side = 1; double startOfRendering = 0; @@ -786,9 +785,31 @@ NSCharacterSet *separators = nil; continue; } + int i = 0; + CGFloat totalLength = 0; + CGFloat prevLength = 0; + + // TODO investigate at what lineCount a binary search is faster + while (i < lineCount - 1) { + prevLength = totalLength; + totalLength = [[lengths objectAtIndex: i] floatValue]; + if (totalLength < midPoint) { + i++; + } else { + break; + } + }; + + CGFloat length = totalLength - prevLength; + CGFloat targetPercent = (midPoint - prevLength) / length; + + NSArray * points = [lines objectAtIndex: i]; + CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; + CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; + CGPoint slope; - CGFloat percentConsumed = midPoint / _pathLength; - CGPoint mid = [_bezierPath pointAtPercent:percentConsumed withSlope:&slope]; + CGPoint mid = InterpolateLineSegment(p1, p2, targetPercent, &slope); + // Calculate the rotation double angle = atan2(slope.y, slope.x); @@ -814,6 +835,20 @@ NSCharacterSet *separators = nil; return path; } +CGPoint InterpolateLineSegment(CGPoint p1, CGPoint p2, CGFloat percent, CGPoint *slope) +{ + CGFloat dx = p2.x - p1.x; + CGFloat dy = p2.y - p1.y; + + if (slope) + *slope = CGPointMake(dx, dy); + + CGFloat px = p1.x + dx * percent; + CGFloat py = p1.y + dy * percent; + + return CGPointMake(px, py); +} + CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) { switch (textAnchor) { @@ -830,17 +865,16 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) - (void)setupTextPath:(CGContextRef)context { - _bezierPath = nil; textPath = nil; - textPathPath = nil; [self traverseTextSuperviews:^(__kindof RNSVGText *node) { if ([node class] == [RNSVGTextPath class]) { textPath = (RNSVGTextPath*) node; - textPathPath = [textPath getPath]; - _bezierPath = [UIBezierPath bezierPathWithCGPath:[textPathPath getPath:nil]]; - _bezierPath = [_bezierPath bezierPathByFlatteningPathAndImmutable:YES]; - _pathLength = _bezierPath.pathLength; - isClosed = [_bezierPath isClosed]; + RNSVGPath *svgPath = [textPath getPath]; + UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:[svgPath getPath:nil]]; + + lines = [NSMutableArray array]; + lengths = [NSMutableArray array]; + [bezierPath getTextProperties](&_pathLength, &lineCount, lengths, lines, &isClosed); return NO; } return YES; diff --git a/ios/Utils/BezierElement.h b/ios/Utils/BezierElement.h new file mode 100644 index 00000000..17af3394 --- /dev/null +++ b/ios/Utils/BezierElement.h @@ -0,0 +1,26 @@ +/* + + Erica Sadun, http://ericasadun.com + + */ + + +#import +#import + +#define NULLPOINT CGRectNull.origin +#define POINT_IS_NULL(_POINT_) CGPointEqualToPoint(_POINT_, NULLPOINT) + +@interface BezierElement : NSObject + +// Element storage +@property (nonatomic, assign) CGPathElementType elementType; +@property (nonatomic, assign) CGPoint point; +@property (nonatomic, assign) CGPoint controlPoint1; +@property (nonatomic, assign) CGPoint controlPoint2; + +// Instance creation ++ (instancetype) elementWithPathElement: (CGPathElement) element; + +@end; + diff --git a/ios/Utils/BezierElement.m b/ios/Utils/BezierElement.m new file mode 100644 index 00000000..ab53465c --- /dev/null +++ b/ios/Utils/BezierElement.m @@ -0,0 +1,62 @@ +/* + + Erica Sadun, http://ericasadun.com + + */ + + +#import "BezierElement.h" + +#pragma mark - Bezier Element - + +@implementation BezierElement +- (instancetype) init +{ + self = [super init]; + if (self) + { + _elementType = kCGPathElementMoveToPoint; + _point = NULLPOINT; + _controlPoint1 = NULLPOINT; + _controlPoint2 = NULLPOINT; + } + return self; +} + ++ (instancetype) elementWithPathElement: (CGPathElement) element +{ + BezierElement *newElement = [[self alloc] init]; + newElement.elementType = element.type; + + switch (newElement.elementType) + { + case kCGPathElementCloseSubpath: + break; + case kCGPathElementMoveToPoint: + case kCGPathElementAddLineToPoint: + { + newElement.point = element.points[0]; + break; + } + case kCGPathElementAddQuadCurveToPoint: + { + newElement.point = element.points[1]; + newElement.controlPoint1 = element.points[0]; + break; + } + case kCGPathElementAddCurveToPoint: + { + newElement.point = element.points[2]; + newElement.controlPoint1 = element.points[0]; + newElement.controlPoint2 = element.points[1]; + break; + } + default: + break; + } + + return newElement; +} + +@end + diff --git a/ios/Utils/UIBezierPath+TextRendering.h b/ios/Utils/UIBezierPath+TextRendering.h new file mode 100644 index 00000000..88eadab2 --- /dev/null +++ b/ios/Utils/UIBezierPath+TextRendering.h @@ -0,0 +1,12 @@ +// +// UIBezierPath+TextRendering.h +// RNSVG +// +// Created by Mikael Sand on 27/12/2017. +// + +#import + +@interface UIBezierPath (TextRendering) +- (void (^)(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengthsP, NSMutableArray * linesP, BOOL *isClosedP)) getTextProperties; +@end diff --git a/ios/Utils/UIBezierPath+TextRendering.m b/ios/Utils/UIBezierPath+TextRendering.m new file mode 100644 index 00000000..d768d6cb --- /dev/null +++ b/ios/Utils/UIBezierPath+TextRendering.m @@ -0,0 +1,227 @@ +// +// UIBezierPath+TextRendering.m +// RNSVG +// +// Created by Mikael Sand on 27/12/2017. +// + +#import "UIBezierPath+TextRendering.h" +#import "BezierElement.h" + +static CGFloat idealFlatness = .01; + +/** + * returns the distance between two points + */ +CGFloat distance(CGPoint p1, CGPoint p2) +{ + CGFloat dx = p2.x - p1.x; + CGFloat dy = p2.y - p1.y; + + return sqrt(dx*dx + dy*dy); +} + +/** + * returns the dot product of two coordinates + */ +CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { + return p1.x * p2.x + p1.y * p2.y; +} + +/** + * returns the shortest distance from a point to a line + */ +CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ + CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); + CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); + CGFloat c1 = dotProduct(w, v); + CGFloat c2 = dotProduct(v, v); + CGFloat d; + if (c1 <= 0) { + d = distance(point, start); + } + else if (c2 <= c1) { + d = distance(point, end); + } + else { + CGFloat b = c1 / c2; + CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); + d = distance(point, Pb); + } + return d; +} + +/** + * calculate the point on a bezier at time t + * where 0 < t < 1 + */ +CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + CGPoint bez1[4]; + CGPoint bez2[4]; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; + + return CGPointMake(bez1[3].x, bez1[3].y); +} + +// Subdivide a Bézier (specific division) +void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + bez1[0].x = bez[0].x; + bez1[0].y = bez[0].y; + bez2[3].x = bez[3].x; + bez2[3].y = bez[3].y; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; +} + +void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { + NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; + [lines addObject:line]; + *length += distance(*last, *next); + [lengths addObject:[NSNumber numberWithDouble:*length]]; + *last = *next; +} + +// Convert one element to BezierElement and save to array +void GetBezierElements(void *info, const CGPathElement *element) +{ + NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; + if (element) + [bezierElements addObject:[BezierElement elementWithPathElement:*element]]; +} + +@implementation UIBezierPath (TextRendering) + +// Retrieve array of component elements +- (NSArray *) elements +{ + NSMutableArray *elements = [NSMutableArray array]; + CGPathApply(self.CGPath, (__bridge void *)elements, GetBezierElements); + return elements; +} + +- (void (^)(CGFloat *, NSInteger *, NSMutableArray *, NSMutableArray *, BOOL *)) getTextProperties{ + return ^(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengths, NSMutableArray * lines, BOOL *isClosedP) { + __block CGPoint origin = CGPointMake (0.0, 0.0); + __block CGPoint last = CGPointMake (0.0, 0.0); + __block NSInteger lineCount = 0; + __block CGFloat length = 0; + __block BOOL isClosed = NO; + NSArray * elements = self.elements; + for (BezierElement *element in elements) { + switch (element.elementType) + { + case kCGPathElementMoveToPoint: + origin = last = element.point; + break; + + case kCGPathElementAddLineToPoint: { + CGPoint next = element.point; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + break; + } + + case kCGPathElementAddQuadCurveToPoint: + case kCGPathElementAddCurveToPoint: + { + // handle both curve types gracefully + CGPoint curveTo; + CGPoint ctrl1; + CGPoint ctrl2; + if (element.elementType == kCGPathElementAddQuadCurveToPoint) { + curveTo = element.point; + ctrl1 = element.controlPoint1; + ctrl2 = ctrl1; + } else if (element.elementType == kCGPathElementAddCurveToPoint) { + curveTo = element.point; + ctrl1 = element.controlPoint1; + ctrl2 = element.controlPoint2; + } else { + break; + } + + // ok, this is the bezier for our current element + CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; + + // define our recursive function that will + // help us split the curve up as needed + void (^__block flattenCurve)(CGPoint bez[4]) = ^(CGPoint bez[4]){ + // calculate the error rate of the curve vs + // a line segement between the start and end points + CGPoint onCurve = bezierPointAtT(bez, .5); + CGPoint next = bez[3]; + CGFloat error = distanceOfPointToLine(onCurve, last, next); + + // if the error is less than our accepted level of error, + // then add a line, + // otherwise, split the curve in half and recur + if (error <= idealFlatness) { + addLine(&last, &next, lines, &length, lengths); + lineCount++; + } else { + CGPoint bez1[4], bez2[4]; + subdivideBezierAtT(bez, bez1, bez2, .5); + flattenCurve(bez1); + flattenCurve(bez2); + } + }; + + flattenCurve(bezier); + last = curveTo; + break; + } + + case kCGPathElementCloseSubpath: { + CGPoint next = origin; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + isClosed = YES; + break; + } + + default: + break; + } + } + *lineCountP = lineCount; + *isClosedP = isClosed; + *lengthP = length; + }; +} +@end + diff --git a/package.json b/package.json index 9e67cde6..bf10452e 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,7 @@ "gradient" ], "scripts": { - "lint": "eslint ./", - "postinstall": "node scripts/install.js" + "lint": "eslint ./" }, "peerDependencies": { "react-native": ">=0.50.0", diff --git a/scripts/install.js b/scripts/install.js deleted file mode 100644 index 379fa89a..00000000 --- a/scripts/install.js +++ /dev/null @@ -1,20 +0,0 @@ -var path = require('path'); -var ghdownload = require('github-download'); - -function downloadSubModuleFromGithub(user, repo, callback) { - var dist = path.join(process.cwd(), 'ios/' + repo); - - console.log('\r\n Start downloading ' + repo + ' to `' + dist + '`'); - ghdownload({user: user, repo: repo, ref: 'master'}, dist) - .on('end', function() { - console.log('Download ' + repo + ' library success!'); - callback && callback(); - }) - .on('error', function (err) { - console.error('Download ' + repo + ' library from github failed with err:', err); - }); -} - -downloadSubModuleFromGithub('adamwulf', 'PerformanceBezier', function () { - downloadSubModuleFromGithub('magicismight', 'QuartzBookPack'); -}); From 77dbd3c306edf7e03e329fbc8b46cd3dda3dbdbd Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 28 Dec 2017 14:57:49 +0200 Subject: [PATCH 02/14] Fix retain cycle warning --- ios/Utils/UIBezierPath+TextRendering.m | 45 +++++++++++++------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/ios/Utils/UIBezierPath+TextRendering.m b/ios/Utils/UIBezierPath+TextRendering.m index d768d6cb..5183f20e 100644 --- a/ios/Utils/UIBezierPath+TextRendering.m +++ b/ios/Utils/UIBezierPath+TextRendering.m @@ -17,7 +17,7 @@ CGFloat distance(CGPoint p1, CGPoint p2) { CGFloat dx = p2.x - p1.x; CGFloat dy = p2.y - p1.y; - + return sqrt(dx*dx + dy*dy); } @@ -59,25 +59,25 @@ CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) { CGPoint q; CGFloat mt = 1 - t; - + CGPoint bez1[4]; CGPoint bez2[4]; - + q.x = mt * bez[1].x + t * bez[2].x; q.y = mt * bez[1].y + t * bez[2].y; bez1[1].x = mt * bez[0].x + t * bez[1].x; bez1[1].y = mt * bez[0].y + t * bez[1].y; bez2[2].x = mt * bez[2].x + t * bez[3].x; bez2[2].y = mt * bez[2].y + t * bez[3].y; - + bez1[2].x = mt * bez1[1].x + t * q.x; bez1[2].y = mt * bez1[1].y + t * q.y; bez2[1].x = mt * q.x + t * bez2[2].x; bez2[1].y = mt * q.y + t * bez2[2].y; - + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; - + return CGPointMake(bez1[3].x, bez1[3].y); } @@ -86,24 +86,24 @@ void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], { CGPoint q; CGFloat mt = 1 - t; - + bez1[0].x = bez[0].x; bez1[0].y = bez[0].y; bez2[3].x = bez[3].x; bez2[3].y = bez[3].y; - + q.x = mt * bez[1].x + t * bez[2].x; q.y = mt * bez[1].y + t * bez[2].y; bez1[1].x = mt * bez[0].x + t * bez[1].x; bez1[1].y = mt * bez[0].y + t * bez[1].y; bez2[2].x = mt * bez[2].x + t * bez[3].x; bez2[2].y = mt * bez[2].y + t * bez[3].y; - + bez1[2].x = mt * bez1[1].x + t * q.x; bez1[2].y = mt * bez1[1].y + t * q.y; bez2[1].x = mt * q.x + t * bez2[2].x; bez2[1].y = mt * q.y + t * bez2[2].y; - + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; } @@ -148,14 +148,14 @@ void GetBezierElements(void *info, const CGPathElement *element) case kCGPathElementMoveToPoint: origin = last = element.point; break; - + case kCGPathElementAddLineToPoint: { CGPoint next = element.point; addLine(&last, &next, lines, &length, lengths); lineCount++; break; } - + case kCGPathElementAddQuadCurveToPoint: case kCGPathElementAddCurveToPoint: { @@ -174,19 +174,20 @@ void GetBezierElements(void *info, const CGPathElement *element) } else { break; } - + // ok, this is the bezier for our current element CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; - + // define our recursive function that will // help us split the curve up as needed - void (^__block flattenCurve)(CGPoint bez[4]) = ^(CGPoint bez[4]){ + __weak void (^ __block weakFlattenCurve)(CGPoint bez[4]); + void (^ __block flattenCurve)(CGPoint bez[4]) = ^(CGPoint bez[4]){ // calculate the error rate of the curve vs // a line segement between the start and end points CGPoint onCurve = bezierPointAtT(bez, .5); CGPoint next = bez[3]; CGFloat error = distanceOfPointToLine(onCurve, last, next); - + // if the error is less than our accepted level of error, // then add a line, // otherwise, split the curve in half and recur @@ -196,16 +197,16 @@ void GetBezierElements(void *info, const CGPathElement *element) } else { CGPoint bez1[4], bez2[4]; subdivideBezierAtT(bez, bez1, bez2, .5); - flattenCurve(bez1); - flattenCurve(bez2); + weakFlattenCurve(bez1); + weakFlattenCurve(bez2); } }; - - flattenCurve(bezier); + weakFlattenCurve = flattenCurve; + weakFlattenCurve(bezier); last = curveTo; break; } - + case kCGPathElementCloseSubpath: { CGPoint next = origin; addLine(&last, &next, lines, &length, lengths); @@ -213,7 +214,7 @@ void GetBezierElements(void *info, const CGPathElement *element) isClosed = YES; break; } - + default: break; } From f49eabf8e8c82f656c4fb1d6d933acc8a731864d Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Thu, 28 Dec 2017 16:55:06 +0200 Subject: [PATCH 03/14] Rewrite recursive block using stack and while loop. --- ios/Utils/UIBezierPath+TextRendering.m | 40 ++++++++++++++------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/ios/Utils/UIBezierPath+TextRendering.m b/ios/Utils/UIBezierPath+TextRendering.m index 5183f20e..e64f8a0c 100644 --- a/ios/Utils/UIBezierPath+TextRendering.m +++ b/ios/Utils/UIBezierPath+TextRendering.m @@ -135,12 +135,12 @@ void GetBezierElements(void *info, const CGPathElement *element) } - (void (^)(CGFloat *, NSInteger *, NSMutableArray *, NSMutableArray *, BOOL *)) getTextProperties{ - return ^(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengths, NSMutableArray * lines, BOOL *isClosedP) { - __block CGPoint origin = CGPointMake (0.0, 0.0); - __block CGPoint last = CGPointMake (0.0, 0.0); - __block NSInteger lineCount = 0; - __block CGFloat length = 0; - __block BOOL isClosed = NO; + return ^(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengths, NSMutableArray * lines, BOOL *isClosedP) { + CGPoint origin = CGPointMake (0.0, 0.0); + CGPoint last = CGPointMake (0.0, 0.0); + NSInteger lineCount = 0; + CGFloat length = 0; + BOOL isClosed = NO; NSArray * elements = self.elements; for (BezierElement *element in elements) { switch (element.elementType) @@ -175,34 +175,36 @@ void GetBezierElements(void *info, const CGPathElement *element) break; } - // ok, this is the bezier for our current element + // this is the bezier for our current element CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; + NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; + NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; + + NSInteger count = 1; + while (count-- > 0) { + CGPoint bez[4]; + [curves[count] getValue:&bez]; + [curves removeLastObject]; - // define our recursive function that will - // help us split the curve up as needed - __weak void (^ __block weakFlattenCurve)(CGPoint bez[4]); - void (^ __block flattenCurve)(CGPoint bez[4]) = ^(CGPoint bez[4]){ // calculate the error rate of the curve vs // a line segement between the start and end points CGPoint onCurve = bezierPointAtT(bez, .5); CGPoint next = bez[3]; CGFloat error = distanceOfPointToLine(onCurve, last, next); - // if the error is less than our accepted level of error, - // then add a line, - // otherwise, split the curve in half and recur + // if the error is less than our accepted level of error + // then add a line, else, split the curve in half if (error <= idealFlatness) { addLine(&last, &next, lines, &length, lengths); lineCount++; } else { CGPoint bez1[4], bez2[4]; subdivideBezierAtT(bez, bez1, bez2, .5); - weakFlattenCurve(bez1); - weakFlattenCurve(bez2); + [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; + [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; + count += 2; } - }; - weakFlattenCurve = flattenCurve; - weakFlattenCurve(bezier); + } last = curveTo; break; } From 7088d52b71c298707bb4348cfb6d28a1adb4e3a9 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 29 Dec 2017 21:50:30 +0200 Subject: [PATCH 04/14] Refactor and simplify, cache path properties. Remove use of UIBezierPath and categories. --- ios/Elements/RNSVGPath.h | 2 +- ios/Elements/RNSVGPath.m | 256 ++++++++++++++++++++++++- ios/RNSVG.xcodeproj/project.pbxproj | 22 +-- ios/Text/RNSVGTSpan.h | 2 - ios/Text/RNSVGTSpan.m | 32 ++-- ios/Utils/BezierElement.h | 11 +- ios/Utils/BezierElement.m | 43 +---- ios/Utils/RNSVGPathParser.m | 53 +++-- ios/Utils/UIBezierPath+TextRendering.h | 12 -- ios/Utils/UIBezierPath+TextRendering.m | 230 ---------------------- 10 files changed, 311 insertions(+), 352 deletions(-) delete mode 100644 ios/Utils/UIBezierPath+TextRendering.h delete mode 100644 ios/Utils/UIBezierPath+TextRendering.m diff --git a/ios/Elements/RNSVGPath.h b/ios/Elements/RNSVGPath.h index 92461cd1..ea8e2431 100644 --- a/ios/Elements/RNSVGPath.h +++ b/ios/Elements/RNSVGPath.h @@ -14,6 +14,6 @@ @property (nonatomic, strong) RNSVGPathParser *d; -- (NSArray *)getBezierCurves; +- (void)getPathLength:(CGFloat*)length lineCount:(NSInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; @end diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index c2ee7815..95eeb199 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -7,10 +7,256 @@ */ #import "RNSVGPath.h" +#import "BezierElement.h" + +/* Bezier logic from PerformanceBezier */ +/* + + ## License + + Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License. + + For attribution, please include: + + 1. Mention original author "Adam Wulf for Loose Leaf app" + 2. Link to https://getlooseleaf.com/opensource/ + 3. Link to https://github.com/adamwulf/PerformanceBezier + + */ +static CGFloat idealFlatness = .01; + +/** + * returns the distance between two points + */ +CGFloat distance(CGPoint p1, CGPoint p2) +{ + CGFloat dx = p2.x - p1.x; + CGFloat dy = p2.y - p1.y; + + return sqrt(dx*dx + dy*dy); +} + +/** + * returns the dot product of two coordinates + */ +CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { + return p1.x * p2.x + p1.y * p2.y; +} + +/** + * returns the shortest distance from a point to a line + */ +CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ + CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); + CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); + CGFloat c1 = dotProduct(w, v); + CGFloat c2 = dotProduct(v, v); + CGFloat d; + if (c1 <= 0) { + d = distance(point, start); + } + else if (c2 <= c1) { + d = distance(point, end); + } + else { + CGFloat b = c1 / c2; + CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); + d = distance(point, Pb); + } + return d; +} + +/** + * calculate the point on a bezier at time t + * where 0 < t < 1 + */ +CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + CGPoint bez1[4]; + CGPoint bez2[4]; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; + + return CGPointMake(bez1[3].x, bez1[3].y); +} + +// Subdivide a Bézier (specific division) +/* + * (c) 2004 Alastair J. Houghton + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. The name of the author of this software may not be used to endorse + * or promote products derived from the software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY DIRECT, INDIRECT, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + bez1[0].x = bez[0].x; + bez1[0].y = bez[0].y; + bez2[3].x = bez[3].x; + bez2[3].y = bez[3].y; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; +} + +void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { + NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; + [lines addObject:line]; + *length += distance(*last, *next); + [lengths addObject:[NSNumber numberWithDouble:*length]]; + *last = *next; +} @implementation RNSVGPath { CGPathRef _path; + NSMutableArray *lengths; + NSMutableArray *lines; + NSInteger lineCount; + CGFloat length; + BOOL isClosed; + BOOL cached; +} + +- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP +{ + if (!cached) { + CGPoint origin = CGPointMake (0.0, 0.0); + CGPoint last = CGPointMake (0.0, 0.0); + lengths = [NSMutableArray array]; + lines = [NSMutableArray array]; + isClosed = NO; + lineCount = 0; + length = 0; + + NSArray *elements = [_d getBezierCurves]; + for (BezierElement *element in elements) { + switch (element.elementType) + { + case kCGPathElementMoveToPoint: + origin = last = element.point; + break; + + case kCGPathElementAddLineToPoint: { + CGPoint next = element.point; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + break; + } + case kCGPathElementAddQuadCurveToPoint: + case kCGPathElementAddCurveToPoint: + { + // handle both curve types gracefully + CGPoint curveTo = element.point; + CGPoint ctrl1 = element.controlPoint1; + CGPoint ctrl2 = element.elementType == kCGPathElementAddQuadCurveToPoint ? ctrl1 : element.controlPoint2; + + // this is the bezier for our current element + CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; + NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; + NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; + + for (NSInteger curveIndex = 0; curveIndex >= 0; curveIndex--) { + CGPoint bez[4]; + [curves[curveIndex] getValue:&bez]; + [curves removeLastObject]; + + // calculate the error rate of the curve vs + // a line segement between the start and end points + CGPoint onCurve = bezierPointAtT(bez, .5); + CGPoint next = bez[3]; + CGFloat error = distanceOfPointToLine(onCurve, last, next); + + // if the error is less than our accepted level of error + // then add a line, else, split the curve in half + if (error <= idealFlatness) { + addLine(&last, &next, lines, &length, lengths); + lineCount++; + } else { + CGPoint bez1[4], bez2[4]; + subdivideBezierAtT(bez, bez1, bez2, .5); + [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; + [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; + curveIndex += 2; + } + } + last = curveTo; + break; + } + + case kCGPathElementCloseSubpath: { + CGPoint next = origin; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + isClosed = YES; + break; + } + + default: + break; + } + } + cached = YES; + } + *lineCountP = lineCount; + *isClosedP = isClosed; + *lengthsP = lengths; + *lengthP = length; + *linesP = lines; } - (void)setD:(RNSVGPathParser *)d @@ -21,6 +267,9 @@ [self invalidate]; _d = d; + lines = nil; + lengths = nil; + cached = false; CGPathRelease(_path); _path = CGPathRetain([d getPath]); } @@ -30,13 +279,10 @@ return _path; } -- (NSArray *)getBezierCurves -{ - return [_d getBezierCurves]; -} - - (void)dealloc { + lines = nil; + lengths = nil; CGPathRelease(_path); } diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index a5dca387..88570bcd 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -87,10 +87,7 @@ 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */; }; 9494C5481F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */ = {isa = PBXBuildFile; fileRef = 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */; }; - 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; - 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */; }; - 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; - 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EB936B1FF4916F00C0B251 /* BezierElement.m */; }; + 94C70B1A1FF6B1C0004DFD49 /* BezierElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C70B171FF6B1C0004DFD49 /* BezierElement.m */; }; A361E76E1EB0C33D00646005 /* RNSVGTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D331CE74E3100887C2B /* RNSVGTextManager.m */; }; A361E76F1EB0C33D00646005 /* RNSVGImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1039D2841CE71EB7001E90A8 /* RNSVGImage.m */; }; A361E7701EB0C33D00646005 /* RNSVGRect.m in Sources */ = {isa = PBXBuildFile; fileRef = 10BA0D471CE74E3D00887C2B /* RNSVGRect.m */; }; @@ -295,11 +292,9 @@ 9494C5351F4C44DD00D5BCFD /* TextPathMidLine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathMidLine.m; path = Text/TextPathMidLine.m; sourceTree = ""; }; 9494C5361F4C44DD00D5BCFD /* TextPathSide.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSide.m; path = Text/TextPathSide.m; sourceTree = ""; }; 9494C5371F4C44DD00D5BCFD /* TextPathSpacing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TextPathSpacing.m; path = Text/TextPathSpacing.m; sourceTree = ""; }; + 94C70B151FF6B1BF004DFD49 /* BezierElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = BezierElement.h; path = Utils/BezierElement.h; sourceTree = ""; }; + 94C70B171FF6B1C0004DFD49 /* BezierElement.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = BezierElement.m; path = Utils/BezierElement.m; sourceTree = ""; }; 94DDAC5C1F3D024300EED511 /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRNSVG-tvOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "UIBezierPath+TextRendering.h"; path = "Utils/UIBezierPath+TextRendering.h"; sourceTree = ""; }; - 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "UIBezierPath+TextRendering.m"; path = "Utils/UIBezierPath+TextRendering.m"; sourceTree = ""; }; - 94EB936B1FF4916F00C0B251 /* BezierElement.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = BezierElement.m; path = Utils/BezierElement.m; sourceTree = ""; }; - 94EB93701FF4918D00C0B251 /* BezierElement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = BezierElement.h; path = Utils/BezierElement.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -501,6 +496,8 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 94C70B151FF6B1BF004DFD49 /* BezierElement.h */, + 94C70B171FF6B1C0004DFD49 /* BezierElement.m */, 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */, 10ABC7381D43982B006CCF6E /* RNSVGVBMOS.h */, 10ABC7371D439779006CCF6E /* RNSVGCGFCRule.h */, @@ -514,10 +511,6 @@ 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */, 1039D29B1CE72177001E90A8 /* RCTConvert+RNSVG.h */, 1039D29C1CE72177001E90A8 /* RCTConvert+RNSVG.m */, - 94EB93151FF4196100C0B251 /* UIBezierPath+TextRendering.h */, - 94EB93161FF4196100C0B251 /* UIBezierPath+TextRendering.m */, - 94EB936B1FF4916F00C0B251 /* BezierElement.m */, - 94EB93701FF4918D00C0B251 /* BezierElement.h */, ); name = Utils; sourceTree = ""; @@ -614,7 +607,6 @@ 10BA0D341CE74E3100887C2B /* RNSVGCircleManager.m in Sources */, 10BEC1BC1D3F66F500FDCB19 /* RNSVGLinearGradient.m in Sources */, 9494C5461F4C44DD00D5BCFD /* TextPathSide.m in Sources */, - 94EB93171FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 1039D2B01CE72F27001E90A8 /* RNSVGPercentageConverter.m in Sources */, 9494C53C1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 10BA0D491CE74E3D00887C2B /* RNSVGEllipse.m in Sources */, @@ -649,7 +641,7 @@ 9494C5251F4B605F00D5BCFD /* GlyphContext.m in Sources */, 10BA0D481CE74E3D00887C2B /* RNSVGCircle.m in Sources */, 9494C5401F4C44DD00D5BCFD /* TextLengthAdjust.m in Sources */, - 94EB936C1FF4916F00C0B251 /* BezierElement.m in Sources */, + 94C70B1A1FF6B1C0004DFD49 /* BezierElement.m in Sources */, 10BA0D351CE74E3100887C2B /* RNSVGEllipseManager.m in Sources */, 1039D2A01CE72177001E90A8 /* RCTConvert+RNSVG.m in Sources */, 9494C4FF1F4B5BE800D5BCFD /* FontData.m in Sources */, @@ -683,7 +675,6 @@ A361E7711EB0C33D00646005 /* RNSVGCircleManager.m in Sources */, A361E7721EB0C33D00646005 /* RNSVGLinearGradient.m in Sources */, A361E7731EB0C33D00646005 /* RNSVGPercentageConverter.m in Sources */, - 94EB93181FF4196100C0B251 /* UIBezierPath+TextRendering.m in Sources */, 9494C53F1F4C44DD00D5BCFD /* TextDecoration.m in Sources */, A361E7751EB0C33D00646005 /* RNSVGEllipse.m in Sources */, A361E7761EB0C33D00646005 /* RNSVGPath.m in Sources */, @@ -718,7 +709,6 @@ 9494C53B1F4C44DD00D5BCFD /* FontVariantLigatures.m in Sources */, 9494C5001F4B5BE800D5BCFD /* FontData.m in Sources */, 9494C5491F4C44DD00D5BCFD /* TextPathSpacing.m in Sources */, - 94EB936D1FF4916F00C0B251 /* BezierElement.m in Sources */, A361E78D1EB0C33D00646005 /* RNSVGLineManager.m in Sources */, 9494C53D1F4C44DD00D5BCFD /* TextAnchor.m in Sources */, 9494C5471F4C44DD00D5BCFD /* TextPathSide.m in Sources */, diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h index 0b1c7c9e..9f909070 100644 --- a/ios/Text/RNSVGTSpan.h +++ b/ios/Text/RNSVGTSpan.h @@ -15,8 +15,6 @@ #import "TextPathSpacing.h" #import "TextLengthAdjust.h" #import "AlignmentBaseline.h" -#import "UIBezierPath+TextRendering.h" - @interface RNSVGTSpan : RNSVGText diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index ed52be91..1d549ccc 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -18,8 +18,8 @@ NSCharacterSet *separators = nil; CGPathRef _cache; CGFloat _pathLength; RNSVGTextPath *textPath; - NSMutableArray *lengths; - NSMutableArray *lines; + NSArray *lengths; + NSArray *lines; NSInteger lineCount; BOOL isClosed; } @@ -788,7 +788,7 @@ NSCharacterSet *separators = nil; int i = 0; CGFloat totalLength = 0; CGFloat prevLength = 0; - + // TODO investigate at what lineCount a binary search is faster while (i < lineCount - 1) { prevLength = totalLength; @@ -799,25 +799,24 @@ NSCharacterSet *separators = nil; break; } }; - + CGFloat length = totalLength - prevLength; CGFloat targetPercent = (midPoint - prevLength) / length; - + NSArray * points = [lines objectAtIndex: i]; CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; - + CGPoint slope; CGPoint mid = InterpolateLineSegment(p1, p2, targetPercent, &slope); - + // Calculate the rotation double angle = atan2(slope.y, slope.x); transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(mid.x, mid.y), transform); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(angle + r), transform); transform = CGAffineTransformScale(transform, scaledDirection, side); - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(-halfWay, dy + baselineShift), transform); - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(0, y), transform); + transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(-halfWay, y + dy + baselineShift), transform); } else { transform = CGAffineTransformMakeTranslation(startPoint, y + dy + baselineShift); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(r), transform); @@ -839,13 +838,13 @@ CGPoint InterpolateLineSegment(CGPoint p1, CGPoint p2, CGFloat percent, CGPoint { CGFloat dx = p2.x - p1.x; CGFloat dy = p2.y - p1.y; - + if (slope) *slope = CGPointMake(dx, dy); - + CGFloat px = p1.x + dx * percent; CGFloat py = p1.y + dy * percent; - + return CGPointMake(px, py); } @@ -865,16 +864,13 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) - (void)setupTextPath:(CGContextRef)context { + lines = nil; + lengths = nil; textPath = nil; [self traverseTextSuperviews:^(__kindof RNSVGText *node) { if ([node class] == [RNSVGTextPath class]) { textPath = (RNSVGTextPath*) node; - RNSVGPath *svgPath = [textPath getPath]; - UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:[svgPath getPath:nil]]; - - lines = [NSMutableArray array]; - lengths = [NSMutableArray array]; - [bezierPath getTextProperties](&_pathLength, &lineCount, lengths, lines, &isClosed); + [[textPath getPath] getPathLength:&_pathLength lineCount:&lineCount lengths:&lengths lines:&lines isClosed:&isClosed]; return NO; } return YES; diff --git a/ios/Utils/BezierElement.h b/ios/Utils/BezierElement.h index 17af3394..a78c8721 100644 --- a/ios/Utils/BezierElement.h +++ b/ios/Utils/BezierElement.h @@ -1,15 +1,13 @@ /* - - Erica Sadun, http://ericasadun.com - - */ + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ #import #import #define NULLPOINT CGRectNull.origin -#define POINT_IS_NULL(_POINT_) CGPointEqualToPoint(_POINT_, NULLPOINT) @interface BezierElement : NSObject @@ -19,8 +17,5 @@ @property (nonatomic, assign) CGPoint controlPoint1; @property (nonatomic, assign) CGPoint controlPoint2; -// Instance creation -+ (instancetype) elementWithPathElement: (CGPathElement) element; - @end; diff --git a/ios/Utils/BezierElement.m b/ios/Utils/BezierElement.m index ab53465c..73a8eb50 100644 --- a/ios/Utils/BezierElement.m +++ b/ios/Utils/BezierElement.m @@ -1,9 +1,8 @@ /* - - Erica Sadun, http://ericasadun.com - - */ + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ #import "BezierElement.h" @@ -22,41 +21,5 @@ } return self; } - -+ (instancetype) elementWithPathElement: (CGPathElement) element -{ - BezierElement *newElement = [[self alloc] init]; - newElement.elementType = element.type; - - switch (newElement.elementType) - { - case kCGPathElementCloseSubpath: - break; - case kCGPathElementMoveToPoint: - case kCGPathElementAddLineToPoint: - { - newElement.point = element.points[0]; - break; - } - case kCGPathElementAddQuadCurveToPoint: - { - newElement.point = element.points[1]; - newElement.controlPoint1 = element.points[0]; - break; - } - case kCGPathElementAddCurveToPoint: - { - newElement.point = element.points[2]; - newElement.controlPoint1 = element.points[0]; - newElement.controlPoint2 = element.points[1]; - break; - } - default: - break; - } - - return newElement; -} - @end diff --git a/ios/Utils/RNSVGPathParser.m b/ios/Utils/RNSVGPathParser.m index 719d8116..76d78c71 100644 --- a/ios/Utils/RNSVGPathParser.m +++ b/ios/Utils/RNSVGPathParser.m @@ -9,14 +9,14 @@ #import "RNSVGPathParser.h" #import #import "math.h" +#import "BezierElement.h" @implementation RNSVGPathParser { NSString* _d; NSString* _originD; NSRegularExpression* _pathRegularExpression; - NSMutableArray* _bezierCurves; - NSValue *_lastStartPoint; + NSMutableArray* _bezierCurves; float _penX; float _penY; float _penDownX; @@ -159,8 +159,10 @@ _pivotY = _penY = y; CGPathMoveToPoint(path, nil, x, y); - _lastStartPoint = [NSValue valueWithCGPoint: CGPointMake(x, y)]; - [_bezierCurves addObject: @[_lastStartPoint]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementMoveToPoint; + newElement.point = CGPointMake(x, y); + [_bezierCurves addObject:newElement]; } - (void)line:(CGMutablePathRef)path x:(float)x y:(float)y @@ -174,8 +176,10 @@ _pivotY = _penY = y; CGPathAddLineToPoint(path, nil, x, y); - NSValue * destination = [NSValue valueWithCGPoint:CGPointMake(x, y)]; - [_bezierCurves addObject: @[destination, destination, destination]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddLineToPoint; + newElement.point = CGPointMake(x, y); + [_bezierCurves addObject:newElement]; } - (void)curve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey @@ -202,11 +206,12 @@ _penY = ey; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); - [_bezierCurves addObject: @[ - [NSValue valueWithCGPoint:CGPointMake(c1x, c1y)], - [NSValue valueWithCGPoint:CGPointMake(c2x, c2y)], - [NSValue valueWithCGPoint:CGPointMake(ex, ey)] - ]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddCurveToPoint; + newElement.controlPoint1 = CGPointMake(c1x, c1y); + newElement.controlPoint2 = CGPointMake(c2x, c2y); + newElement.point = CGPointMake(ex, ey); + [_bezierCurves addObject:newElement]; } - (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey @@ -367,14 +372,20 @@ float cp2x = x + k * y; float cp2y = y - k * x; - CGPathAddCurveToPoint(path, - nil, - cx + xx * cp1x + yx * cp1y, - cy + xy * cp1x + yy * cp1y, - cx + xx * cp2x + yx * cp2y, - cy + xy * cp2x + yy * cp2y, - cx + xx * x + yx * y, - cy + xy * x + yy * y); + float c1x = cx + xx * cp1x + yx * cp1y; + float c1y = cy + xy * cp1x + yy * cp1y; + float c2x = cx + xx * cp2x + yx * cp2y; + float c2y = cy + xy * cp2x + yy * cp2y; + float ex = cx + xx * x + yx * y; + float ey = cy + xy * x + yy * y; + CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); + + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementAddCurveToPoint; + newElement.controlPoint1 = CGPointMake(c1x, c1y); + newElement.controlPoint2 = CGPointMake(c2x, c2y); + newElement.point = CGPointMake(ex, ey); + [_bezierCurves addObject:newElement]; } } @@ -385,7 +396,9 @@ _penY = _penDownY; _penDownSet = NO; CGPathCloseSubpath(path); - [_bezierCurves addObject: @[_lastStartPoint, _lastStartPoint, _lastStartPoint]]; + BezierElement *newElement = [[BezierElement alloc] init]; + newElement.elementType = kCGPathElementCloseSubpath; + [_bezierCurves addObject:newElement]; } } diff --git a/ios/Utils/UIBezierPath+TextRendering.h b/ios/Utils/UIBezierPath+TextRendering.h deleted file mode 100644 index 88eadab2..00000000 --- a/ios/Utils/UIBezierPath+TextRendering.h +++ /dev/null @@ -1,12 +0,0 @@ -// -// UIBezierPath+TextRendering.h -// RNSVG -// -// Created by Mikael Sand on 27/12/2017. -// - -#import - -@interface UIBezierPath (TextRendering) -- (void (^)(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengthsP, NSMutableArray * linesP, BOOL *isClosedP)) getTextProperties; -@end diff --git a/ios/Utils/UIBezierPath+TextRendering.m b/ios/Utils/UIBezierPath+TextRendering.m deleted file mode 100644 index e64f8a0c..00000000 --- a/ios/Utils/UIBezierPath+TextRendering.m +++ /dev/null @@ -1,230 +0,0 @@ -// -// UIBezierPath+TextRendering.m -// RNSVG -// -// Created by Mikael Sand on 27/12/2017. -// - -#import "UIBezierPath+TextRendering.h" -#import "BezierElement.h" - -static CGFloat idealFlatness = .01; - -/** - * returns the distance between two points - */ -CGFloat distance(CGPoint p1, CGPoint p2) -{ - CGFloat dx = p2.x - p1.x; - CGFloat dy = p2.y - p1.y; - - return sqrt(dx*dx + dy*dy); -} - -/** - * returns the dot product of two coordinates - */ -CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { - return p1.x * p2.x + p1.y * p2.y; -} - -/** - * returns the shortest distance from a point to a line - */ -CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ - CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); - CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); - CGFloat c1 = dotProduct(w, v); - CGFloat c2 = dotProduct(v, v); - CGFloat d; - if (c1 <= 0) { - d = distance(point, start); - } - else if (c2 <= c1) { - d = distance(point, end); - } - else { - CGFloat b = c1 / c2; - CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); - d = distance(point, Pb); - } - return d; -} - -/** - * calculate the point on a bezier at time t - * where 0 < t < 1 - */ -CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - CGPoint bez1[4]; - CGPoint bez2[4]; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; - - return CGPointMake(bez1[3].x, bez1[3].y); -} - -// Subdivide a Bézier (specific division) -void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - bez1[0].x = bez[0].x; - bez1[0].y = bez[0].y; - bez2[3].x = bez[3].x; - bez2[3].y = bez[3].y; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; -} - -void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { - NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; - [lines addObject:line]; - *length += distance(*last, *next); - [lengths addObject:[NSNumber numberWithDouble:*length]]; - *last = *next; -} - -// Convert one element to BezierElement and save to array -void GetBezierElements(void *info, const CGPathElement *element) -{ - NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; - if (element) - [bezierElements addObject:[BezierElement elementWithPathElement:*element]]; -} - -@implementation UIBezierPath (TextRendering) - -// Retrieve array of component elements -- (NSArray *) elements -{ - NSMutableArray *elements = [NSMutableArray array]; - CGPathApply(self.CGPath, (__bridge void *)elements, GetBezierElements); - return elements; -} - -- (void (^)(CGFloat *, NSInteger *, NSMutableArray *, NSMutableArray *, BOOL *)) getTextProperties{ - return ^(CGFloat *lengthP, NSInteger *lineCountP, NSMutableArray * lengths, NSMutableArray * lines, BOOL *isClosedP) { - CGPoint origin = CGPointMake (0.0, 0.0); - CGPoint last = CGPointMake (0.0, 0.0); - NSInteger lineCount = 0; - CGFloat length = 0; - BOOL isClosed = NO; - NSArray * elements = self.elements; - for (BezierElement *element in elements) { - switch (element.elementType) - { - case kCGPathElementMoveToPoint: - origin = last = element.point; - break; - - case kCGPathElementAddLineToPoint: { - CGPoint next = element.point; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - break; - } - - case kCGPathElementAddQuadCurveToPoint: - case kCGPathElementAddCurveToPoint: - { - // handle both curve types gracefully - CGPoint curveTo; - CGPoint ctrl1; - CGPoint ctrl2; - if (element.elementType == kCGPathElementAddQuadCurveToPoint) { - curveTo = element.point; - ctrl1 = element.controlPoint1; - ctrl2 = ctrl1; - } else if (element.elementType == kCGPathElementAddCurveToPoint) { - curveTo = element.point; - ctrl1 = element.controlPoint1; - ctrl2 = element.controlPoint2; - } else { - break; - } - - // this is the bezier for our current element - CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; - NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; - NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; - - NSInteger count = 1; - while (count-- > 0) { - CGPoint bez[4]; - [curves[count] getValue:&bez]; - [curves removeLastObject]; - - // calculate the error rate of the curve vs - // a line segement between the start and end points - CGPoint onCurve = bezierPointAtT(bez, .5); - CGPoint next = bez[3]; - CGFloat error = distanceOfPointToLine(onCurve, last, next); - - // if the error is less than our accepted level of error - // then add a line, else, split the curve in half - if (error <= idealFlatness) { - addLine(&last, &next, lines, &length, lengths); - lineCount++; - } else { - CGPoint bez1[4], bez2[4]; - subdivideBezierAtT(bez, bez1, bez2, .5); - [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; - [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; - count += 2; - } - } - last = curveTo; - break; - } - - case kCGPathElementCloseSubpath: { - CGPoint next = origin; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - isClosed = YES; - break; - } - - default: - break; - } - } - *lineCountP = lineCount; - *isClosedP = isClosed; - *lengthP = length; - }; -} -@end - From f07e8f041caf258cb5bfb6abf27e316d312b2554 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sat, 30 Dec 2017 03:35:58 +0200 Subject: [PATCH 05/14] Allow TextPath to reference any RNSVGNode instead of only RNSVGPath. Remove unused RNSVGTextAnchor --- ios/Elements/RNSVGPath.h | 2 - ios/Elements/RNSVGPath.m | 251 ------------------------- ios/RNSVG.xcodeproj/project.pbxproj | 2 - ios/Text/RNSVGTSpan.m | 2 +- ios/Text/RNSVGText.h | 1 - ios/Text/RNSVGTextPath.h | 3 +- ios/Text/RNSVGTextPath.m | 267 +++++++++++++++++++++++++-- ios/Utils/BezierElement.h | 4 + ios/Utils/BezierElement.m | 52 ++++++ ios/Utils/RCTConvert+RNSVG.h | 2 - ios/Utils/RCTConvert+RNSVG.m | 7 - ios/Utils/RNSVGPathParser.h | 1 - ios/Utils/RNSVGPathParser.m | 38 ---- ios/Utils/RNSVGTextAnchor.h | 14 -- ios/ViewManagers/RNSVGTSpanManager.m | 1 - 15 files changed, 312 insertions(+), 335 deletions(-) delete mode 100644 ios/Utils/RNSVGTextAnchor.h diff --git a/ios/Elements/RNSVGPath.h b/ios/Elements/RNSVGPath.h index ea8e2431..2caf060e 100644 --- a/ios/Elements/RNSVGPath.h +++ b/ios/Elements/RNSVGPath.h @@ -14,6 +14,4 @@ @property (nonatomic, strong) RNSVGPathParser *d; -- (void)getPathLength:(CGFloat*)length lineCount:(NSInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; - @end diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index 95eeb199..fc77c23f 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -7,256 +7,10 @@ */ #import "RNSVGPath.h" -#import "BezierElement.h" - -/* Bezier logic from PerformanceBezier */ -/* - - ## License - - Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License. - - For attribution, please include: - - 1. Mention original author "Adam Wulf for Loose Leaf app" - 2. Link to https://getlooseleaf.com/opensource/ - 3. Link to https://github.com/adamwulf/PerformanceBezier - - */ -static CGFloat idealFlatness = .01; - -/** - * returns the distance between two points - */ -CGFloat distance(CGPoint p1, CGPoint p2) -{ - CGFloat dx = p2.x - p1.x; - CGFloat dy = p2.y - p1.y; - - return sqrt(dx*dx + dy*dy); -} - -/** - * returns the dot product of two coordinates - */ -CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { - return p1.x * p2.x + p1.y * p2.y; -} - -/** - * returns the shortest distance from a point to a line - */ -CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ - CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); - CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); - CGFloat c1 = dotProduct(w, v); - CGFloat c2 = dotProduct(v, v); - CGFloat d; - if (c1 <= 0) { - d = distance(point, start); - } - else if (c2 <= c1) { - d = distance(point, end); - } - else { - CGFloat b = c1 / c2; - CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); - d = distance(point, Pb); - } - return d; -} - -/** - * calculate the point on a bezier at time t - * where 0 < t < 1 - */ -CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - CGPoint bez1[4]; - CGPoint bez2[4]; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; - - return CGPointMake(bez1[3].x, bez1[3].y); -} - -// Subdivide a Bézier (specific division) -/* - * (c) 2004 Alastair J. Houghton - * All Rights Reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * 3. The name of the author of this software may not be used to endorse - * or promote products derived from the software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS - * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY DIRECT, INDIRECT, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO - * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; - * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR - * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF - * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - */ -void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - bez1[0].x = bez[0].x; - bez1[0].y = bez[0].y; - bez2[3].x = bez[3].x; - bez2[3].y = bez[3].y; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; -} - -void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { - NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; - [lines addObject:line]; - *length += distance(*last, *next); - [lengths addObject:[NSNumber numberWithDouble:*length]]; - *last = *next; -} @implementation RNSVGPath { CGPathRef _path; - NSMutableArray *lengths; - NSMutableArray *lines; - NSInteger lineCount; - CGFloat length; - BOOL isClosed; - BOOL cached; -} - -- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP -{ - if (!cached) { - CGPoint origin = CGPointMake (0.0, 0.0); - CGPoint last = CGPointMake (0.0, 0.0); - lengths = [NSMutableArray array]; - lines = [NSMutableArray array]; - isClosed = NO; - lineCount = 0; - length = 0; - - NSArray *elements = [_d getBezierCurves]; - for (BezierElement *element in elements) { - switch (element.elementType) - { - case kCGPathElementMoveToPoint: - origin = last = element.point; - break; - - case kCGPathElementAddLineToPoint: { - CGPoint next = element.point; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - break; - } - case kCGPathElementAddQuadCurveToPoint: - case kCGPathElementAddCurveToPoint: - { - // handle both curve types gracefully - CGPoint curveTo = element.point; - CGPoint ctrl1 = element.controlPoint1; - CGPoint ctrl2 = element.elementType == kCGPathElementAddQuadCurveToPoint ? ctrl1 : element.controlPoint2; - - // this is the bezier for our current element - CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; - NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; - NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; - - for (NSInteger curveIndex = 0; curveIndex >= 0; curveIndex--) { - CGPoint bez[4]; - [curves[curveIndex] getValue:&bez]; - [curves removeLastObject]; - - // calculate the error rate of the curve vs - // a line segement between the start and end points - CGPoint onCurve = bezierPointAtT(bez, .5); - CGPoint next = bez[3]; - CGFloat error = distanceOfPointToLine(onCurve, last, next); - - // if the error is less than our accepted level of error - // then add a line, else, split the curve in half - if (error <= idealFlatness) { - addLine(&last, &next, lines, &length, lengths); - lineCount++; - } else { - CGPoint bez1[4], bez2[4]; - subdivideBezierAtT(bez, bez1, bez2, .5); - [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; - [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; - curveIndex += 2; - } - } - last = curveTo; - break; - } - - case kCGPathElementCloseSubpath: { - CGPoint next = origin; - addLine(&last, &next, lines, &length, lengths); - lineCount++; - isClosed = YES; - break; - } - - default: - break; - } - } - cached = YES; - } - *lineCountP = lineCount; - *isClosedP = isClosed; - *lengthsP = lengths; - *lengthP = length; - *linesP = lines; } - (void)setD:(RNSVGPathParser *)d @@ -267,9 +21,6 @@ void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat [self invalidate]; _d = d; - lines = nil; - lengths = nil; - cached = false; CGPathRelease(_path); _path = CGPathRetain([d getPath]); } @@ -281,8 +32,6 @@ void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat - (void)dealloc { - lines = nil; - lengths = nil; CGPathRelease(_path); } diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index 88570bcd..6bfe28ac 100644 --- a/ios/RNSVG.xcodeproj/project.pbxproj +++ b/ios/RNSVG.xcodeproj/project.pbxproj @@ -248,7 +248,6 @@ 7F08CE9D1E23479700650F83 /* RNSVGTextPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGTextPath.m; path = Text/RNSVGTextPath.m; sourceTree = ""; }; 7F08CE9E1E23479700650F83 /* RNSVGTSpan.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGTSpan.h; path = Text/RNSVGTSpan.h; sourceTree = ""; }; 7F08CE9F1E23479700650F83 /* RNSVGTSpan.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGTSpan.m; path = Text/RNSVGTSpan.m; sourceTree = ""; }; - 7F08CEA31E23481F00650F83 /* RNSVGTextAnchor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGTextAnchor.h; path = Utils/RNSVGTextAnchor.h; sourceTree = ""; }; 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGUnits.h; path = Utils/RNSVGUnits.h; sourceTree = ""; }; 7F9CDAF81E1F809C00E0C805 /* RNSVGPathParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNSVGPathParser.h; path = Utils/RNSVGPathParser.h; sourceTree = ""; }; 7F9CDAF91E1F809C00E0C805 /* RNSVGPathParser.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNSVGPathParser.m; path = Utils/RNSVGPathParser.m; sourceTree = ""; }; @@ -501,7 +500,6 @@ 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */, 10ABC7381D43982B006CCF6E /* RNSVGVBMOS.h */, 10ABC7371D439779006CCF6E /* RNSVGCGFCRule.h */, - 7F08CEA31E23481F00650F83 /* RNSVGTextAnchor.h */, 7FC260CC1E3499BC00A39833 /* RNSVGViewBox.h */, 7FC260CD1E3499BC00A39833 /* RNSVGViewBox.m */, 1039D29E1CE72177001E90A8 /* RNSVGCGFloatArray.h */, diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 1d549ccc..486699dd 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -870,7 +870,7 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) [self traverseTextSuperviews:^(__kindof RNSVGText *node) { if ([node class] == [RNSVGTextPath class]) { textPath = (RNSVGTextPath*) node; - [[textPath getPath] getPathLength:&_pathLength lineCount:&lineCount lengths:&lengths lines:&lines isClosed:&isClosed]; + [textPath getPathLength:&_pathLength lineCount:&lineCount lengths:&lengths lines:&lines isClosed:&isClosed]; return NO; } return YES; diff --git a/ios/Text/RNSVGText.h b/ios/Text/RNSVGText.h index 42f66e40..47dddf2e 100644 --- a/ios/Text/RNSVGText.h +++ b/ios/Text/RNSVGText.h @@ -8,7 +8,6 @@ #import #import "RNSVGGroup.h" -#import "RNSVGTextAnchor.h" #import "AlignmentBaseline.h" @interface RNSVGText : RNSVGGroup diff --git a/ios/Text/RNSVGTextPath.h b/ios/Text/RNSVGTextPath.h index ec08b7ad..4105dc6f 100644 --- a/ios/Text/RNSVGTextPath.h +++ b/ios/Text/RNSVGTextPath.h @@ -19,6 +19,7 @@ @property (nonatomic, strong) NSString *spacing; @property (nonatomic, strong) NSString *startOffset; -- (RNSVGPath *)getPath; +- (void)getPathLength:(CGFloat*)length lineCount:(NSInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; + @end diff --git a/ios/Text/RNSVGTextPath.m b/ios/Text/RNSVGTextPath.m index 310a6b0e..db633b60 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -8,28 +8,267 @@ #import "RNSVGTextPath.h" +#import "BezierElement.h" + +/* Bezier logic from PerformanceBezier */ +/* + + ## License + + Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 United States License. + + For attribution, please include: + + 1. Mention original author "Adam Wulf for Loose Leaf app" + 2. Link to https://getlooseleaf.com/opensource/ + 3. Link to https://github.com/adamwulf/PerformanceBezier + + */ +static CGFloat idealFlatness = .01; + +/** + * returns the distance between two points + */ +CGFloat distance(CGPoint p1, CGPoint p2) +{ + CGFloat dx = p2.x - p1.x; + CGFloat dy = p2.y - p1.y; + + return sqrt(dx*dx + dy*dy); +} + +/** + * returns the dot product of two coordinates + */ +CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { + return p1.x * p2.x + p1.y * p2.y; +} + +/** + * returns the shortest distance from a point to a line + */ +CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ + CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); + CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); + CGFloat c1 = dotProduct(w, v); + CGFloat c2 = dotProduct(v, v); + CGFloat d; + if (c1 <= 0) { + d = distance(point, start); + } + else if (c2 <= c1) { + d = distance(point, end); + } + else { + CGFloat b = c1 / c2; + CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); + d = distance(point, Pb); + } + return d; +} + +/** + * calculate the point on a bezier at time t + * where 0 < t < 1 + */ +CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + CGPoint bez1[4]; + CGPoint bez2[4]; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; + + return CGPointMake(bez1[3].x, bez1[3].y); +} + +// Subdivide a Bézier (specific division) +/* + * (c) 2004 Alastair J. Houghton + * All Rights Reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. The name of the author of this software may not be used to endorse + * or promote products derived from the software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS + * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER BE LIABLE FOR ANY DIRECT, INDIRECT, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ +void subdivideBezierAtT(const CGPoint bez[4], CGPoint bez1[4], CGPoint bez2[4], CGFloat t) +{ + CGPoint q; + CGFloat mt = 1 - t; + + bez1[0].x = bez[0].x; + bez1[0].y = bez[0].y; + bez2[3].x = bez[3].x; + bez2[3].y = bez[3].y; + + q.x = mt * bez[1].x + t * bez[2].x; + q.y = mt * bez[1].y + t * bez[2].y; + bez1[1].x = mt * bez[0].x + t * bez[1].x; + bez1[1].y = mt * bez[0].y + t * bez[1].y; + bez2[2].x = mt * bez[2].x + t * bez[3].x; + bez2[2].y = mt * bez[2].y + t * bez[3].y; + + bez1[2].x = mt * bez1[1].x + t * q.x; + bez1[2].y = mt * bez1[1].y + t * q.y; + bez2[1].x = mt * q.x + t * bez2[2].x; + bez2[1].y = mt * q.y + t * bez2[2].y; + + bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; + bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; +} + +void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat *length, NSMutableArray *lengths) { + NSArray *line = @[[NSValue valueWithCGPoint:*last], [NSValue valueWithCGPoint:*next]]; + [lines addObject:line]; + *length += distance(*last, *next); + [lengths addObject:[NSNumber numberWithDouble:*length]]; + *last = *next; +} @implementation RNSVGTextPath +{ + CGPathRef _path; + NSMutableArray *lengths; + NSMutableArray *lines; + NSInteger lineCount; + CGFloat length; + BOOL isClosed; +} + +- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP +{ + RNSVGSvgView *svg = [self getSvgView]; + RNSVGNode *template = [svg getDefinedTemplate:self.href]; + CGPathRef path = [template getPath:nil]; + + if (_path != path) { + _path = path; + CGPoint origin = CGPointMake (0.0, 0.0); + CGPoint last = CGPointMake (0.0, 0.0); + lengths = [NSMutableArray array]; + lines = [NSMutableArray array]; + isClosed = NO; + lineCount = 0; + length = 0; + + NSArray *elements = [BezierElement elementsFromCGPath:path]; + for (BezierElement *element in elements) { + switch (element.elementType) + { + case kCGPathElementMoveToPoint: + origin = last = element.point; + break; + + case kCGPathElementAddLineToPoint: { + CGPoint next = element.point; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + break; + } + case kCGPathElementAddQuadCurveToPoint: + case kCGPathElementAddCurveToPoint: + { + // handle both curve types gracefully + CGPoint curveTo = element.point; + CGPoint ctrl1 = element.controlPoint1; + CGPoint ctrl2 = element.elementType == kCGPathElementAddQuadCurveToPoint ? ctrl1 : element.controlPoint2; + + // this is the bezier for our current element + CGPoint bezier[4] = { last, ctrl1, ctrl2, curveTo }; + NSValue *arr = [NSValue valueWithBytes:&bezier objCType:@encode(CGPoint[4])]; + NSMutableArray *curves = [NSMutableArray arrayWithObjects:arr, nil]; + + for (NSInteger curveIndex = 0; curveIndex >= 0; curveIndex--) { + CGPoint bez[4]; + [curves[curveIndex] getValue:&bez]; + [curves removeLastObject]; + + // calculate the error rate of the curve vs + // a line segement between the start and end points + CGPoint onCurve = bezierPointAtT(bez, .5); + CGPoint next = bez[3]; + CGFloat error = distanceOfPointToLine(onCurve, last, next); + + // if the error is less than our accepted level of error + // then add a line, else, split the curve in half + if (error <= idealFlatness) { + addLine(&last, &next, lines, &length, lengths); + lineCount++; + } else { + CGPoint bez1[4], bez2[4]; + subdivideBezierAtT(bez, bez1, bez2, .5); + [curves addObject:[NSValue valueWithBytes:&bez2 objCType:@encode(CGPoint[4])]]; + [curves addObject:[NSValue valueWithBytes:&bez1 objCType:@encode(CGPoint[4])]]; + curveIndex += 2; + } + } + last = curveTo; + break; + } + + case kCGPathElementCloseSubpath: { + CGPoint next = origin; + addLine(&last, &next, lines, &length, lengths); + lineCount++; + isClosed = YES; + break; + } + + default: + break; + } + } + } + + *lineCountP = lineCount; + *isClosedP = isClosed; + *lengthsP = lengths; + *lengthP = length; + *linesP = lines; +} - (void)renderLayerTo:(CGContextRef)context { [self renderGroupTo:context]; } -- (RNSVGPath *)getPath -{ - RNSVGSvgView *svg = [self getSvgView]; - RNSVGNode *template = [svg getDefinedTemplate:self.href]; - - if ([template class] != [RNSVGPath class]) { - // warning about this. - return nil; - } - - RNSVGPath *path = (RNSVGPath *)template; - return path; -} - - (CGPathRef)getPath:(CGContextRef)context { return [self getGroupPath:context]; diff --git a/ios/Utils/BezierElement.h b/ios/Utils/BezierElement.h index a78c8721..5d59efc0 100644 --- a/ios/Utils/BezierElement.h +++ b/ios/Utils/BezierElement.h @@ -17,5 +17,9 @@ @property (nonatomic, assign) CGPoint controlPoint1; @property (nonatomic, assign) CGPoint controlPoint2; +// Instance creation ++ (instancetype) elementWithPathElement: (CGPathElement) element; ++ (NSArray *) elementsFromCGPath:(CGPathRef)path; + @end; diff --git a/ios/Utils/BezierElement.m b/ios/Utils/BezierElement.m index 73a8eb50..98bc65d1 100644 --- a/ios/Utils/BezierElement.m +++ b/ios/Utils/BezierElement.m @@ -21,5 +21,57 @@ } return self; } + ++ (instancetype) elementWithPathElement: (CGPathElement) element +{ + BezierElement *newElement = [[self alloc] init]; + newElement.elementType = element.type; + + switch (newElement.elementType) + { + case kCGPathElementCloseSubpath: + break; + case kCGPathElementMoveToPoint: + case kCGPathElementAddLineToPoint: + { + newElement.point = element.points[0]; + break; + } + case kCGPathElementAddQuadCurveToPoint: + { + newElement.point = element.points[1]; + newElement.controlPoint1 = element.points[0]; + break; + } + case kCGPathElementAddCurveToPoint: + { + newElement.point = element.points[2]; + newElement.controlPoint1 = element.points[0]; + newElement.controlPoint2 = element.points[1]; + break; + } + default: + break; + } + + return newElement; +} + +// Convert one element to BezierElement and save to array +void GetBezierElements(void *info, const CGPathElement *element) +{ + NSMutableArray *bezierElements = (__bridge NSMutableArray *)info; + if (element) + [bezierElements addObject:[BezierElement elementWithPathElement:*element]]; +} + +// Retrieve array of component elements ++ (NSArray *) elementsFromCGPath:(CGPathRef)path +{ + NSMutableArray *elements = [NSMutableArray array]; + CGPathApply(path, (__bridge void *)elements, GetBezierElements); + return elements; +} + @end diff --git a/ios/Utils/RCTConvert+RNSVG.h b/ios/Utils/RCTConvert+RNSVG.h index 0b266d45..49500fd6 100644 --- a/ios/Utils/RCTConvert+RNSVG.h +++ b/ios/Utils/RCTConvert+RNSVG.h @@ -13,7 +13,6 @@ #import #import "RNSVGCGFCRule.h" #import "RNSVGVBMOS.h" -#import "RNSVGTextAnchor.h" #import "RNSVGUnits.h" #import "RNSVGPathParser.h" @@ -21,7 +20,6 @@ @interface RCTConvert (RNSVG) -+ (RNSVGTextAnchor)RNSVGTextAnchor:(id)json; + (RNSVGCGFCRule)RNSVGCGFCRule:(id)json; + (RNSVGVBMOS)RNSVGVBMOS:(id)json; + (RNSVGUnits)RNSVGUnits:(id)json; diff --git a/ios/Utils/RCTConvert+RNSVG.m b/ios/Utils/RCTConvert+RNSVG.m index d3554ed0..53655148 100644 --- a/ios/Utils/RCTConvert+RNSVG.m +++ b/ios/Utils/RCTConvert+RNSVG.m @@ -26,13 +26,6 @@ RCT_ENUM_CONVERTER(RNSVGVBMOS, (@{ @"none": @(kRNSVGVBMOSNone) }), kRNSVGVBMOSMeet, intValue) -RCT_ENUM_CONVERTER(RNSVGTextAnchor, (@{ - @"auto": @(kRNSVGTextAnchorAuto), - @"start": @(kRNSVGTextAnchorStart), - @"middle": @(kRNSVGTextAnchorMiddle), - @"end": @(kRNSVGTextAnchorEnd) - }), kRNSVGTextAnchorAuto, intValue) - RCT_ENUM_CONVERTER(RNSVGUnits, (@{ @"objectBoundingBox": @(kRNSVGUnitsObjectBoundingBox), @"userSpaceOnUse": @(kRNSVGUnitsUserSpaceOnUse), diff --git a/ios/Utils/RNSVGPathParser.h b/ios/Utils/RNSVGPathParser.h index da53782b..5ea68b82 100644 --- a/ios/Utils/RNSVGPathParser.h +++ b/ios/Utils/RNSVGPathParser.h @@ -13,6 +13,5 @@ - (instancetype) initWithPathString:(NSString *)d; - (CGPathRef)getPath; -- (NSArray *)getBezierCurves; @end diff --git a/ios/Utils/RNSVGPathParser.m b/ios/Utils/RNSVGPathParser.m index 76d78c71..1c434cde 100644 --- a/ios/Utils/RNSVGPathParser.m +++ b/ios/Utils/RNSVGPathParser.m @@ -16,7 +16,6 @@ NSString* _d; NSString* _originD; NSRegularExpression* _pathRegularExpression; - NSMutableArray* _bezierCurves; float _penX; float _penY; float _penDownX; @@ -42,7 +41,6 @@ { CGMutablePathRef path = CGPathCreateMutable(); NSArray* results = [_pathRegularExpression matchesInString:_d options:0 range:NSMakeRange(0, [_d length])]; - _bezierCurves = [[NSMutableArray alloc] init]; unsigned long count = [results count]; if (count) { @@ -121,15 +119,6 @@ return (CGPathRef)CFAutorelease(path); } -- (NSArray *)getBezierCurves -{ - if (!_bezierCurves) { - CGPathRelease([self getPath]); - } - - return [_bezierCurves copy]; -} - - (NSString *)getNextValue:(NSTextCheckingResult *)result { if (!result) { @@ -158,11 +147,6 @@ _pivotX = _penX = x; _pivotY = _penY = y; CGPathMoveToPoint(path, nil, x, y); - - BezierElement *newElement = [[BezierElement alloc] init]; - newElement.elementType = kCGPathElementMoveToPoint; - newElement.point = CGPointMake(x, y); - [_bezierCurves addObject:newElement]; } - (void)line:(CGMutablePathRef)path x:(float)x y:(float)y @@ -175,11 +159,6 @@ _pivotX = _penX = x; _pivotY = _penY = y; CGPathAddLineToPoint(path, nil, x, y); - - BezierElement *newElement = [[BezierElement alloc] init]; - newElement.elementType = kCGPathElementAddLineToPoint; - newElement.point = CGPointMake(x, y); - [_bezierCurves addObject:newElement]; } - (void)curve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey @@ -205,13 +184,6 @@ _penX = ex; _penY = ey; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); - - BezierElement *newElement = [[BezierElement alloc] init]; - newElement.elementType = kCGPathElementAddCurveToPoint; - newElement.controlPoint1 = CGPointMake(c1x, c1y); - newElement.controlPoint2 = CGPointMake(c2x, c2y); - newElement.point = CGPointMake(ex, ey); - [_bezierCurves addObject:newElement]; } - (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey @@ -379,13 +351,6 @@ float ex = cx + xx * x + yx * y; float ey = cy + xy * x + yy * y; CGPathAddCurveToPoint(path, nil, c1x, c1y, c2x, c2y, ex, ey); - - BezierElement *newElement = [[BezierElement alloc] init]; - newElement.elementType = kCGPathElementAddCurveToPoint; - newElement.controlPoint1 = CGPointMake(c1x, c1y); - newElement.controlPoint2 = CGPointMake(c2x, c2y); - newElement.point = CGPointMake(ex, ey); - [_bezierCurves addObject:newElement]; } } @@ -396,9 +361,6 @@ _penY = _penDownY; _penDownSet = NO; CGPathCloseSubpath(path); - BezierElement *newElement = [[BezierElement alloc] init]; - newElement.elementType = kCGPathElementCloseSubpath; - [_bezierCurves addObject:newElement]; } } diff --git a/ios/Utils/RNSVGTextAnchor.h b/ios/Utils/RNSVGTextAnchor.h deleted file mode 100644 index 06eded9d..00000000 --- a/ios/Utils/RNSVGTextAnchor.h +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Copyright (c) 2015-present, Horcrux. - * All rights reserved. - * - * This source code is licensed under the MIT-style license found in the - * LICENSE file in the root directory of this source tree. - */ - -typedef CF_ENUM(int32_t, RNSVGTextAnchor) { - kRNSVGTextAnchorAuto, - kRNSVGTextAnchorStart, - kRNSVGTextAnchorMiddle, - kRNSVGTextAnchorEnd -}; diff --git a/ios/ViewManagers/RNSVGTSpanManager.m b/ios/ViewManagers/RNSVGTSpanManager.m index d9ddc646..fc5ca79d 100644 --- a/ios/ViewManagers/RNSVGTSpanManager.m +++ b/ios/ViewManagers/RNSVGTSpanManager.m @@ -9,7 +9,6 @@ #import "RNSVGTSpanManager.h" #import "RNSVGTSpan.h" -#import "RNSVGTextAnchor.h" #import "RCTConvert+RNSVG.h" @implementation RNSVGTSpanManager From 9ab405cbec9b8fe83ff892ffd08e6faabe882d8a Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sun, 31 Dec 2017 01:38:28 +0200 Subject: [PATCH 06/14] Add binary search for text paths with lineCount >= 16 --- ios/Text/RNSVGTSpan.m | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 486699dd..fb3fca95 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -785,20 +785,25 @@ NSCharacterSet *separators = nil; continue; } - int i = 0; - CGFloat totalLength = 0; - CGFloat prevLength = 0; + // Investigation suggests binary search is faster at lineCount >= 16 + // https://gist.github.com/msand/4c7993319425f9d7933be58ad9ada1a4 + NSUInteger i = lineCount < 16 ? + [lengths + indexOfObjectPassingTest:^(NSNumber* length, NSUInteger index, BOOL * _Nonnull stop) { + BOOL contains = midPoint <= [length doubleValue]; + return contains; + }] + : + [lengths + indexOfObject:[NSNumber numberWithDouble:midPoint] + inSortedRange:NSMakeRange(0, lineCount) + options:NSBinarySearchingInsertionIndex + usingComparator:^(NSNumber* obj1, NSNumber* obj2) { + return [obj1 compare:obj2]; + }]; - // TODO investigate at what lineCount a binary search is faster - while (i < lineCount - 1) { - prevLength = totalLength; - totalLength = [[lengths objectAtIndex: i] floatValue]; - if (totalLength < midPoint) { - i++; - } else { - break; - } - }; + CGFloat totalLength = [lengths[i] doubleValue]; + CGFloat prevLength = i == 0 ? 0 : [lengths[i - 1] doubleValue]; CGFloat length = totalLength - prevLength; CGFloat targetPercent = (midPoint - prevLength) / length; From f8b0d34c6ab6f985ebd19e73b923b5d53b855309 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sun, 31 Dec 2017 04:23:58 +0200 Subject: [PATCH 07/14] Fix types and glyph context nextX spacing handling. --- ios/Text/RNSVGTSpan.m | 18 ++++++++---------- ios/Text/RNSVGTextPath.h | 2 +- ios/Text/RNSVGTextPath.m | 4 ++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index fb3fca95..4800b421 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -20,7 +20,7 @@ NSCharacterSet *separators = nil; RNSVGTextPath *textPath; NSArray *lengths; NSArray *lines; - NSInteger lineCount; + NSUInteger lineCount; BOOL isClosed; } @@ -115,7 +115,7 @@ NSCharacterSet *separators = nil; FontData* font = [gc getFont]; NSUInteger n = str.length; bool ligature[n]; - for (int i = 0; i < n; i++){ + for (NSUInteger i = 0; i < n; i++){ ligature[i] = NO; } /* @@ -682,7 +682,6 @@ NSCharacterSet *separators = nil; break; } } - int i = -1; CFDictionaryRef ligattributes; NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; @@ -697,8 +696,7 @@ NSCharacterSet *separators = nil; }; } for(CFIndex g = 0; g < runGlyphCount; g++) { - i++; - bool alreadyRenderedGraphemeCluster = ligature[i]; + bool alreadyRenderedGraphemeCluster = ligature[g]; /* Determine the glyph's charwidth (i.e., the amount which the current text position @@ -721,13 +719,13 @@ NSCharacterSet *separators = nil; kerning = kerned - charWidth; } - char currentChar = [str characterAtIndex:i]; + char currentChar = [str characterAtIndex:g]; bool isWordSeparator = [separators characterIsMember:currentChar]; double wordSpace = isWordSeparator ? wordSpacing : 0; double spacing = wordSpace + letterSpacing; double advance = charWidth + spacing; - double x = [gc nextXWithDouble:kerning + charWidth]; + double x = [gc nextXWithDouble:kerning + advance]; double y = [gc nextY]; double dx = [gc nextDeltaX]; double dy = [gc nextDeltaY]; @@ -739,12 +737,12 @@ NSCharacterSet *separators = nil; continue; } - int len = 2; - int nextIndex = i; + NSUInteger len = 2; + NSUInteger nextIndex = g; CGGlyph glyph = glyphs[g]; bool hasLigature = false; while (++nextIndex < n) { - NSString* nextLigature = [str substringWithRange:NSMakeRange(i, len++)]; + NSString* nextLigature = [str substringWithRange:NSMakeRange(g, len++)]; bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); if (hasNextLigature) { ligature[nextIndex] = true; diff --git a/ios/Text/RNSVGTextPath.h b/ios/Text/RNSVGTextPath.h index 4105dc6f..f6a3bfeb 100644 --- a/ios/Text/RNSVGTextPath.h +++ b/ios/Text/RNSVGTextPath.h @@ -19,7 +19,7 @@ @property (nonatomic, strong) NSString *spacing; @property (nonatomic, strong) NSString *startOffset; -- (void)getPathLength:(CGFloat*)length lineCount:(NSInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; +- (void)getPathLength:(CGFloat*)length lineCount:(NSUInteger*)lineCount lengths:(NSArray* __strong *)lengths lines:(NSArray* __strong *)lines isClosed:(BOOL*)isClosed; @end diff --git a/ios/Text/RNSVGTextPath.m b/ios/Text/RNSVGTextPath.m index db633b60..ab9b5f67 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -167,12 +167,12 @@ void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat CGPathRef _path; NSMutableArray *lengths; NSMutableArray *lines; - NSInteger lineCount; + NSUInteger lineCount; CGFloat length; BOOL isClosed; } -- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP +- (void)getPathLength:(CGFloat*)lengthP lineCount:(NSUInteger*)lineCountP lengths:(NSArray* __strong *)lengthsP lines:(NSArray* __strong *)linesP isClosed:(BOOL*)isClosedP { RNSVGSvgView *svg = [self getSvgView]; RNSVGNode *template = [svg getDefinedTemplate:self.href]; From 17a66f3c2c36bd7aff359d9a71b57510a2dae055 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sun, 31 Dec 2017 15:36:26 +0200 Subject: [PATCH 08/14] Fix ligature glyph advance value calculation on ios --- ios/Text/RNSVGTSpan.m | 114 ++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 60 deletions(-) diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index 4800b421..c073a0d6 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -696,36 +696,58 @@ NSCharacterSet *separators = nil; }; } for(CFIndex g = 0; g < runGlyphCount; g++) { + CGGlyph glyph = glyphs[g]; + bool hasLigature = false; + CGFloat charWidth = 0; + double advance = 0; + bool alreadyRenderedGraphemeCluster = ligature[g]; + if (!alreadyRenderedGraphemeCluster) { + /* + Determine the glyph's charwidth (i.e., the amount which the current text position + advances horizontally when the glyph is drawn using horizontal text layout). + */ + double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, glyphs + g, NULL, 1); + charWidth = unkernedAdvance * scaleSpacingAndGlyphs; - /* - Determine the glyph's charwidth (i.e., the amount which the current text position - advances horizontally when the glyph is drawn using horizontal text layout). - */ - double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, glyphs + g, NULL, 1); - CGFloat charWidth = unkernedAdvance * scaleSpacingAndGlyphs; + /* + For each subsequent glyph, set a new startpoint-on-the-path as the previous + endpoint-on-the-path, but with appropriate adjustments taking into account + horizontal kerning tables in the font and current values of various attributes + and properties, including spacing properties (e.g. letter-spacing and word-spacing) + and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All + adjustments are calculated as distance adjustments along the path, calculated + using the user agent's distance along the path algorithm. + */ + if (autoKerning) { + double kerned = advances[g].width * scaleSpacingAndGlyphs; + kerning = kerned - charWidth; + } - /* - For each subsequent glyph, set a new startpoint-on-the-path as the previous - endpoint-on-the-path, but with appropriate adjustments taking into account - horizontal kerning tables in the font and current values of various attributes - and properties, including spacing properties (e.g. letter-spacing and word-spacing) - and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All - adjustments are calculated as distance adjustments along the path, calculated - using the user agent's distance along the path algorithm. - */ - if (autoKerning) { - double kerned = advances[g].width * scaleSpacingAndGlyphs; - kerning = kerned - charWidth; + NSUInteger len = 2; + NSUInteger nextIndex = g; + while (++nextIndex < n) { + NSString* nextLigature = [str substringWithRange:NSMakeRange(g, len++)]; + bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); + if (hasNextLigature) { + ligature[nextIndex] = true; + hasLigature = true; + } else { + break; + } + } + if (hasLigature) { + charWidth = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); + } + + char currentChar = [str characterAtIndex:g]; + bool isWordSeparator = [separators characterIsMember:currentChar]; + double wordSpace = isWordSeparator ? wordSpacing : 0; + double spacing = wordSpace + letterSpacing; + advance = charWidth + spacing; } - char currentChar = [str characterAtIndex:g]; - bool isWordSeparator = [separators characterIsMember:currentChar]; - double wordSpace = isWordSeparator ? wordSpacing : 0; - double spacing = wordSpace + letterSpacing; - double advance = charWidth + spacing; - - double x = [gc nextXWithDouble:kerning + advance]; + double x = [gc nextXWithDouble:(alreadyRenderedGraphemeCluster ? 0 : kerning + advance)]; double y = [gc nextY]; double dx = [gc nextDeltaX]; double dy = [gc nextDeltaY]; @@ -736,21 +758,6 @@ NSCharacterSet *separators = nil; // But, make sure to increment index positions by making gc.next() calls. continue; } - - NSUInteger len = 2; - NSUInteger nextIndex = g; - CGGlyph glyph = glyphs[g]; - bool hasLigature = false; - while (++nextIndex < n) { - NSString* nextLigature = [str substringWithRange:NSMakeRange(g, len++)]; - bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); - if (hasNextLigature) { - ligature[nextIndex] = true; - hasLigature = true; - } else { - break; - } - } CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyph, nil); advance *= side; @@ -804,19 +811,20 @@ NSCharacterSet *separators = nil; CGFloat prevLength = i == 0 ? 0 : [lengths[i - 1] doubleValue]; CGFloat length = totalLength - prevLength; - CGFloat targetPercent = (midPoint - prevLength) / length; + CGFloat percent = (midPoint - prevLength) / length; NSArray * points = [lines objectAtIndex: i]; CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; - CGPoint slope; - CGPoint mid = InterpolateLineSegment(p1, p2, targetPercent, &slope); + CGFloat ldx = p2.x - p1.x; + CGFloat ldy = p2.y - p1.y; + CGFloat angle = atan2(ldy, ldx); - // Calculate the rotation - double angle = atan2(slope.y, slope.x); + CGFloat px = p1.x + ldx * percent; + CGFloat py = p1.y + ldy * percent; - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(mid.x, mid.y), transform); + transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(px, py), transform); transform = CGAffineTransformConcat(CGAffineTransformMakeRotation(angle + r), transform); transform = CGAffineTransformScale(transform, scaledDirection, side); transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(-halfWay, y + dy + baselineShift), transform); @@ -837,20 +845,6 @@ NSCharacterSet *separators = nil; return path; } -CGPoint InterpolateLineSegment(CGPoint p1, CGPoint p2, CGFloat percent, CGPoint *slope) -{ - CGFloat dx = p2.x - p1.x; - CGFloat dy = p2.y - p1.y; - - if (slope) - *slope = CGPointMake(dx, dy); - - CGFloat px = p1.x + dx * percent; - CGFloat py = p1.y + dy * percent; - - return CGPointMake(px, py); -} - CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) { switch (textAnchor) { From 4a8808b0fbcdb633c6b9c7063b2992ba57d52fdf Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Tue, 2 Jan 2018 23:17:04 +0200 Subject: [PATCH 09/14] Fix handling of combined ligatures and kerning. Remove hasGlyph helper and ligature tracking array. Use single call to: - CFAttributedStringCreate - CTLineCreateWithAttributedString and a call per bi-di/layout run to: - CTRunGetGlyphs for glyphs - CTFontGetAdvancesForGlyphs for advances - CTRunGetStringIndices for indices of ligatures And an efficient loop over the substituted characters of ligatures, to make glyph context calls for incrementing the attribute indices. --- ios/Text/RNSVGTSpan.m | 999 ++++++++++++++++++++---------------------- 1 file changed, 469 insertions(+), 530 deletions(-) diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index c073a0d6..3499c4e0 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -11,6 +11,7 @@ #import "FontData.h" NSCharacterSet *separators = nil; +static double radToDeg = 180 / M_PI; @implementation RNSVGTSpan { @@ -93,31 +94,10 @@ NSCharacterSet *separators = nil; { // Create a dictionary for this font CTFontRef fontRef = [self getFontFromContext]; - CFDictionaryRef attributes; - if (fontRef != nil) { - attributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTFontAttributeName: (__bridge id)fontRef, - (NSString *)kCTForegroundColorFromContextAttributeName: @YES, - (NSString *)NSLigatureAttributeName: @0 }; - } else { - attributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTForegroundColorFromContextAttributeName: @YES, - (NSString *)NSLigatureAttributeName: @0 }; - } - - CFStringRef string = (__bridge CFStringRef)str; - CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); - CTLineRef line = CTLineCreateWithAttributedString(attrString); - CGMutablePathRef path = CGPathCreateMutable(); - CFArrayRef runs = CTLineGetGlyphRuns(line); GlyphContext* gc = [[self getTextRoot] getGlyphContext]; FontData* font = [gc getFont]; NSUInteger n = str.length; - bool ligature[n]; - for (NSUInteger i = 0; i < n; i++){ - ligature[i] = NO; - } /* * * Three properties affect the space between characters and words: @@ -250,513 +230,499 @@ NSCharacterSet *separators = nil; // OpenType.js font data NSDictionary * fontData = font->fontData; + NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; + CFDictionaryRef attributes; + if (fontRef != nil) { + attributes = (__bridge CFDictionaryRef)@{ + (NSString *)kCTFontAttributeName: (__bridge id)fontRef, + (NSString *)NSLigatureAttributeName: lig }; + } else { + attributes = (__bridge CFDictionaryRef)@{ + (NSString *)NSLigatureAttributeName: lig }; + } - double tau = 2 * M_PI; - double radToDeg = 360 / tau; + CFStringRef string = (__bridge CFStringRef)str; + CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); + CTLineRef line = CTLineCreateWithAttributedString(attrString); + + /* + Determine the startpoint-on-the-path for the first glyph using attribute ‘startOffset’ + and property text-anchor. + + For text-anchor:start, startpoint-on-the-path is the point + on the path which represents the point on the path which is ‘startOffset’ distance + along the path from the start of the path, calculated using the user agent's distance + along the path algorithm. + + For text-anchor:middle, startpoint-on-the-path is the point + on the path which represents the point on the path which is [ ‘startOffset’ minus half + of the total advance values for all of the glyphs in the ‘textPath’ element ] distance + along the path from the start of the path, calculated using the user agent's distance + along the path algorithm. + + For text-anchor:end, startpoint-on-the-path is the point on + the path which represents the point on the path which is [ ‘startOffset’ minus the + total advance values for all of the glyphs in the ‘textPath’ element ]. + + Before rendering the first glyph, the horizontal component of the startpoint-on-the-path + is adjusted to take into account various horizontal alignment text properties and + attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. + */ + enum TextAnchor textAnchor = font->textAnchor; + CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); + double textMeasure = CGRectGetWidth(textBounds); + double offset = getTextAnchorOffset(textAnchor, textMeasure); + + bool hasTextPath = textPath != nil; + + int side = 1; + double startOfRendering = 0; + double endOfRendering = _pathLength; + double fontSize = [gc getFontSize]; + bool sharpMidLine = false; + if (hasTextPath) { + sharpMidLine = TextPathMidLineFromString([textPath midLine]) == TextPathMidLineSharp; + /* + Name + side + Value + left | right + initial value + left + Animatable + yes + + Determines the side of the path the text is placed on + (relative to the path direction). + + Specifying a value of right effectively reverses the path. + + Added in SVG 2 to allow text either inside or outside closed subpaths + and basic shapes (e.g. rectangles, circles, and ellipses). + + Adding 'side' was resolved at the Sydney (2015) meeting. + */ + side = TextPathSideFromString([textPath side]) == TextPathSideRight ? -1 : 1; + /* + Name + startOffset + Value + | | + initial value + 0 + Animatable + yes + + An offset from the start of the path for the initial current text position, + calculated using the user agent's distance along the path algorithm, + after converting the path to the ‘textPath’ element's coordinate system. + + If a other than a percentage is given, then the ‘startOffset’ + represents a distance along the path measured in the current user coordinate + system for the ‘textPath’ element. + + If a percentage is given, then the ‘startOffset’ represents a percentage + distance along the entire path. Thus, startOffset="0%" indicates the start + point of the path and startOffset="100%" indicates the end point of the path. + + Negative values and values larger than the path length (e.g. 150%) are allowed. + + Any typographic characters with mid-points that are not on the path are not rendered + + For paths consisting of a single closed subpath (including an equivalent path for a + basic shape), typographic characters are rendered along one complete circuit of the + path. The text is aligned as determined by the text-anchor property to a position + along the path set by the ‘startOffset’ attribute. + + For the start (end) value, the text is rendered from the start (end) of the line + until the initial position along the path is reached again. + + For the middle, the text is rendered from the middle point in both directions until + a point on the path equal distance in both directions from the initial position on + the path is reached. + */ + double absoluteStartOffset = [PropHelper fromRelativeWithNSString:textPath.startOffset + relative:_pathLength + offset:0 + scale:1 + fontSize:fontSize]; + offset += absoluteStartOffset; + if (isClosed) { + double halfPathDistance = _pathLength / 2; + startOfRendering = absoluteStartOffset + (textAnchor == TextAnchorMiddle ? -halfPathDistance : 0); + endOfRendering = startOfRendering + _pathLength; + } + /* + TextPathSpacing spacing = textPath.getSpacing(); + if (spacing == TextPathSpacing.auto) { + // Hmm, what to do here? + // https://svgwg.org/svg2-draft/text.html#TextPathElementSpacingAttribute + } + */ + } + + /* + Name + method + Value + align | stretch + initial value + align + Animatable + yes + Indicates the method by which text should be rendered along the path. + + A value of align indicates that the typographic character should be rendered using + simple 2×3 matrix transformations such that there is no stretching/warping of the + typographic characters. Typically, supplemental rotation, scaling and translation + transformations are done for each typographic characters to be rendered. + + As a result, with align, in fonts where the typographic characters are designed to be + connected (e.g., cursive fonts), the connections may not align properly when text is + rendered along a path. + + A value of stretch indicates that the typographic character outlines will be converted + into paths, and then all end points and control points will be adjusted to be along the + perpendicular vectors from the path, thereby stretching and possibly warping the glyphs. + + With this approach, connected typographic characters, such as in cursive scripts, + will maintain their connections. (Non-vertical straight path segments should be + converted to Bézier curves in such a way that horizontal straight paths have an + (approximately) constant offset from the path along which the typographic characters + are rendered.) + + TODO implement stretch + */ + + /* + Name Value Initial value Animatable + textLength | | See below yes + + The author's computation of the total sum of all of the advance values that correspond + to character data within this element, including the advance value on the glyph + (horizontal or vertical), the effect of properties letter-spacing and word-spacing and + adjustments due to attributes ‘dx’ and ‘dy’ on this ‘text’ or ‘tspan’ element or any + descendants. This value is used to calibrate the user agent's own calculations with + that of the author. + + The purpose of this attribute is to allow the author to achieve exact alignment, + in visual rendering order after any bidirectional reordering, for the first and + last rendered glyphs that correspond to this element; thus, for the last rendered + character (in visual rendering order after any bidirectional reordering), + any supplemental inter-character spacing beyond normal glyph advances are ignored + (in most cases) when the user agent determines the appropriate amount to expand/compress + the text string to fit within a length of ‘textLength’. + + If attribute ‘textLength’ is specified on a given element and also specified on an + ancestor, the adjustments on all character data within this element are controlled by + the value of ‘textLength’ on this element exclusively, with the possible side-effect + that the adjustment ratio for the contents of this element might be different than the + adjustment ratio used for other content that shares the same ancestor. The user agent + must assume that the total advance values for the other content within that ancestor is + the difference between the advance value on that ancestor and the advance value for + this element. + + This attribute is not intended for use to obtain effects such as shrinking or + expanding text. + + A negative value is an error (see Error processing). + + The ‘textLength’ attribute is only applied when the wrapping area is not defined by the + TODO shape-inside or the inline-size properties. It is also not applied for any ‘text’ or + TODO ‘tspan’ element that has forced line breaks (due to a white-space value of pre or + pre-line). + + If the attribute is not specified anywhere within a ‘text’ element, the effect is as if + the author's computation exactly matched the value calculated by the user agent; + thus, no advance adjustments are made. + */ + double scaleSpacingAndGlyphs = 1; + NSString *mTextLength = [self textLength]; + enum TextLengthAdjust mLengthAdjust = TextLengthAdjustFromString([self lengthAdjust]); + if (mTextLength != nil) { + double author = [PropHelper fromRelativeWithNSString:mTextLength + relative:[gc getWidth] + offset:0 + scale:1 + fontSize:fontSize]; + if (author < 0) { + NSException *e = [NSException + exceptionWithName:@"NegativeTextLength" + reason:@"Negative textLength value" + userInfo:nil]; + @throw e; + } + switch (mLengthAdjust) { + default: + case TextLengthAdjustSpacing: + // TODO account for ligatures + letterSpacing += (author - textMeasure) / (n - 1); + break; + case TextLengthAdjustSpacingAndGlyphs: + scaleSpacingAndGlyphs = author / textMeasure; + break; + } + } + double scaledDirection = scaleSpacingAndGlyphs * side; + + /* + https://developer.mozilla.org/en/docs/Web/CSS/vertical-align + https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html + https://www.microsoft.com/typography/otspec/base.htm + http://apike.ca/prog_svg_text_style.html + https://www.w3schools.com/tags/canvas_textbaseline.asp + http://vanseodesign.com/web-design/svg-text-baseline-alignment/ + https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align + https://tympanus.net/codrops/css_reference/vertical-align/ + + https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty + 11.10.2.6. The ‘alignment-baseline’ property + + This property is defined in the CSS Line Layout Module 3 specification. See 'alignment-baseline'. [css-inline-3] + https://drafts.csswg.org/css-inline/#propdef-alignment-baseline + + The vertical-align property shorthand should be preferred in new content. + + SVG 2 introduces some changes to the definition of this property. + In particular: the values 'auto', 'before-edge', and 'after-edge' have been removed. + For backwards compatibility, 'text-before-edge' should be mapped to 'text-top' and + 'text-after-edge' should be mapped to 'text-bottom'. + + Neither 'text-before-edge' nor 'text-after-edge' should be used with the vertical-align property. + */ + /* + CGRect fontBounds = CTFontGetBoundingBox(fontRef); + double textHeight = CGRectGetHeight(textBounds); + double fontWidth = CGRectGetWidth(textBounds); + CGPoint fontOrigin = fontBounds.origin; + + CGFloat fontMinX = fontOrigin.x; + CGFloat fontMinY = fontOrigin.y; + CGFloat fontMaxX = fontMinX + fontWidth; + CGFloat fontMaxY = fontMinY + textHeight; + */ + // TODO + double descenderDepth = CTFontGetDescent(fontRef); + double bottom = descenderDepth + CTFontGetLeading(fontRef); + double ascenderHeight = CTFontGetAscent(fontRef); + double top = ascenderHeight; + double totalHeight = top + bottom; + double baselineShift = 0; + NSString *baselineShiftString = [self getBaselineShift]; + enum AlignmentBaseline baseline = AlignmentBaselineFromString([self getAlignmentBaseline]); + if (baseline != AlignmentBaselineBaseline) { + // TODO alignment-baseline, test / verify behavior + // TODO get per glyph baselines from font baseline table, for high-precision alignment + CGFloat xHeight = CTFontGetXHeight(fontRef); + switch (baseline) { + // https://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling + default: + case AlignmentBaselineBaseline: + // Use the dominant baseline choice of the parent. + // Match the box’s corresponding baseline to that of its parent. + baselineShift = 0; + break; + + case AlignmentBaselineTextBottom: + case AlignmentBaselineAfterEdge: + case AlignmentBaselineTextAfterEdge: + // Match the bottom of the box to the bottom of the parent’s content area. + // text-after-edge = text-bottom + // text-after-edge = descender depth + baselineShift = -descenderDepth; + break; + + case AlignmentBaselineAlphabetic: + // Match the box’s alphabetic baseline to that of its parent. + // alphabetic = 0 + baselineShift = 0; + break; + + case AlignmentBaselineIdeographic: + // Match the box’s ideographic character face under-side baseline to that of its parent. + // ideographic = descender depth + baselineShift = -descenderDepth; + break; + + case AlignmentBaselineMiddle: + // Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. TODO + // middle = x height / 2 + baselineShift = xHeight / 2; + break; + + case AlignmentBaselineCentral: + // Match the box’s central baseline to the central baseline of its parent. + // central = (ascender height - descender depth) / 2 + baselineShift = (ascenderHeight - descenderDepth) / 2; + break; + + case AlignmentBaselineMathematical: + // Match the box’s mathematical baseline to that of its parent. + // Hanging and mathematical baselines + // There are no obvious formulas to calculate the position of these baselines. + // At the time of writing FOP puts the hanging baseline at 80% of the ascender + // height and the mathematical baseline at 50%. + baselineShift = 0.5 * ascenderHeight; + break; + + case AlignmentBaselineHanging: + baselineShift = 0.8 * ascenderHeight; + break; + + case AlignmentBaselineTextTop: + case AlignmentBaselineBeforeEdge: + case AlignmentBaselineTextBeforeEdge: + // Match the top of the box to the top of the parent’s content area. + // text-before-edge = text-top + // text-before-edge = ascender height + baselineShift = ascenderHeight; + break; + + case AlignmentBaselineBottom: + // Align the top of the aligned subtree with the top of the line box. + baselineShift = bottom; + break; + + case AlignmentBaselineCenter: + // Align the center of the aligned subtree with the center of the line box. + baselineShift = totalHeight / 2; + break; + + case AlignmentBaselineTop: + // Align the bottom of the aligned subtree with the bottom of the line box. + baselineShift = top; + break; + } + } + /* + 2.2.2. Alignment Shift: baseline-shift longhand + + This property specifies by how much the box is shifted up from its alignment point. + It does not apply when alignment-baseline is top or bottom. + + Authors should use the vertical-align shorthand instead of this property. + + Values have the following meanings: + + + Raise (positive value) or lower (negative value) by the specified length. + + Raise (positive value) or lower (negative value) by the specified percentage of the line-height. + TODO sub + Lower by the offset appropriate for subscripts of the parent’s box. + (The UA should use the parent’s font data to find this offset whenever possible.) + TODO super + Raise by the offset appropriate for superscripts of the parent’s box. + (The UA should use the parent’s font data to find this offset whenever possible.) + + User agents may additionally support the keyword baseline as computing to 0 + if is necessary for them to support legacy SVG content. + Issue: We would prefer to remove this, + and are looking for feedback from SVG user agents as to whether it’s necessary. + + https://www.w3.org/TR/css-inline-3/#propdef-baseline-shift + */ + if (baselineShiftString != nil && ![baselineShiftString isEqualToString:@""]) { + switch (baseline) { + case AlignmentBaselineTop: + case AlignmentBaselineBottom: + break; + + default: + if (fontData != nil && [baselineShiftString isEqualToString:@"sub"]) { + // TODO + NSDictionary* tables = [fontData objectForKey:@"tables"]; + NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; + NSDictionary* os2 = [tables objectForKey:@"os2"]; + NSNumber* ySubscriptYOffset = [os2 objectForKey:@"ySubscriptYOffset"]; + if (ySubscriptYOffset) { + double subOffset = [ySubscriptYOffset doubleValue]; + baselineShift += fontSize * subOffset / [unitsPerEm doubleValue]; + } + } else if (fontData != nil && [baselineShiftString isEqualToString:@"super"]) { + // TODO + NSDictionary* tables = [fontData objectForKey:@"tables"]; + NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; + NSDictionary* os2 = [tables objectForKey:@"os2"]; + NSNumber* ySuperscriptYOffset = [os2 objectForKey:@"ySuperscriptYOffset"]; + if (ySuperscriptYOffset) { + double superOffset = [ySuperscriptYOffset doubleValue]; + baselineShift -= fontSize * superOffset / [unitsPerEm doubleValue]; + } + } else if ([baselineShiftString isEqualToString:@"baseline"]) { + } else { + baselineShift -= [PropHelper fromRelativeWithNSString:baselineShiftString + relative:fontSize + offset:0 + scale:1 + fontSize:fontSize]; + + } + break; + } + } + + CFArrayRef runs = CTLineGetGlyphRuns(line); CFIndex runEnd = CFArrayGetCount(runs); for (CFIndex r = 0; r < runEnd; r++) { CTRunRef run = CFArrayGetValueAtIndex(runs, r); - CFIndex runGlyphCount = CTRunGetGlyphCount(run); + CFIndex indices[runGlyphCount]; CGSize advances[runGlyphCount]; CGGlyph glyphs[runGlyphCount]; // Grab the glyphs and font CTRunGetGlyphs(run, CFRangeMake(0, 0), glyphs); + CTRunGetStringIndices(run, CFRangeMake(0, 0), indices); CTFontRef runFont = CFDictionaryGetValue(CTRunGetAttributes(run), kCTFontAttributeName); CTFontGetAdvancesForGlyphs(runFont, kCTFontOrientationHorizontal, glyphs, advances, runGlyphCount); - /* - Determine the startpoint-on-the-path for the first glyph using attribute ‘startOffset’ - and property text-anchor. - - For text-anchor:start, startpoint-on-the-path is the point - on the path which represents the point on the path which is ‘startOffset’ distance - along the path from the start of the path, calculated using the user agent's distance - along the path algorithm. - - For text-anchor:middle, startpoint-on-the-path is the point - on the path which represents the point on the path which is [ ‘startOffset’ minus half - of the total advance values for all of the glyphs in the ‘textPath’ element ] distance - along the path from the start of the path, calculated using the user agent's distance - along the path algorithm. - - For text-anchor:end, startpoint-on-the-path is the point on - the path which represents the point on the path which is [ ‘startOffset’ minus the - total advance values for all of the glyphs in the ‘textPath’ element ]. - - Before rendering the first glyph, the horizontal component of the startpoint-on-the-path - is adjusted to take into account various horizontal alignment text properties and - attributes, such as a ‘dx’ attribute value on a ‘tspan’ element. - */ - enum TextAnchor textAnchor = font->textAnchor; - CGRect textBounds = CTLineGetBoundsWithOptions(line, 0); - double textMeasure = CGRectGetWidth(textBounds); - double offset = getTextAnchorOffset(textAnchor, textMeasure); - - bool hasTextPath = textPath != nil; - - int side = 1; - double startOfRendering = 0; - double endOfRendering = _pathLength; - double fontSize = [gc getFontSize]; - bool sharpMidLine = false; - if (hasTextPath) { - sharpMidLine = TextPathMidLineFromString([textPath midLine]) == TextPathMidLineSharp; - /* - Name - side - Value - left | right - initial value - left - Animatable - yes - - Determines the side of the path the text is placed on - (relative to the path direction). - - Specifying a value of right effectively reverses the path. - - Added in SVG 2 to allow text either inside or outside closed subpaths - and basic shapes (e.g. rectangles, circles, and ellipses). - - Adding 'side' was resolved at the Sydney (2015) meeting. - */ - side = TextPathSideFromString([textPath side]) == TextPathSideRight ? -1 : 1; - /* - Name - startOffset - Value - | | - initial value - 0 - Animatable - yes - - An offset from the start of the path for the initial current text position, - calculated using the user agent's distance along the path algorithm, - after converting the path to the ‘textPath’ element's coordinate system. - - If a other than a percentage is given, then the ‘startOffset’ - represents a distance along the path measured in the current user coordinate - system for the ‘textPath’ element. - - If a percentage is given, then the ‘startOffset’ represents a percentage - distance along the entire path. Thus, startOffset="0%" indicates the start - point of the path and startOffset="100%" indicates the end point of the path. - - Negative values and values larger than the path length (e.g. 150%) are allowed. - - Any typographic characters with mid-points that are not on the path are not rendered - - For paths consisting of a single closed subpath (including an equivalent path for a - basic shape), typographic characters are rendered along one complete circuit of the - path. The text is aligned as determined by the text-anchor property to a position - along the path set by the ‘startOffset’ attribute. - - For the start (end) value, the text is rendered from the start (end) of the line - until the initial position along the path is reached again. - - For the middle, the text is rendered from the middle point in both directions until - a point on the path equal distance in both directions from the initial position on - the path is reached. - */ - double absoluteStartOffset = [PropHelper fromRelativeWithNSString:textPath.startOffset - relative:_pathLength - offset:0 - scale:1 - fontSize:fontSize]; - offset += absoluteStartOffset; - if (isClosed) { - double halfPathDistance = _pathLength / 2; - startOfRendering = absoluteStartOffset + (textAnchor == TextAnchorMiddle ? -halfPathDistance : 0); - endOfRendering = startOfRendering + _pathLength; - } - /* - TextPathSpacing spacing = textPath.getSpacing(); - if (spacing == TextPathSpacing.auto) { - // Hmm, what to do here? - // https://svgwg.org/svg2-draft/text.html#TextPathElementSpacingAttribute - } - */ - } - - /* - Name - method - Value - align | stretch - initial value - align - Animatable - yes - Indicates the method by which text should be rendered along the path. - - A value of align indicates that the typographic character should be rendered using - simple 2×3 matrix transformations such that there is no stretching/warping of the - typographic characters. Typically, supplemental rotation, scaling and translation - transformations are done for each typographic characters to be rendered. - - As a result, with align, in fonts where the typographic characters are designed to be - connected (e.g., cursive fonts), the connections may not align properly when text is - rendered along a path. - - A value of stretch indicates that the typographic character outlines will be converted - into paths, and then all end points and control points will be adjusted to be along the - perpendicular vectors from the path, thereby stretching and possibly warping the glyphs. - - With this approach, connected typographic characters, such as in cursive scripts, - will maintain their connections. (Non-vertical straight path segments should be - converted to Bézier curves in such a way that horizontal straight paths have an - (approximately) constant offset from the path along which the typographic characters - are rendered.) - - TODO implement stretch - */ - - /* - Name Value Initial value Animatable - textLength | | See below yes - - The author's computation of the total sum of all of the advance values that correspond - to character data within this element, including the advance value on the glyph - (horizontal or vertical), the effect of properties letter-spacing and word-spacing and - adjustments due to attributes ‘dx’ and ‘dy’ on this ‘text’ or ‘tspan’ element or any - descendants. This value is used to calibrate the user agent's own calculations with - that of the author. - - The purpose of this attribute is to allow the author to achieve exact alignment, - in visual rendering order after any bidirectional reordering, for the first and - last rendered glyphs that correspond to this element; thus, for the last rendered - character (in visual rendering order after any bidirectional reordering), - any supplemental inter-character spacing beyond normal glyph advances are ignored - (in most cases) when the user agent determines the appropriate amount to expand/compress - the text string to fit within a length of ‘textLength’. - - If attribute ‘textLength’ is specified on a given element and also specified on an - ancestor, the adjustments on all character data within this element are controlled by - the value of ‘textLength’ on this element exclusively, with the possible side-effect - that the adjustment ratio for the contents of this element might be different than the - adjustment ratio used for other content that shares the same ancestor. The user agent - must assume that the total advance values for the other content within that ancestor is - the difference between the advance value on that ancestor and the advance value for - this element. - - This attribute is not intended for use to obtain effects such as shrinking or - expanding text. - - A negative value is an error (see Error processing). - - The ‘textLength’ attribute is only applied when the wrapping area is not defined by the - TODO shape-inside or the inline-size properties. It is also not applied for any ‘text’ or - TODO ‘tspan’ element that has forced line breaks (due to a white-space value of pre or - pre-line). - - If the attribute is not specified anywhere within a ‘text’ element, the effect is as if - the author's computation exactly matched the value calculated by the user agent; - thus, no advance adjustments are made. - */ - double scaleSpacingAndGlyphs = 1; - NSString *mTextLength = [self textLength]; - enum TextLengthAdjust mLengthAdjust = TextLengthAdjustFromString([self lengthAdjust]); - if (mTextLength != nil) { - double author = [PropHelper fromRelativeWithNSString:mTextLength - relative:[gc getWidth] - offset:0 - scale:1 - fontSize:fontSize]; - if (author < 0) { - NSException *e = [NSException - exceptionWithName:@"NegativeTextLength" - reason:@"Negative textLength value" - userInfo:nil]; - @throw e; - } - switch (mLengthAdjust) { - default: - case TextLengthAdjustSpacing: - letterSpacing += (author - textMeasure) / (n - 1); - break; - case TextLengthAdjustSpacingAndGlyphs: - scaleSpacingAndGlyphs = author / textMeasure; - break; - } - } - double scaledDirection = scaleSpacingAndGlyphs * side; - - /* - https://developer.mozilla.org/en/docs/Web/CSS/vertical-align - https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6bsln.html - https://www.microsoft.com/typography/otspec/base.htm - http://apike.ca/prog_svg_text_style.html - https://www.w3schools.com/tags/canvas_textbaseline.asp - http://vanseodesign.com/web-design/svg-text-baseline-alignment/ - https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align - https://tympanus.net/codrops/css_reference/vertical-align/ - - https://svgwg.org/svg2-draft/text.html#AlignmentBaselineProperty - 11.10.2.6. The ‘alignment-baseline’ property - - This property is defined in the CSS Line Layout Module 3 specification. See 'alignment-baseline'. [css-inline-3] - https://drafts.csswg.org/css-inline/#propdef-alignment-baseline - - The vertical-align property shorthand should be preferred in new content. - - SVG 2 introduces some changes to the definition of this property. - In particular: the values 'auto', 'before-edge', and 'after-edge' have been removed. - For backwards compatibility, 'text-before-edge' should be mapped to 'text-top' and - 'text-after-edge' should be mapped to 'text-bottom'. - - Neither 'text-before-edge' nor 'text-after-edge' should be used with the vertical-align property. - */ - /* - CGRect fontBounds = CTFontGetBoundingBox(fontRef); - double textHeight = CGRectGetHeight(textBounds); - double fontWidth = CGRectGetWidth(textBounds); - CGPoint fontOrigin = fontBounds.origin; - - CGFloat fontMinX = fontOrigin.x; - CGFloat fontMinY = fontOrigin.y; - CGFloat fontMaxX = fontMinX + fontWidth; - CGFloat fontMaxY = fontMinY + textHeight; - */ - // TODO - double descenderDepth = CTFontGetDescent(fontRef); - double bottom = descenderDepth + CTFontGetLeading(fontRef); - double ascenderHeight = CTFontGetAscent(fontRef); - double top = ascenderHeight; - double totalHeight = top + bottom; - double baselineShift = 0; - // TODO, alignmentBaseline and baselineShift are always nil for some reason? - NSString *baselineShiftString = [self getBaselineShift]; - enum AlignmentBaseline baseline = AlignmentBaselineFromString([self getAlignmentBaseline]); - if (baseline != AlignmentBaselineBaseline) { - // TODO alignment-baseline, test / verify behavior - // TODO get per glyph baselines from font baseline table, for high-precision alignment - CGFloat xHeight = CTFontGetXHeight(fontRef); - switch (baseline) { - // https://wiki.apache.org/xmlgraphics-fop/LineLayout/AlignmentHandling - default: - case AlignmentBaselineBaseline: - // Use the dominant baseline choice of the parent. - // Match the box’s corresponding baseline to that of its parent. - baselineShift = 0; - break; - - case AlignmentBaselineTextBottom: - case AlignmentBaselineAfterEdge: - case AlignmentBaselineTextAfterEdge: - // Match the bottom of the box to the bottom of the parent’s content area. - // text-after-edge = text-bottom - // text-after-edge = descender depth - baselineShift = -descenderDepth; - break; - - case AlignmentBaselineAlphabetic: - // Match the box’s alphabetic baseline to that of its parent. - // alphabetic = 0 - baselineShift = 0; - break; - - case AlignmentBaselineIdeographic: - // Match the box’s ideographic character face under-side baseline to that of its parent. - // ideographic = descender depth - baselineShift = -descenderDepth; - break; - - case AlignmentBaselineMiddle: - // Align the vertical midpoint of the box with the baseline of the parent box plus half the x-height of the parent. TODO - // middle = x height / 2 - baselineShift = xHeight / 2; - break; - - case AlignmentBaselineCentral: - // Match the box’s central baseline to the central baseline of its parent. - // central = (ascender height - descender depth) / 2 - baselineShift = (ascenderHeight - descenderDepth) / 2; - break; - - case AlignmentBaselineMathematical: - // Match the box’s mathematical baseline to that of its parent. - // Hanging and mathematical baselines - // There are no obvious formulas to calculate the position of these baselines. - // At the time of writing FOP puts the hanging baseline at 80% of the ascender - // height and the mathematical baseline at 50%. - baselineShift = 0.5 * ascenderHeight; - break; - - case AlignmentBaselineHanging: - baselineShift = 0.8 * ascenderHeight; - break; - - case AlignmentBaselineTextTop: - case AlignmentBaselineBeforeEdge: - case AlignmentBaselineTextBeforeEdge: - // Match the top of the box to the top of the parent’s content area. - // text-before-edge = text-top - // text-before-edge = ascender height - baselineShift = ascenderHeight; - break; - - case AlignmentBaselineBottom: - // Align the top of the aligned subtree with the top of the line box. - baselineShift = bottom; - break; - - case AlignmentBaselineCenter: - // Align the center of the aligned subtree with the center of the line box. - baselineShift = totalHeight / 2; - break; - - case AlignmentBaselineTop: - // Align the bottom of the aligned subtree with the bottom of the line box. - baselineShift = top; - break; - } - } - /* - 2.2.2. Alignment Shift: baseline-shift longhand - - This property specifies by how much the box is shifted up from its alignment point. - It does not apply when alignment-baseline is top or bottom. - - Authors should use the vertical-align shorthand instead of this property. - - Values have the following meanings: - - - Raise (positive value) or lower (negative value) by the specified length. - - Raise (positive value) or lower (negative value) by the specified percentage of the line-height. - TODO sub - Lower by the offset appropriate for subscripts of the parent’s box. - (The UA should use the parent’s font data to find this offset whenever possible.) - TODO super - Raise by the offset appropriate for superscripts of the parent’s box. - (The UA should use the parent’s font data to find this offset whenever possible.) - - User agents may additionally support the keyword baseline as computing to 0 - if is necessary for them to support legacy SVG content. - Issue: We would prefer to remove this, - and are looking for feedback from SVG user agents as to whether it’s necessary. - - https://www.w3.org/TR/css-inline-3/#propdef-baseline-shift - */ - if (baselineShiftString != nil && ![baselineShiftString isEqualToString:@""]) { - switch (baseline) { - case AlignmentBaselineTop: - case AlignmentBaselineBottom: - break; - - default: - if (fontData != nil && [baselineShiftString isEqualToString:@"sub"]) { - // TODO - NSDictionary* tables = [fontData objectForKey:@"tables"]; - NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; - NSDictionary* os2 = [tables objectForKey:@"os2"]; - NSNumber* ySubscriptYOffset = [os2 objectForKey:@"ySubscriptYOffset"]; - if (ySubscriptYOffset) { - double subOffset = [ySubscriptYOffset doubleValue]; - baselineShift += fontSize * subOffset / [unitsPerEm doubleValue]; - } - } else if (fontData != nil && [baselineShiftString isEqualToString:@"super"]) { - // TODO - NSDictionary* tables = [fontData objectForKey:@"tables"]; - NSNumber* unitsPerEm = [fontData objectForKey:@"unitsPerEm"]; - NSDictionary* os2 = [tables objectForKey:@"os2"]; - NSNumber* ySuperscriptYOffset = [os2 objectForKey:@"ySuperscriptYOffset"]; - if (ySuperscriptYOffset) { - double superOffset = [ySuperscriptYOffset doubleValue]; - baselineShift -= fontSize * superOffset / [unitsPerEm doubleValue]; - } - } else if ([baselineShiftString isEqualToString:@"baseline"]) { - } else { - baselineShift -= [PropHelper fromRelativeWithNSString:baselineShiftString - relative:fontSize - offset:0 - scale:1 - fontSize:fontSize]; - - } - break; - } - } - CFDictionaryRef ligattributes; - NSNumber *lig = [NSNumber numberWithInt:allowOptionalLigatures ? 2 : 1]; - - if (fontRef != nil) { - ligattributes = (__bridge CFDictionaryRef)@{ - (NSString *)kCTFontAttributeName: (__bridge id)fontRef, - (NSString *)NSLigatureAttributeName: lig - }; - } else { - ligattributes = (__bridge CFDictionaryRef)@{ - (NSString *)NSLigatureAttributeName: lig - }; - } for(CFIndex g = 0; g < runGlyphCount; g++) { CGGlyph glyph = glyphs[g]; - bool hasLigature = false; - CGFloat charWidth = 0; - double advance = 0; - bool alreadyRenderedGraphemeCluster = ligature[g]; - if (!alreadyRenderedGraphemeCluster) { - /* - Determine the glyph's charwidth (i.e., the amount which the current text position - advances horizontally when the glyph is drawn using horizontal text layout). - */ - double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, glyphs + g, NULL, 1); - charWidth = unkernedAdvance * scaleSpacingAndGlyphs; + /* + Determine the glyph's charwidth (i.e., the amount which the current text position + advances horizontally when the glyph is drawn using horizontal text layout). + */ + double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); + CGFloat charWidth = unkernedAdvance * scaleSpacingAndGlyphs; - /* - For each subsequent glyph, set a new startpoint-on-the-path as the previous - endpoint-on-the-path, but with appropriate adjustments taking into account - horizontal kerning tables in the font and current values of various attributes - and properties, including spacing properties (e.g. letter-spacing and word-spacing) - and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All - adjustments are calculated as distance adjustments along the path, calculated - using the user agent's distance along the path algorithm. - */ - if (autoKerning) { - double kerned = advances[g].width * scaleSpacingAndGlyphs; - kerning = kerned - charWidth; - } - - NSUInteger len = 2; - NSUInteger nextIndex = g; - while (++nextIndex < n) { - NSString* nextLigature = [str substringWithRange:NSMakeRange(g, len++)]; - bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); - if (hasNextLigature) { - ligature[nextIndex] = true; - hasLigature = true; - } else { - break; - } - } - if (hasLigature) { - charWidth = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); - } - - char currentChar = [str characterAtIndex:g]; - bool isWordSeparator = [separators characterIsMember:currentChar]; - double wordSpace = isWordSeparator ? wordSpacing : 0; - double spacing = wordSpace + letterSpacing; - advance = charWidth + spacing; + /* + For each subsequent glyph, set a new startpoint-on-the-path as the previous + endpoint-on-the-path, but with appropriate adjustments taking into account + horizontal kerning tables in the font and current values of various attributes + and properties, including spacing properties (e.g. letter-spacing and word-spacing) + and ‘tspan’ elements with values provided for attributes ‘dx’ and ‘dy’. All + adjustments are calculated as distance adjustments along the path, calculated + using the user agent's distance along the path algorithm. + */ + if (autoKerning) { + double kerned = advances[g].width * scaleSpacingAndGlyphs; + kerning = kerned - charWidth; } - double x = [gc nextXWithDouble:(alreadyRenderedGraphemeCluster ? 0 : kerning + advance)]; + CFIndex currIndex = indices[g]; + char currentChar = [str characterAtIndex:currIndex]; + bool isWordSeparator = [separators characterIsMember:currentChar]; + double wordSpace = isWordSeparator ? wordSpacing : 0; + double spacing = wordSpace + letterSpacing; + double advance = charWidth + spacing; + + double x = [gc nextXWithDouble:kerning + advance]; double y = [gc nextY]; double dx = [gc nextDeltaX]; double dy = [gc nextDeltaY]; double r = [[gc nextRotation] doubleValue] / radToDeg; - if (alreadyRenderedGraphemeCluster) { + CFIndex endIndex = g + 1 == runGlyphCount ? currIndex : indices[g + 1]; + while (++currIndex < endIndex) { // Skip rendering other grapheme clusters of ligatures (already rendered), - // But, make sure to increment index positions by making gc.next() calls. - continue; + // And, make sure to increment index positions by making gc.next() calls. + [gc nextXWithDouble:0]; + [gc nextY]; + [gc nextDeltaX]; + [gc nextDeltaY]; + [gc nextRotation]; } CGPathRef glyphPath = CTFontCreatePathForGlyph(runFont, glyph, nil); @@ -890,31 +856,4 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) } } -bool hasGlyph(CTFontRef fontRef, NSString * str, CGGlyph* glyph, CFDictionaryRef attributes) -{ - CFStringRef string = (__bridge CFStringRef)str; - CFAttributedStringRef attrString = CFAttributedStringCreate(kCFAllocatorDefault, string, attributes); - CTLineRef line = CTLineCreateWithAttributedString(attrString); - CFArrayRef runs = CTLineGetGlyphRuns(line); - - CFIndex runEnd = CFArrayGetCount(runs); - if (runEnd > 1) { - CFRelease(attrString); - CFRelease(line); - return false; - } - CTRunRef run = CFArrayGetValueAtIndex(runs, 0); - CFIndex runGlyphCount = CTRunGetGlyphCount(run); - bool hasGlyph = runGlyphCount == 1; - - if (hasGlyph) { - CTRunGetGlyphs(run, CFRangeMake(0, 1), glyph); - } - - CFRelease(attrString); - CFRelease(line); - - return hasGlyph; -} - @end From 83d00d09dbd53c3dd001b753687dc7566277decb Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Tue, 2 Jan 2018 23:39:58 +0200 Subject: [PATCH 10/14] Optimize and improve accuracy of textPath. --- ios/Text/RNSVGTextPath.m | 77 ++++++---------------------------------- 1 file changed, 11 insertions(+), 66 deletions(-) diff --git a/ios/Text/RNSVGTextPath.m b/ios/Text/RNSVGTextPath.m index ab9b5f67..81fd5df5 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -10,7 +10,7 @@ #import "RNSVGTextPath.h" #import "BezierElement.h" -/* Bezier logic from PerformanceBezier */ +/* Some Bezier logic from PerformanceBezier */ /* ## License @@ -34,67 +34,7 @@ CGFloat distance(CGPoint p1, CGPoint p2) CGFloat dx = p2.x - p1.x; CGFloat dy = p2.y - p1.y; - return sqrt(dx*dx + dy*dy); -} - -/** - * returns the dot product of two coordinates - */ -CGFloat dotProduct(const CGPoint p1, const CGPoint p2) { - return p1.x * p2.x + p1.y * p2.y; -} - -/** - * returns the shortest distance from a point to a line - */ -CGFloat distanceOfPointToLine(CGPoint point, CGPoint start, CGPoint end){ - CGPoint v = CGPointMake(end.x - start.x, end.y - start.y); - CGPoint w = CGPointMake(point.x - start.x, point.y - start.y); - CGFloat c1 = dotProduct(w, v); - CGFloat c2 = dotProduct(v, v); - CGFloat d; - if (c1 <= 0) { - d = distance(point, start); - } - else if (c2 <= c1) { - d = distance(point, end); - } - else { - CGFloat b = c1 / c2; - CGPoint Pb = CGPointMake(start.x + b * v.x, start.y + b * v.y); - d = distance(point, Pb); - } - return d; -} - -/** - * calculate the point on a bezier at time t - * where 0 < t < 1 - */ -CGPoint bezierPointAtT(const CGPoint bez[4], CGFloat t) -{ - CGPoint q; - CGFloat mt = 1 - t; - - CGPoint bez1[4]; - CGPoint bez2[4]; - - q.x = mt * bez[1].x + t * bez[2].x; - q.y = mt * bez[1].y + t * bez[2].y; - bez1[1].x = mt * bez[0].x + t * bez[1].x; - bez1[1].y = mt * bez[0].y + t * bez[1].y; - bez2[2].x = mt * bez[2].x + t * bez[3].x; - bez2[2].y = mt * bez[2].y + t * bez[3].y; - - bez1[2].x = mt * bez1[1].x + t * q.x; - bez1[2].y = mt * bez1[1].y + t * q.y; - bez2[1].x = mt * q.x + t * bez2[2].x; - bez2[1].y = mt * q.y + t * bez2[2].y; - - bez1[3].x = bez2[0].x = mt * bez1[2].x + t * bez2[1].x; - bez1[3].y = bez2[0].y = mt * bez1[2].y + t * bez2[1].y; - - return CGPointMake(bez1[3].x, bez1[3].y); + return hypot(dx, dy); } // Subdivide a Bézier (specific division) @@ -221,10 +161,16 @@ void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat [curves removeLastObject]; // calculate the error rate of the curve vs - // a line segement between the start and end points - CGPoint onCurve = bezierPointAtT(bez, .5); + // a line segment between the start and end points + CGPoint ctrl1 = bez[1]; + CGPoint ctrl2 = bez[2]; CGPoint next = bez[3]; - CGFloat error = distanceOfPointToLine(onCurve, last, next); + CGFloat polyLen = + distance(last, ctrl1) + + distance(ctrl1, ctrl2) + + distance(ctrl2, next); + CGFloat chordLen = distance(last, next); + CGFloat error = polyLen - chordLen; // if the error is less than our accepted level of error // then add a line, else, split the curve in half @@ -239,7 +185,6 @@ void addLine(CGPoint *last, const CGPoint *next, NSMutableArray *lines, CGFloat curveIndex += 2; } } - last = curveTo; break; } From 310fbed841d9ef5cb4d47dec7ecac4b8c89fef58 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Wed, 3 Jan 2018 03:38:37 +0200 Subject: [PATCH 11/14] Fix image rendering --- ios/Elements/RNSVGImage.m | 55 ++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/ios/Elements/RNSVGImage.m b/ios/Elements/RNSVGImage.m index fed48df2..64c313f4 100644 --- a/ios/Elements/RNSVGImage.m +++ b/ios/Elements/RNSVGImage.m @@ -96,55 +96,46 @@ - (void)renderLayerTo:(CGContextRef)context { - CGRect rect = [self getRect:context]; - // add hit area - CGPathRef hitArea = CGPathCreateWithRect(rect, nil); - [self setHitArea:hitArea]; - CGPathRelease(hitArea); - CGContextSaveGState(context); - CGContextTranslateCTM(context, 0, rect.size.height + 2 * rect.origin.y); - CGContextScaleCTM(context, 1, -1); + + // add hit area + CGRect hitArea = [self getHitArea]; + CGPathRef hitAreaPath = CGPathCreateWithRect(hitArea, nil); + [self setHitArea:hitAreaPath]; + CGPathRelease(hitAreaPath); // apply viewBox transform on Image render. - CGRect renderRect = CGRectMake(0, 0, _imageSize.width, _imageSize.height); - - CGFloat rectWidth = CGRectGetWidth(rect); - CGFloat rectHeight = CGRectGetHeight(rect); - CGFloat rectX = CGRectGetMinX(rect); - CGFloat rectY = CGRectGetMinY(rect); - CGFloat canvasLeft = [self getContextLeft]; - CGFloat canvasTop = [self getContextTop]; - - CGRect eRect = CGRectMake(canvasLeft, canvasTop, rectWidth, rectHeight); - CGRect vbRect = CGRectMake(0, 0, CGRectGetWidth(renderRect), CGRectGetHeight(renderRect)); - CGAffineTransform transform = [RNSVGViewBox getTransform:vbRect eRect:eRect align:self.align meetOrSlice:self.meetOrSlice]; - renderRect = CGRectApplyAffineTransform(renderRect, transform); - - CGFloat dx = rectX + canvasLeft; - CGFloat dy = rectY + canvasTop; - renderRect = CGRectApplyAffineTransform(renderRect, CGAffineTransformMakeTranslation(dx, dy)); - + CGRect imageBounds = CGRectMake(0, 0, _imageSize.width, _imageSize.height); + CGAffineTransform viewbox = [RNSVGViewBox getTransform:imageBounds eRect:hitArea align:self.align meetOrSlice:self.meetOrSlice]; + + CGContextTranslateCTM(context, 0, hitArea.size.height); + CGContextScaleCTM(context, 1, -1); [self clip:context]; - CGContextClipToRect(context, rect); - - CGContextDrawImage(context, renderRect, _image); + CGContextClipToRect(context, hitArea); + CGContextConcatCTM(context, viewbox); + CGContextDrawImage(context, imageBounds, _image); CGContextRestoreGState(context); - } -- (CGRect)getRect:(CGContextRef)context +- (CGRect)getHitArea { CGFloat x = [self relativeOnWidth:self.x]; CGFloat y = [self relativeOnHeight:self.y]; CGFloat width = [self relativeOnWidth:self.width]; CGFloat height = [self relativeOnHeight:self.height]; + if (width == 0) { + width = _imageSize.width; + } + if (height == 0) { + height = _imageSize.height; + } + return CGRectMake(x, y, width, height); } - (CGPathRef)getPath:(CGContextRef)context { - return (CGPathRef)CFAutorelease(CGPathCreateWithRect([self getRect:context], nil)); + return (CGPathRef)CFAutorelease(CGPathCreateWithRect([self getHitArea], nil)); } @end From b9d37ac8971c60c5bbf9676bc9db9d40df33a272 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Wed, 3 Jan 2018 23:05:05 +0200 Subject: [PATCH 12/14] Fix parsing of floatingPointConstants in transformParser Relax transforms commaWsp requirement from one or more to zero or more. --- lib/extract/extractTransform.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index 44146ab2..92df6490 100644 --- a/lib/extract/extractTransform.js +++ b/lib/extract/extractTransform.js @@ -44,7 +44,7 @@ transformList = wsp* ts:transforms? wsp* { return ts; } transforms - = t:transform commaWsp+ ts:transforms + = t:transform commaWsp* ts:transforms { return multiply_matrices(t, ts); } @@ -147,15 +147,15 @@ integerConstant = ds:digitSequence { return ds.join(""); } floatingPointConstant - = fractionalConstant exponent? - / digitSequence exponent + = f:(fractionalConstant exponent?) { return f.join(""); } + / d:(digitSequence exponent) { return d.join(""); } - fractionalConstant "fractionalConstant" +fractionalConstant "fractionalConstant" = d1:digitSequence? "." d2:digitSequence { return [d1 ? d1.join("") : null, ".", d2.join("")].join(""); } / d:digitSequence "." { return d.join(""); } exponent - = [eE] sign? digitSequence + = e:([eE] sign? digitSequence) { return [e[0], e[1], e[2].join("")].join(""); } sign = [+-] From 84beda169c60f2305d5c8621309c55c067ec8eba Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 5 Jan 2018 01:48:19 +0200 Subject: [PATCH 13/14] Fix image rendering and ligature advance on android --- .../java/com/horcrux/svg/ImageShadowNode.java | 42 +++++++------------ .../java/com/horcrux/svg/TSpanShadowNode.java | 2 +- 2 files changed, 15 insertions(+), 29 deletions(-) diff --git a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java index 8aa92e53..d27bd3fa 100644 --- a/android/src/main/java/com/horcrux/svg/ImageShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/ImageShadowNode.java @@ -160,7 +160,7 @@ class ImageShadowNode extends RenderableShadowNode { @Override protected Path getPath(Canvas canvas, Paint paint) { Path path = new Path(); - path.addRect(new RectF(getRect()), Path.Direction.CW); + path.addRect(getRect(), Path.Direction.CW); return path; } @@ -187,51 +187,37 @@ class ImageShadowNode extends RenderableShadowNode { } @Nonnull - private Rect getRect() { + private RectF getRect() { double x = relativeOnWidth(mX); double y = relativeOnHeight(mY); double w = relativeOnWidth(mW); double h = relativeOnHeight(mH); + if (w == 0) { + w = mImageWidth * mScale; + } + if (h == 0) { + h = mImageHeight * mScale; + } - return new Rect((int) x, (int) y, (int) (x + w), (int) (y + h)); + return new RectF((float)x, (float)y, (float)(x + w), (float)(y + h)); } private void doRender(Canvas canvas, Paint paint, Bitmap bitmap, float opacity) { - // apply viewBox transform on Image render. - Rect rect = getRect(); - float rectWidth = (float)rect.width(); - float rectHeight = (float)rect.height(); - float rectX = (float)rect.left; - float rectY = (float)rect.top; - float canvasLeft = getCanvasLeft(); - float canvasTop = getCanvasTop(); - if (mImageWidth == 0 || mImageHeight == 0) { mImageWidth = bitmap.getWidth(); mImageHeight = bitmap.getHeight(); } - RectF renderRect = new RectF(0, 0, mImageWidth, mImageHeight); - + RectF renderRect = getRect(); RectF vbRect = new RectF(0, 0, mImageWidth, mImageHeight); - RectF eRect = new RectF(canvasLeft, canvasTop, (rectWidth / mScale) + canvasLeft, (rectHeight / mScale) + canvasTop); - Matrix transform = ViewBox.getTransform(vbRect, eRect, mAlign, mMeetOrSlice); + Matrix transform = ViewBox.getTransform(vbRect, renderRect, mAlign, mMeetOrSlice); + transform.mapRect(vbRect); - Matrix translation = new Matrix(); - transform.mapRect(renderRect); if (mMatrix != null) { - translation.postConcat(mMatrix); - //mMatrix.mapRect(renderRect); + mMatrix.mapRect(vbRect); } - float dx = rectX / mScale + canvasLeft; - float dy = rectY / mScale + canvasTop; - translation.postTranslate(dx, dy); - translation.postScale(mScale, mScale); - translation.mapRect(renderRect); - Path clip = new Path(); - Path clipPath = getClipPath(canvas, paint); Path path = getPath(canvas, paint); if (clipPath != null) { @@ -256,7 +242,7 @@ class ImageShadowNode extends RenderableShadowNode { Paint alphaPaint = new Paint(); alphaPaint.setAlpha((int) (opacity * 255)); - canvas.drawBitmap(bitmap, null, renderRect, alphaPaint); + canvas.drawBitmap(bitmap, null, vbRect, alphaPaint); } private void tryRender(ImageRequest request, Canvas canvas, Paint paint, float opacity) { diff --git a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java index 6af11d8e..c83bea81 100644 --- a/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java +++ b/android/src/main/java/com/horcrux/svg/TSpanShadowNode.java @@ -726,7 +726,7 @@ class TSpanShadowNode extends TextShadowNode { double spacing = wordSpace + letterSpacing; double advance = charWidth + spacing; - double x = gc.nextX(kerning + advance); + double x = gc.nextX(alreadyRenderedGraphemeCluster ? 0 : kerning + advance); double y = gc.nextY(); double dx = gc.nextDeltaX(); double dy = gc.nextDeltaY(); From 4e37a48e6edcd531cf88163db4e4d2a733f8940d Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Fri, 5 Jan 2018 12:13:58 +0200 Subject: [PATCH 14/14] Fix Use of const in strict mode --- lib/extract/extractTransform.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index 92df6490..01167332 100644 --- a/lib/extract/extractTransform.js +++ b/lib/extract/extractTransform.js @@ -16,7 +16,7 @@ function transformToMatrix(props, transform) { const transformParser = peg.generate(` { - const deg2rad = Math.PI / 180; + var deg2rad = Math.PI / 180; /* ╔═ ═╗ ╔═ ═╗ ╔═ ═╗ @@ -26,15 +26,15 @@ const transformParser = peg.generate(` ╚═ ═╝ ╚═ ═╝ ╚═ ═╝ */ function multiply_matrices(l, r) { - const [al, cl, el, bl, dl, fl] = l; - const [ar, cr, er, br, dr, fr] = r; + var [al, cl, el, bl, dl, fl] = l; + var [ar, cr, er, br, dr, fr] = r; - const a = al * ar + cl * br; - const c = al * cr + cl * dr; - const e = al * er + cl * fr + el; - const b = bl * ar + dl * br; - const d = bl * cr + dl * dr; - const f = bl * er + dl * fr + fl; + var a = al * ar + cl * br; + var c = al * cr + cl * dr; + var e = al * er + cl * fr + el; + var b = bl * ar + dl * br; + var d = bl * cr + dl * dr; + var f = bl * er + dl * fr + fl; return [a, c, e, b, d, f]; } @@ -94,10 +94,10 @@ scale rotate = "rotate" wsp* "(" wsp* angle:number c:commaWspTwoNumbers? wsp* ")" { - const cos = Math.cos(deg2rad * angle); - const sin = Math.sin(deg2rad * angle); + var cos = Math.cos(deg2rad * angle); + var sin = Math.sin(deg2rad * angle); if (c !== null) { - const [x, y] = c; + var [x, y] = c; return [ cos, -sin, cos * -x + -sin * -y + x, sin, cos, sin * -x + cos * -y + y