shade shade
shade shade

Reputation: 77

Unable to convert svg to png using svglib

When I convert svg to png I'm getting an uncomplete png file and the error. Please someone help.

from reportlab.graphics import renderPM
from svglib.svglib import svg2rlg

svg_file = 'svgfile.svg'

drawing = svg2rlg(svg_file)
renderPM.drawToFile(drawing, "new_file.png", fmt="PNG")
Can't handle color: url(#a)
Can't handle color: url(#b)
Can't handle color: url(#c)

Upvotes: 6

Views: 1588

Answers (3)

Anderson Laverde
Anderson Laverde

Reputation: 395

Javascript / Typescript version:

/**
   * TL;DR: Removes "fill="url()"" attributes from SVGs and replaces them with a solid color
   *
   * Replaces linear gradient fill attributes (eg fill="url(#a)") with a solid color from the linear gradiente.
   * If a fill attribute refers to another linear gradient, the stop color from the referenced gradient will be used.
   * The defs tag containing the linear gradients is also removed from the SVG.
   * * @example
   * const inputSVG = `
   *   <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
   *     <defs>
   *       <linearGradient id="a">
   *         <stop offset="0" stop-color="#f00"/>
   *         <stop offset="1" stop-color="#0f0"/>
   *       </linearGradient>
   *       <linearGradient id="b" xlink:href="#a"/>
   *     </defs>
   *     <rect x="10" y="10" width="80" height="80" fill="url(#a)" />
   *     <rect x="25" y="25" width="50" height="50" fill="url(#b)" />
   *   </svg>
   * `
   *
   * const outputSVG = replaceLinearGradientsForSolidColors(inputSVG)
   *
   * console.log(outputSVG)
   * <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
   *  <rect x="10" y="10" width="80" height="80" fill="#f00" />
   *  <rect x="25" y="25" width="50" height="50" fill="#f00" />
   * </svg>
   */
  replaceLinearGradientsForSolidColors = (svg: string): string => {
    // Step 1: Parse the input svg string into an XML document
    const parser = new DOMParser()
    const xmlDoc = parser.parseFromString(svg, 'text/xml')

    // Step 2: Get the 'defs' element in the xml document
    const defs = xmlDoc.getElementsByTagName('defs')[0]

    // Step 3: Create a mapping of gradient id to first stop color
    // Get all the 'linearGradient' elements in the xml document
    const gradients = xmlDoc.getElementsByTagName('linearGradient')
    const gradientMap = new Map()
    // For each 'linearGradient' element, get its first stop color
    for (const gradient of gradients) {
      const stops = gradient.getElementsByTagName('stop')
      // If the gradient has at least one stop, set the first stop color in the mapping
      if (stops.length > 0) {
        const firstStop = stops[0]
        const stopColor = firstStop.getAttribute('stop-color')
        gradientMap.set(gradient.getAttribute('id'), stopColor)
      }
    }

    // Step 4: Replace fill attributes with first stop color or referenced gradient
    // Get all the child elements in the xml document
    const childs = xmlDoc.getElementsByTagName('*')
    // For each child element, get its 'fill' attribute
    for (const child of childs) {
      const fill = child.getAttribute('fill')
      // If the 'fill' attribute starts with 'url(#', it refers to a gradient
      if (fill && fill.startsWith('url(#')) {
        // Extract the gradient id from the 'fill' attribute (e.g. url(#a) -> a)
        const gradientId = fill.substring(5, fill.length - 1)
        let stopColor = gradientMap.get(gradientId)
        // If the gradient id is not in the mapping, it may be a reference to another gradient
        if (!stopColor) {
          const referencedGradient = xmlDoc.getElementById(gradientId)
          // If there is a referenced gradient, extract its reference id
          if (referencedGradient) {
            const referenceId = referencedGradient.getAttribute('xlink:href')
            if (referenceId) {
              // Get the stop color from the referenced gradient in the mapping
              stopColor = gradientMap.get(referenceId.substring(1))
            }
          }
        }
        // If there is a stop color, set it as the 'fill' attribute for the child element
        if (stopColor) child.setAttribute('fill', stopColor)
      }
    }

    // Step 5: Get the 'svg' element in the xml document
    const svgElement = xmlDoc.getElementsByTagName('svg')[0]

    // Step 6: Remove the 'defs' element from its parent node
    defs.parentNode?.removeChild(defs)

    // Step 7: Serialize the xml document back into a string
    return new XMLSerializer().serializeToString(svgElement)
  }

Upvotes: 1

DrakeGladiator
DrakeGladiator

Reputation: 21

  1. Search for linear gradients within a svg file using the built-in xml.etree.ElementTree library using findall() with XPath expressions.
  2. Extract a solid color by selecting the first attribute "stop-color".
  3. Extract the "id" attribute to search the tree where the corresponding gradient was used.
  4. Search the tree and overwrite the attribute "fill" by the previously obtained solid-color.

(At the moment it only looks for linear gradients and does just take the first color as template. The script could be extendend by other gradient types and a method that extracts multiple colors to build an average color as template.)

import xml.etree.ElementTree as ET
from svglib.svglib import svg2rlg
from reportlab.graphics import renderPDF

# Remove gradients from svg file and replace them by solid color
# First open the svg file to be modified
tree = ET.parse("Graphs-Screenshot.svg")
root = tree.getroot()

# Search for the linearGradient tag within the svg file
for descendant in root.findall("{*}defs/{*}linearGradient"):
    print(descendant)
    id = descendant.get("id")
    print("id: " + id)

    # Extract the (first) color of the gradient as template
    # for solid color fill.
    # Might be extended to extract average of multiple colors
    color = descendant[0].get("stop-color")
    print("color: " + color)
    print("{*}g/{*}"+id)

    # Replace all gradients with previously extracted solid color
    for gradient in root.iterfind("{*}g/"):
        if gradient.get("fill") == ("url(#"+id+")"):
            print("old gradient:", gradient.get("fill"))
            gradient.set("fill", color)
            print("new solid color:", gradient.get("fill"))
    
# Save the edited svg file
tree.write("newSVG.svg", encoding="UTF-8")

# Convert svg to reportlab own rlg format
rlg = svg2rlg("newSVG.svg")
# Save a PDF out of rlg file
renderPDF.drawToFile(rlg, "Graphs-Screenshot.pdf")

Code can also be found here: https://github.com/DrakeGladiator/SVGGradientRemover

Upvotes: 1

dsaves
dsaves

Reputation: 301

Not sure if this is helpful in your case, but it answered my issue (same "Can't handle color: url(...)" message.

From the svglib PyPi documentation (https://pypi.org/project/svglib/):

color gradients are not supported (limitation of reportlab)

In my case, these colors were gradients, so I'm going to convert the svg to a png on the side, and then just embed the png image in the pdf via my Python script. Not ideally what I wanted, but this png workaround suffices for my requirements.

Upvotes: 4

Related Questions