svg.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. import lxml.etree as etree
  2. import lxml.builder as builder
  3. def stripStyles(svgImage):
  4. """
  5. Converts all instances of CSS style attibutes in an SVG to basic XML attributes.
  6. """
  7. elements = svgImage.findall(f"//*[@style]")
  8. for e in elements:
  9. styleString = e.attrib['style']
  10. styleListPre = styleString.split(";")
  11. for style in styleListPre:
  12. if style: # if it's not blank because splits can generate blank ends if there's only one style.
  13. splitStyle = style.split(":")
  14. #print(splitStyle)
  15. e.attrib[splitStyle[0]] = splitStyle[1]
  16. e.attrib.pop("style")
  17. def affinityDesignerCompensate(svgImage):
  18. """
  19. Compensates for shortcomings in Affinity's SVG exporter, whereby certain shapes
  20. (that are supposed to be filled black) are not given explicit fills or strokes.
  21. This is fine in an image context, but in a font context, these shapes take on
  22. the set colour of the text because they have no explicit color values.
  23. This function looks for paths and rects that have no explicit fill and no
  24. explicit stroke, and gives them a black fill.
  25. """
  26. xmlns = "{http://www.w3.org/2000/svg}"
  27. pathXP = "//" + xmlns + "path"
  28. rectXP = "//" + xmlns + "rect"
  29. circleXP = "//" + xmlns + "circle"
  30. ellipseXP = "//" + xmlns + "ellipse"
  31. serifRect = svgImage.find("//{http://www.w3.org/2000/svg}rect[@id]")
  32. if serifRect is not None:
  33. serifRect.getparent().remove(serifRect)
  34. if svgImage.find(pathXP) is not None:
  35. for e in svgImage.findall(pathXP):
  36. if "fill" not in e.attrib and "stroke" not in e.attrib:
  37. e.attrib["fill"] = "#000000"
  38. if svgImage.find(rectXP) is not None:
  39. for e in svgImage.findall(rectXP):
  40. if "fill" not in e.attrib and "stroke" not in e.attrib:
  41. e.attrib["fill"] = "#000000"
  42. if svgImage.find(circleXP) is not None:
  43. for e in svgImage.findall(circleXP):
  44. if "fill" not in e.attrib and "stroke" not in e.attrib:
  45. e.attrib["fill"] = "#000000"
  46. if svgImage.find(ellipseXP) is not None:
  47. for e in svgImage.findall(ellipseXP):
  48. if "fill" not in e.attrib and "stroke" not in e.attrib:
  49. e.attrib["fill"] = "#000000"
  50. def viewboxCompensate(metrics, svgImage):
  51. """
  52. viewboxes in SVGinOT are poorly and inconsistently implemented among many vendors,
  53. leading to serious font display issuies.
  54. This function strips out the viewBox attribute of an SVG and transforms it using metrics
  55. determined in the manifest to compensate for the loss of the viewBox.
  56. """
  57. svgRoot = svgImage.getroot()
  58. # calculate the transform
  59. # ---------------------------------------------------------------------------
  60. viewBoxWidth = svgRoot.attrib['viewBox'].split(' ')[2] # get the 3rd viewBox number (width)
  61. xPos = str(metrics['xMin'])
  62. yPos = str(-(metrics['yMax'])) # negate
  63. scale = metrics['unitsPerEm'] / int(viewBoxWidth) # determine the scale for the glyph based on UPEM.
  64. # make a transform group to wrap the SVG contents around
  65. # ---------------------------------------------------------------------------
  66. transformGroup = etree.Element("g", {"transform": f"translate({xPos}, {yPos}) scale({scale})"})
  67. for tag in iter(svgRoot):
  68. transformGroup.append(tag)
  69. # make a new SVG tag without the viewbox and append the transform group to it.
  70. # ---------------------------------------------------------------------------
  71. nsmap = { None: "http://www.w3.org/2000/svg"
  72. , "xlink" : "http://www.w3.org/1999/xlink"
  73. }
  74. svgcdata = etree.Element(svgRoot.tag, svgRoot.attrib, nsmap = nsmap)
  75. svgcdata.attrib.pop("viewBox")
  76. svgcdata.attrib["version"] = "1.1"
  77. svgcdata.append(transformGroup)
  78. # because lxml has a thing for annoying namespaces, you've gotta strip those out
  79. # ---------------------------------------------------------------------------
  80. svgcdatatree = svgcdata.getroottree()
  81. return svgcdatatree
  82. def compensateSVG(svgImage, m, afsc):
  83. """
  84. A function that determines what edits need to be made to an SVG before
  85. being loaded into an img class.
  86. """
  87. metrics = m['metrics']
  88. # strip styles if there are any.
  89. if svgImage.find(f"//*[@style]") is not None:
  90. stripStyles(svgImage)
  91. if afsc:
  92. affinityDesignerCompensate(svgImage)
  93. if svgImage.find(".[@viewBox]") is not None:
  94. return viewboxCompensate(metrics, svgImage)
  95. else:
  96. return svgImage