styledpil.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import qrcode.image.base
  2. from PIL import Image
  3. from qrcode.image.styles.colormasks import QRColorMask, SolidFillColorMask
  4. from qrcode.image.styles.moduledrawers import SquareModuleDrawer
  5. class StyledPilImage(qrcode.image.base.BaseImageWithDrawer):
  6. """
  7. Styled PIL image builder, default format is PNG.
  8. This differs from the PilImage in that there is a module_drawer, a
  9. color_mask, and an optional image
  10. The module_drawer should extend the QRModuleDrawer class and implement the
  11. drawrect_context(self, box, active, context), and probably also the
  12. initialize function. This will draw an individual "module" or square on
  13. the QR code.
  14. The color_mask will extend the QRColorMask class and will at very least
  15. implement the get_fg_pixel(image, x, y) function, calculating a color to
  16. put on the image at the pixel location (x,y) (more advanced functionality
  17. can be gotten by instead overriding other functions defined in the
  18. QRColorMask class)
  19. The Image can be specified either by path or with a Pillow Image, and if it
  20. is there will be placed in the middle of the QR code. No effort is done to
  21. ensure that the QR code is still legible after the image has been placed
  22. there; Q or H level error correction levels are recommended to maintain
  23. data integrity A resampling filter can be specified (defaulting to
  24. PIL.Image.Resampling.LANCZOS) for resizing; see PIL.Image.resize() for possible
  25. options for this parameter.
  26. The image size can be controlled by `embedded_image_ratio` which is a ratio
  27. between 0 and 1 that's set in relation to the overall width of the QR code.
  28. """
  29. kind = "PNG"
  30. needs_processing = True
  31. color_mask: QRColorMask
  32. default_drawer_class = SquareModuleDrawer
  33. def __init__(self, *args, **kwargs):
  34. self.color_mask = kwargs.get("color_mask", SolidFillColorMask())
  35. # allow embeded_ parameters with typos for backwards compatibility
  36. embedded_image_path = kwargs.get(
  37. "embedded_image_path", kwargs.get("embeded_image_path", None)
  38. )
  39. self.embedded_image = kwargs.get(
  40. "embedded_image", kwargs.get("embeded_image", None)
  41. )
  42. self.embedded_image_ratio = kwargs.get(
  43. "embedded_image_ratio", kwargs.get("embeded_image_ratio", 0.25)
  44. )
  45. self.embedded_image_resample = kwargs.get(
  46. "embedded_image_resample",
  47. kwargs.get("embeded_image_resample", Image.Resampling.LANCZOS),
  48. )
  49. if not self.embedded_image and embedded_image_path:
  50. self.embedded_image = Image.open(embedded_image_path)
  51. # the paint_color is the color the module drawer will use to draw upon
  52. # a canvas During the color mask process, pixels that are paint_color
  53. # are replaced by a newly-calculated color
  54. self.paint_color = tuple(0 for i in self.color_mask.back_color)
  55. if self.color_mask.has_transparency:
  56. self.paint_color = tuple([*self.color_mask.back_color[:3], 255])
  57. super().__init__(*args, **kwargs)
  58. def new_image(self, **kwargs):
  59. mode = (
  60. "RGBA"
  61. if (
  62. self.color_mask.has_transparency
  63. or (self.embedded_image and "A" in self.embedded_image.getbands())
  64. )
  65. else "RGB"
  66. )
  67. # This is the background color. Should be white or whiteish
  68. back_color = self.color_mask.back_color
  69. return Image.new(mode, (self.pixel_size, self.pixel_size), back_color)
  70. def init_new_image(self):
  71. self.color_mask.initialize(self, self._img)
  72. super().init_new_image()
  73. def process(self):
  74. self.color_mask.apply_mask(self._img)
  75. if self.embedded_image:
  76. self.draw_embedded_image()
  77. def draw_embedded_image(self):
  78. if not self.embedded_image:
  79. return
  80. total_width, _ = self._img.size
  81. total_width = int(total_width)
  82. logo_width_ish = int(total_width * self.embedded_image_ratio)
  83. logo_offset = (
  84. int((int(total_width / 2) - int(logo_width_ish / 2)) / self.box_size)
  85. * self.box_size
  86. ) # round the offset to the nearest module
  87. logo_position = (logo_offset, logo_offset)
  88. logo_width = total_width - logo_offset * 2
  89. region = self.embedded_image
  90. region = region.resize((logo_width, logo_width), self.embedded_image_resample)
  91. if "A" in region.getbands():
  92. self._img.alpha_composite(region, logo_position)
  93. else:
  94. self._img.paste(region, logo_position)
  95. def save(self, stream, format=None, **kwargs):
  96. if format is None:
  97. format = kwargs.get("kind", self.kind)
  98. if "kind" in kwargs:
  99. del kwargs["kind"]
  100. self._img.save(stream, format=format, **kwargs)
  101. def __getattr__(self, name):
  102. return getattr(self._img, name)