svg.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import decimal
  2. from decimal import Decimal
  3. from typing import Optional, Union, overload, Literal
  4. import qrcode.image.base
  5. from qrcode.compat.etree import ET
  6. from qrcode.image.styles.moduledrawers import svg as svg_drawers
  7. from qrcode.image.styles.moduledrawers.base import QRModuleDrawer
  8. class SvgFragmentImage(qrcode.image.base.BaseImageWithDrawer):
  9. """
  10. SVG image builder
  11. Creates a QR-code image as a SVG document fragment.
  12. """
  13. _SVG_namespace = "http://www.w3.org/2000/svg"
  14. kind = "SVG"
  15. allowed_kinds = ("SVG",)
  16. default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgSquareDrawer
  17. def __init__(self, *args, **kwargs):
  18. ET.register_namespace("svg", self._SVG_namespace)
  19. super().__init__(*args, **kwargs)
  20. # Save the unit size, for example the default box_size of 10 is '1mm'.
  21. self.unit_size = self.units(self.box_size)
  22. @overload
  23. def units(self, pixels: Union[int, Decimal], text: Literal[False]) -> Decimal: ...
  24. @overload
  25. def units(self, pixels: Union[int, Decimal], text: Literal[True] = True) -> str: ...
  26. def units(self, pixels, text=True):
  27. """
  28. A box_size of 10 (default) equals 1mm.
  29. """
  30. units = Decimal(pixels) / 10
  31. if not text:
  32. return units
  33. units = units.quantize(Decimal("0.001"))
  34. context = decimal.Context(traps=[decimal.Inexact])
  35. try:
  36. for d in (Decimal("0.01"), Decimal("0.1"), Decimal("0")):
  37. units = units.quantize(d, context=context)
  38. except decimal.Inexact:
  39. pass
  40. return f"{units}mm"
  41. def save(self, stream, kind=None):
  42. self.check_kind(kind=kind)
  43. self._write(stream)
  44. def to_string(self, **kwargs):
  45. return ET.tostring(self._img, **kwargs)
  46. def new_image(self, **kwargs):
  47. return self._svg(**kwargs)
  48. def _svg(self, tag=None, version="1.1", **kwargs):
  49. if tag is None:
  50. tag = ET.QName(self._SVG_namespace, "svg")
  51. dimension = self.units(self.pixel_size)
  52. return ET.Element(
  53. tag, # type: ignore
  54. width=dimension,
  55. height=dimension,
  56. version=version,
  57. **kwargs,
  58. )
  59. def _write(self, stream):
  60. ET.ElementTree(self._img).write(stream, xml_declaration=False)
  61. class SvgImage(SvgFragmentImage):
  62. """
  63. Standalone SVG image builder
  64. Creates a QR-code image as a standalone SVG document.
  65. """
  66. background: Optional[str] = None
  67. drawer_aliases: qrcode.image.base.DrawerAliases = {
  68. "circle": (svg_drawers.SvgCircleDrawer, {}),
  69. "gapped-circle": (svg_drawers.SvgCircleDrawer, {"size_ratio": Decimal(0.8)}),
  70. "gapped-square": (svg_drawers.SvgSquareDrawer, {"size_ratio": Decimal(0.8)}),
  71. }
  72. def _svg(self, tag="svg", **kwargs):
  73. svg = super()._svg(tag=tag, **kwargs)
  74. svg.set("xmlns", self._SVG_namespace)
  75. if self.background:
  76. svg.append(
  77. ET.Element(
  78. "rect",
  79. fill=self.background,
  80. x="0",
  81. y="0",
  82. width="100%",
  83. height="100%",
  84. )
  85. )
  86. return svg
  87. def _write(self, stream):
  88. ET.ElementTree(self._img).write(stream, encoding="UTF-8", xml_declaration=True)
  89. class SvgPathImage(SvgImage):
  90. """
  91. SVG image builder with one single <path> element (removes white spaces
  92. between individual QR points).
  93. """
  94. QR_PATH_STYLE = {
  95. "fill": "#000000",
  96. "fill-opacity": "1",
  97. "fill-rule": "nonzero",
  98. "stroke": "none",
  99. }
  100. needs_processing = True
  101. path: Optional[ET.Element] = None
  102. default_drawer_class: type[QRModuleDrawer] = svg_drawers.SvgPathSquareDrawer
  103. drawer_aliases = {
  104. "circle": (svg_drawers.SvgPathCircleDrawer, {}),
  105. "gapped-circle": (
  106. svg_drawers.SvgPathCircleDrawer,
  107. {"size_ratio": Decimal(0.8)},
  108. ),
  109. "gapped-square": (
  110. svg_drawers.SvgPathSquareDrawer,
  111. {"size_ratio": Decimal(0.8)},
  112. ),
  113. }
  114. def __init__(self, *args, **kwargs):
  115. self._subpaths: list[str] = []
  116. super().__init__(*args, **kwargs)
  117. def _svg(self, viewBox=None, **kwargs):
  118. if viewBox is None:
  119. dimension = self.units(self.pixel_size, text=False)
  120. viewBox = "0 0 {d} {d}".format(d=dimension)
  121. return super()._svg(viewBox=viewBox, **kwargs)
  122. def process(self):
  123. # Store the path just in case someone wants to use it again or in some
  124. # unique way.
  125. self.path = ET.Element(
  126. ET.QName("path"), # type: ignore
  127. d="".join(self._subpaths),
  128. id="qr-path",
  129. **self.QR_PATH_STYLE,
  130. )
  131. self._subpaths = []
  132. self._img.append(self.path)
  133. class SvgFillImage(SvgImage):
  134. """
  135. An SvgImage that fills the background to white.
  136. """
  137. background = "white"
  138. class SvgPathFillImage(SvgPathImage):
  139. """
  140. An SvgPathImage that fills the background to white.
  141. """
  142. background = "white"