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(); 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 diff --git a/ios/Elements/RNSVGPath.h b/ios/Elements/RNSVGPath.h index 92461cd1..2caf060e 100644 --- a/ios/Elements/RNSVGPath.h +++ b/ios/Elements/RNSVGPath.h @@ -14,6 +14,4 @@ @property (nonatomic, strong) RNSVGPathParser *d; -- (NSArray *)getBezierCurves; - @end diff --git a/ios/Elements/RNSVGPath.m b/ios/Elements/RNSVGPath.m index c2ee7815..fc77c23f 100644 --- a/ios/Elements/RNSVGPath.m +++ b/ios/Elements/RNSVGPath.m @@ -30,11 +30,6 @@ return _path; } -- (NSArray *)getBezierCurves -{ - return [_d getBezierCurves]; -} - - (void)dealloc { CGPathRelease(_path); diff --git a/ios/RNSVG.xcodeproj/project.pbxproj b/ios/RNSVG.xcodeproj/project.pbxproj index 3044c51e..6bfe28ac 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,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 */; }; + 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 */; }; @@ -137,37 +136,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; @@ -280,8 +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 = ""; }; - 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 +259,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; }; @@ -326,6 +291,8 @@ 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; }; /* End PBXFileReference section */ @@ -334,14 +301,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 +323,6 @@ 0CF68AB81AF0540F00FF9E5C = { isa = PBXGroup; children = ( - 7F4BB4FF1FB1DEC300663D5F /* QuartzBookPack.xcodeproj */, - 9494C4711F4710FE00D5BCFD /* PerformanceBezier.xcodeproj */, 1039D29A1CE7212C001E90A8 /* Utils */, 1039D2801CE71DCF001E90A8 /* Elements */, 1039D27F1CE71D9B001E90A8 /* Text */, @@ -532,10 +495,11 @@ 1039D29A1CE7212C001E90A8 /* Utils */ = { isa = PBXGroup; children = ( + 94C70B151FF6B1BF004DFD49 /* BezierElement.h */, + 94C70B171FF6B1C0004DFD49 /* BezierElement.m */, 7F69160D1E3703D800DA6EDC /* RNSVGUnits.h */, 10ABC7381D43982B006CCF6E /* RNSVGVBMOS.h */, 10ABC7371D439779006CCF6E /* RNSVGCGFCRule.h */, - 7F08CEA31E23481F00650F83 /* RNSVGTextAnchor.h */, 7FC260CC1E3499BC00A39833 /* RNSVGViewBox.h */, 7FC260CD1E3499BC00A39833 /* RNSVGViewBox.m */, 1039D29E1CE72177001E90A8 /* RNSVGCGFloatArray.h */, @@ -549,15 +513,6 @@ name = Utils; sourceTree = ""; }; - 7F4BB5001FB1DEC300663D5F /* Products */ = { - isa = PBXGroup; - children = ( - 7F4BB5051FB1DEC300663D5F /* QuartzBookPack.framework */, - 7F4BB5071FB1DEC300663D5F /* QuartzBookPackTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; 9494C2B31F46139600D5BCFD /* Frameworks */ = { isa = PBXGroup; children = ( @@ -571,15 +526,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 +586,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 +594,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; @@ -734,6 +639,7 @@ 9494C5251F4B605F00D5BCFD /* GlyphContext.m in Sources */, 10BA0D481CE74E3D00887C2B /* RNSVGCircle.m in Sources */, 9494C5401F4C44DD00D5BCFD /* TextLengthAdjust.m in Sources */, + 94C70B1A1FF6B1C0004DFD49 /* BezierElement.m in Sources */, 10BA0D351CE74E3100887C2B /* RNSVGEllipseManager.m in Sources */, 1039D2A01CE72177001E90A8 /* RCTConvert+RNSVG.m in Sources */, 9494C4FF1F4B5BE800D5BCFD /* FontData.m in Sources */, diff --git a/ios/Text/RNSVGTSpan.h b/ios/Text/RNSVGTSpan.h index 0f15645b..9f909070 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" diff --git a/ios/Text/RNSVGTSpan.m b/ios/Text/RNSVGTSpan.m index d23da576..3499c4e0 100644 --- a/ios/Text/RNSVGTSpan.m +++ b/ios/Text/RNSVGTSpan.m @@ -5,24 +5,24 @@ * 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" #import "FontData.h" NSCharacterSet *separators = nil; +static double radToDeg = 180 / M_PI; @implementation RNSVGTSpan { CGFloat startOffset; - UIBezierPath *_bezierPath; CGPathRef _cache; CGFloat _pathLength; - RNSVGTextPath * textPath; - RNSVGPath * textPathPath; - bool isClosed; + RNSVGTextPath *textPath; + NSArray *lengths; + NSArray *lines; + NSUInteger lineCount; + BOOL isClosed; } - (id)init @@ -94,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 (int i = 0; i < n; i++){ - ligature[i] = NO; - } /* * * Three properties affect the space between characters and words: @@ -251,461 +230,461 @@ 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 = _bezierPath != 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; - } - } - int i = -1; - 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++) { - i++; - bool alreadyRenderedGraphemeCluster = ligature[i]; + CGGlyph glyph = glyphs[g]; /* 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); + double unkernedAdvance = CTFontGetAdvancesForGlyphs(fontRef, kCTFontOrientationHorizontal, &glyph, NULL, 1); CGFloat charWidth = unkernedAdvance * scaleSpacingAndGlyphs; /* @@ -722,37 +701,28 @@ NSCharacterSet *separators = nil; kerning = kerned - charWidth; } - char currentChar = [str characterAtIndex:i]; + 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 + charWidth]; + 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; - } - - int len = 2; - int nextIndex = i; - CGGlyph glyph = glyphs[g]; - bool hasLigature = false; - while (++nextIndex < n) { - NSString* nextLigature = [str substringWithRange:NSMakeRange(i, len++)]; - bool hasNextLigature = hasGlyph(fontRef, nextLigature, &glyph, ligattributes); - if (hasNextLigature) { - ligature[nextIndex] = true; - hasLigature = true; - } else { - break; - } + // 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); @@ -786,17 +756,44 @@ NSCharacterSet *separators = nil; continue; } - CGPoint slope; - CGFloat percentConsumed = midPoint / _pathLength; - CGPoint mid = [_bezierPath pointAtPercent:percentConsumed withSlope:&slope]; - // Calculate the rotation - double angle = atan2(slope.y, slope.x); + // 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]; + }]; - transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(mid.x, mid.y), transform); + CGFloat totalLength = [lengths[i] doubleValue]; + CGFloat prevLength = i == 0 ? 0 : [lengths[i - 1] doubleValue]; + + CGFloat length = totalLength - prevLength; + CGFloat percent = (midPoint - prevLength) / length; + + NSArray * points = [lines objectAtIndex: i]; + CGPoint p1 = [[points objectAtIndex: 0] CGPointValue]; + CGPoint p2 = [[points objectAtIndex: 1] CGPointValue]; + + CGFloat ldx = p2.x - p1.x; + CGFloat ldy = p2.y - p1.y; + CGFloat angle = atan2(ldy, ldx); + + CGFloat px = p1.x + ldx * percent; + CGFloat py = p1.y + ldy * percent; + + transform = CGAffineTransformConcat(CGAffineTransformMakeTranslation(px, py), 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); @@ -830,17 +827,13 @@ CGFloat getTextAnchorOffset(enum TextAnchor textAnchor, CGFloat width) - (void)setupTextPath:(CGContextRef)context { - _bezierPath = nil; + lines = nil; + lengths = 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]; + [textPath getPathLength:&_pathLength lineCount:&lineCount lengths:&lengths lines:&lines isClosed:&isClosed]; return NO; } return YES; @@ -863,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 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..f6a3bfeb 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:(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 310a6b0e..81fd5df5 100644 --- a/ios/Text/RNSVGTextPath.m +++ b/ios/Text/RNSVGTextPath.m @@ -8,28 +8,212 @@ #import "RNSVGTextPath.h" +#import "BezierElement.h" + +/* Some 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 hypot(dx, dy); +} + +// 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; + NSUInteger lineCount; + CGFloat length; + BOOL isClosed; +} + +- (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]; + 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 segment between the start and end points + CGPoint ctrl1 = bez[1]; + CGPoint ctrl2 = bez[2]; + CGPoint next = bez[3]; + 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 + 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; + } + } + 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 new file mode 100644 index 00000000..5d59efc0 --- /dev/null +++ b/ios/Utils/BezierElement.h @@ -0,0 +1,25 @@ +/* + + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ + +#import +#import + +#define NULLPOINT CGRectNull.origin + +@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; ++ (NSArray *) elementsFromCGPath:(CGPathRef)path; + +@end; + diff --git a/ios/Utils/BezierElement.m b/ios/Utils/BezierElement.m new file mode 100644 index 00000000..98bc65d1 --- /dev/null +++ b/ios/Utils/BezierElement.m @@ -0,0 +1,77 @@ +/* + + Erica Sadun, http://ericasadun.com + https://github.com/erica/iOS-Drawing/tree/master/C08/Quartz%20Book%20Pack/Bezier + */ + +#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; +} + +// 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 719d8116..1c434cde 100644 --- a/ios/Utils/RNSVGPathParser.m +++ b/ios/Utils/RNSVGPathParser.m @@ -9,14 +9,13 @@ #import "RNSVGPathParser.h" #import #import "math.h" +#import "BezierElement.h" @implementation RNSVGPathParser { NSString* _d; NSString* _originD; NSRegularExpression* _pathRegularExpression; - NSMutableArray* _bezierCurves; - NSValue *_lastStartPoint; 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,9 +147,6 @@ _pivotX = _penX = x; _pivotY = _penY = y; CGPathMoveToPoint(path, nil, x, y); - - _lastStartPoint = [NSValue valueWithCGPoint: CGPointMake(x, y)]; - [_bezierCurves addObject: @[_lastStartPoint]]; } - (void)line:(CGMutablePathRef)path x:(float)x y:(float)y @@ -173,9 +159,6 @@ _pivotX = _penX = x; _pivotY = _penY = y; CGPathAddLineToPoint(path, nil, x, y); - - NSValue * destination = [NSValue valueWithCGPoint:CGPointMake(x, y)]; - [_bezierCurves addObject: @[destination, destination, destination]]; } - (void)curve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y c2x:(float)c2x c2y:(float)c2y ex:(float)ex ey:(float)ey @@ -201,12 +184,6 @@ _penX = ex; _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)] - ]]; } - (void)smoothCurve:(CGMutablePathRef)path c1x:(float)c1x c1y:(float)c1y ex:(float)ex ey:(float)ey @@ -367,14 +344,13 @@ 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); } } @@ -385,7 +361,6 @@ _penY = _penDownY; _penDownSet = NO; CGPathCloseSubpath(path); - [_bezierCurves addObject: @[_lastStartPoint, _lastStartPoint, _lastStartPoint]]; } } 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 diff --git a/lib/extract/extractTransform.js b/lib/extract/extractTransform.js index 44146ab2..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]; } @@ -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); } @@ -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 @@ -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 = [+-] 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'); -});