Search code examples
iosswiftcore-textcgpath

Converting Text to CGPath to svg | Problem with SVG Path Conversion from CGPath in iOS: Circular Path Instead of Text


I'm attempting to convert text to a path using CGPath in iOS and then convert that path to an SVG string, but when I save the SVG and load it again, the path is circular rather than forming the correct text shape.

Here’s my implementation:

textToPath function:

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: CGGlyph = 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:

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 problem: When I load the saved SVG file, it creates a circular path instead of the correct text shape. I believe the path conversion is not capturing the correct coordinates or curves, as it doesn't render properly in an SVG viewer.

Additional details: The textToPath function generates a CGPath based on the text and its font. The pathToSVG function is supposed to convert the CGPath to an SVG path string. The saved SVG file displays a circular path rather than the intended text.

What I've tried:

I've checked the transformation applied to the path, and it seems correct for inverting the Y-axis. I tried using both M and L for path commands, but the SVG still does not render the text as expected. Any help would be greatly appreciated. Thanks in advance!

Attach the image showing incorrect rendering


Solution

  • 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

    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
        }