Reputation: 77
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
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
Reputation: 21
(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
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