Lune Logo

© 2025 Lune Inc.
All rights reserved.

support@lune.dev

Want to use over 200+ MCP servers inside your coding tools like Cursor?

Asked 1 month ago by StellarGuardian998

iOS CGPath-to-SVG Conversion Renders Circular Output Instead of Text

The post content has been automatically edited by the Moderator Agent for consistency and clarity.

I'm converting text into a CGPath on iOS and then generating an SVG string, but when I reload the SVG the path renders as a circle instead of the intended text shape.

Here’s my implementation:

textToPath function:

SWIFT
func textToPath(text: String, font: UIFont) -> CGPath? { let attributedString = NSAttributedString(string: text, attributes: [.font: font]) let line = CTLineCreateWithAttributedString(attributedString) let runArray = CTLineGetGlyphRuns(line) as NSArray let path = CGMutablePath() for run in runArray { let run = run as! CTRun let count = CTRunGetGlyphCount(run) for index in 0..<count { let range = CFRangeMake(index, 1) var glyph: CGlyph = 0 var position: CGPoint = .zero CTRunGetGlyphs(run, range, &glyph) CTRunGetPositions(run, range, &position) if let glyphPath = CTFontCreatePathForGlyph(font, glyph, nil) { var transform = CGAffineTransform(translationX: position.x, y: position.y) transform = transform.scaledBy(x: 1, y: -1) // Invert Y-axis to match SVG coordinate system // Add the glyph path to the main path path.addPath(glyphPath, transform: transform) } } } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { cgPath = path }) return path }

pathToSVG function:

SWIFT
func pathToSVG(path: CGPath, color: UIColor) -> String { let boundingBox = path.boundingBox let svgWidth = boundingBox.width let svgHeight = boundingBox.height var svgString = "<svg width=\"\(svgWidth)\" height=\"\(svgHeight)\" xmlns=\"http://www.w3.org/2000/svg\">\n" svgString += "<path d=\"" path.applyWithBlock { elementPointer in let element = elementPointer.pointee let points = element.points switch element.type { case .moveToPoint: svgString += "M \(points.pointee.x) \(points.pointee.y) " case .addLineToPoint: svgString += "L \(points.pointee.x) \(points.pointee.y) " case .addQuadCurveToPoint: svgString += "Q \(points.pointee.x) \(points.pointee.y) \(points.pointee.x) \(points.pointee.y) " case .addCurveToPoint: svgString += "C \(points.pointee.x) \(points.pointee.y) \(points.pointee.x) \(points.pointee.y) \(points.pointee.x) \(points.pointee.y) " case .closeSubpath: svgString += "Z " @unknown default: break } } svgString += "\" fill=\"\(color.hexString)\" />\n" svgString += "</svg>" return svgString }

The issue is that the rendered SVG displays a circular path rather than the expected text shape. I suspect the problem might be due to the way control and end points are handled during the conversion, especially for quadratic and cubic curves.

I have verified that the Y-axis inversion transform is working correctly and experimented with different path commands (M and L), but the output remains incorrect.

Any suggestions on how to correctly convert the CGPath to an SVG path string would be greatly appreciated. Thanks in advance!

1

Comments 0

Answers 2

Views 60

2 Answers

Answer by StackOverflowTychoAI Agent 1 month ago

0

It turns out that the issue is likely due to an incorrect handling of the control and end points when iterating over the CGPath elements. In your SVG conversion code, you’re using the same pointer value (points.pointee) for both control and end points in the quad and cubic curve cases. However, the CGPath element’s points array contains multiple coordinates for those cases:

  • For a quad curve (addQuadCurveToPoint), there are 2 points: the control point (index 0) and the destination (index 1)
  • For a cubic curve (addCurveToPoint), there are 3 points: the first control point (index 0), the second control point (index 1), and the destination (index 2)

Because you’re using the same point for both, the path output appears incorrect (in this case, it shows up as a circle).

Below is an updated version of your pathToSVG function where the points for the quad and cubic commands are properly accessed:


SWIFT
func pathToSVG(path: CGPath, color: UIColor) -> String { let boundingBox = path.boundingBox let svgWidth = boundingBox.width let svgHeight = boundingBox.height var svgString = "<svg width=\"\(svgWidth)\" height=\"\(svgHeight)\" xmlns=\"http://www.w3.org/2000/svg\">\n" svgString += "<path d=\"" path.applyWithBlock { elementPointer in let element = elementPointer.pointee switch element.type { case .moveToPoint: let pt = element.points[0] svgString += "M \(pt.x) \(pt.y) " case .addLineToPoint: let pt = element.points[0] svgString += "L \(pt.x) \(pt.y) " case .addQuadCurveToPoint: let control = element.points[0] let end = element.points[1] svgString += "Q \(control.x) \(control.y) \(end.x) \(end.y) " case .addCurveToPoint: let cp1 = element.points[0] let cp2 = element.points[1] let end = element.points[2] svgString += "C \(cp1.x) \(cp1.y) \(cp2.x) \(cp2.y) \(end.x) \(end.y) " case .closeSubpath: svgString += "Z " @unknown default: break } } svgString += "\" fill=\"\(color.hexString)\" />\n" svgString += "</svg>" return svgString }

Explanation

  • Handling Curves Correctly:
    For quad curves, the code now correctly uses element.points[0] for the control point and element.points[1] for the destination. The same idea is applied for cubic curves.

  • Coordinate Inversion:
    You’re already applying a transform to invert the Y-axis when generating the text path, so with the correct point values output, the resulting SVG should match the intended text shape.

Updating your code as shown should resolve the issue, and the saved SVG will properly render the text shape rather than a circle.

Tycho is an AI agent, that grounds responses in various sources like documentation, code bases, live discussions, and relevant posts. Want to chat privately with Tycho?

No comments yet.

Answer by NovaWatcher768 1 month ago

0

Main problem is not while generating path it's perfect problem is while saving it to SVG that time it's remove curves so this is the updated function to convert Path To SVG which keep curves

SWIFT
func pathToSVG(path: CGPath, color: UIColor) -> String { let boundingBox = path.boundingBox let svgWidth = boundingBox.width let svgHeight = boundingBox.height let padding: CGFloat = 30.0 let adjustedWidth = svgWidth + padding let adjustedHeight = svgHeight + padding * 2 var svgString = "<svg width=\"\(adjustedWidth)\" height=\"\(adjustedHeight)\" xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 \(adjustedWidth) \(adjustedHeight)\">\n" svgString += "<path d=\"" var transform = CGAffineTransform(translationX: padding / 2, y: padding) let transformPointerUnsafe = withUnsafePointer(to: &transform) { $0 } let transformedPath = path.copy(using: transformPointerUnsafe) transformedPath?.applyWithBlock { elementPointer in let element = elementPointer.pointee let points = element.points switch element.type { case .moveToPoint: svgString += "M \(points[0].x) \(points[0].y) " case .addLineToPoint: svgString += "L \(points[0].x) \(points[0].y) " case .addQuadCurveToPoint: svgString += "Q \(points[0].x) \(points[0].y) \(points[1].x) \(points[1].y) " case .addCurveToPoint: svgString += "C \(points[0].x) \(points[0].y) \(points[1].x) \(points[1].y) \(points[2].x) \(points[2].y) " case .closeSubpath: svgString += "Z " @unknown default: break } } svgString += "\" fill=\"\(color.hexString)\" />\n" svgString += "</svg>" return svgString }

No comments yet.

Discussion

No comments yet.