
| Current Path : /var/www/wsgi/www/api/venv/lib64/python3.12/site-packages/pyhanko/pdf_utils/ |
Linux ift1.ift-informatik.de 5.4.0-216-generic #236-Ubuntu SMP Fri Apr 11 19:53:21 UTC 2025 x86_64 |
| Current File : /var/www/wsgi/www/api/venv/lib64/python3.12/site-packages/pyhanko/pdf_utils/qr.py |
import itertools
import math
from typing import List, Optional
import qrcode.util
from pyhanko.pdf_utils.content import PdfContent
from pyhanko.pdf_utils.misc import rd
from qrcode.image.base import BaseImage
class PdfStreamQRImage(BaseImage):
"""
Quick-and-dirty implementation of the Image interface required
by the qrcode package.
"""
kind = "PDF"
allowed_kinds = ("PDF",)
qr_color = (0, 0, 0)
def new_image(self, **kwargs):
return []
def drawrect(self, row, col):
self._img.append((row, col))
def append_single_rect(self, command_stream, row, col):
command_stream.append(b'%g %g 1 1 re' % (col, row))
def format_qr_color(self):
return (b"%g %g %g rg\n" % self.qr_color) + (
b"%g %g %g RG" % self.qr_color
)
def setup_drawing_area(self):
# start a command stream with fill colour set to black (default)
# and transform the coordinate system to line up with our grid
brd = rd(self.border * self.box_size)
ydiff = rd(self.width * self.box_size)
cm = f"{rd(self.box_size)} 0 0 {-rd(self.box_size)} {brd} {brd + ydiff} cm"
return b"%s\n%s" % (self.format_qr_color(), cm.encode('ascii'))
def render_command_stream(self):
command_stream = [self.setup_drawing_area()]
for row, col in self._img:
# paint a rectangle
self.append_single_rect(command_stream, row, col)
command_stream.append(b"f")
return b'\n'.join(command_stream)
def save(self, stream, kind=None):
raise NotImplementedError
def process(self):
raise NotImplementedError
def drawrect_context(self, row, col, active, context):
return self.drawrect(row, col) # pragma: nocover
class PdfFancyQRImage(PdfStreamQRImage):
centerpiece_corner_radius = 0.2
def __init__(
self,
border,
width,
box_size,
*_args,
version,
center_image: Optional[PdfContent] = None,
**kwargs,
):
super().__init__(border, width, box_size, **kwargs)
self._version = version
self._centerpiece = center_image
def save(self, stream, kind=None):
raise NotImplementedError
def process(self):
raise NotImplementedError
def append_single_rect(self, command_stream, row, col):
if self.is_position_pattern(row, col):
return
command_stream.extend(rounded_square(col, row, 0.9, 0.3))
def is_major_position_pattern(self, row, col):
return (
(row < 7 and col < 7)
or ((row > self.width - 8) and col < 7)
or (row < 7 and (col > self.width - 8))
)
def _enumerate_alignment_patterns(self):
adj_ptns = qrcode.util.pattern_position(self._version)
for pr, pc in itertools.product(adj_ptns, adj_ptns):
# don't consider the bits that fall within the "big" position
# patterns
if self.is_major_position_pattern(pr, pc):
continue
yield pr, pc
def is_position_pattern(self, row, col):
if self.is_major_position_pattern(row, col):
return True
# check whether this pixel is part of an alignment pattern
return any(
abs(row - pr) <= 2 and abs(col - pc) <= 2
for pr, pc in self._enumerate_alignment_patterns()
)
def draw_position_patterns(self):
# draw the surrounding squares of the location patterns first
# -> stroked paths (outer squares)
# (these require drawing at module midpoints)
command_stream = [b'q\n1 0 0 1 0.5 0.5 cm\n0.7 w']
sz = self.width
command_stream.extend(rounded_square(0, 0, 6, 1))
command_stream.extend(rounded_square(0, sz - 7, 6, 1))
command_stream.extend(rounded_square(sz - 7, 0, 6, 1))
for pr, pc in self._enumerate_alignment_patterns():
command_stream.extend(rounded_square(pr - 2, pc - 2, 4, 0.7))
command_stream.append(b"S\nQ")
# draw the inner squares of the location patterns
command_stream.extend(rounded_square(2, 2, 3, 0.6))
command_stream.extend(rounded_square(2, sz - 7 + 2, 3, 0.6))
command_stream.extend(rounded_square(sz - 7 + 2, 2, 3, 0.6))
for pr, pc in self._enumerate_alignment_patterns():
command_stream.extend(rounded_square(pr, pc, 1, 0.1))
command_stream.append(b"f")
return b"\n".join(command_stream)
def draw_centerpiece(self):
c_x, c_y, c_sz = self._measure_out_centerpiece()
# better hope it's square
c_w = self._centerpiece.box.width
c_h = self._centerpiece.box.height
# draw the border of the centerpiece
centerpiece_commands = [b"q", b"0.2 w"]
centerpiece_commands.extend(
rounded_square(
c_x, c_y, c_sz, self.centerpiece_corner_radius * c_sz
)
)
centerpiece_commands.append(b"S\nQ\nq")
# transform back into the internal coordinates of the centerpiece
# (including any y-axis reversals)
# -> f"{x_scale} 0 0 {-y_scale} {c_x} {c_y + c_sz} cm"
x_scale = rd(c_sz / c_w)
y_scale = rd(c_sz / c_h)
# COMBINED WITH:
# we slightly shrink and offset the centerpiece to create
# a border of sorts
# -> f"{shrink} 0 0 {shrink} {x_shift} {y_shift} cm"
shrink = 0.85
x_shift = rd((1 - shrink) * c_w / 2)
y_shift = rd((1 - shrink) * c_h / 2)
centerpiece_commands.append(
f"{x_scale * shrink} 0 0 {-y_scale * shrink} "
f"{c_x + x_shift * x_scale} {c_y + c_sz - y_shift * y_scale} "
f"cm".encode('ascii')
)
# resource management left up to the caller
centerpiece_commands.append(self._centerpiece.render())
centerpiece_commands.append(b"Q")
return b"\n".join(centerpiece_commands)
def _measure_out_centerpiece(self):
# Centerpiece area takes up a square in the QR code with a side of
# about 28% the total size of the QR code itself
c_sz = 0.28 * self.width
c_x = (self.width - c_sz) / 2
c_y = (self.width - c_sz) / 2
return rd(c_x), rd(c_y), rd(c_sz)
def setup_drawing_area(self):
basic_setup = super().setup_drawing_area()
commands = [basic_setup]
if self._centerpiece is not None:
# we need to clip out the centerpiece area.
# we'll do that by creating a clip path that goes around the
# entire QR code, and a rounded rectangle where the centerpiece
# would go using opposite orientation.
# -> clockwise rectangle
w = rd(self.width)
# save the state to undo the clipping later
commands.append(b"q")
commands.append(b"0.2 w")
commands.append(
f"0 0 m 0 {w} l {w} {w} l {w} 0 l h".encode("ascii"),
)
c_x, c_y, c_sz = self._measure_out_centerpiece()
commands.extend(
rounded_square(
c_x, c_y, c_sz, self.centerpiece_corner_radius * c_sz
)
)
commands.append(b"W n")
return b"\n".join(commands)
def render_command_stream(self):
parts = [super().render_command_stream(), self.draw_position_patterns()]
if self._centerpiece:
parts.append(b"Q") # undo clipping
parts.append(self.draw_centerpiece())
return b"\n".join(parts)
def rounded_square(
x_pos: float, y_pos: float, sz: float, rad: float
) -> List[bytes]:
"""
Add a subpath of a square with rounded corners at the given position.
Doesn't include any painting or clipping operations.
The path is drawn counterclockwise.
:param x_pos:
The x-coordinate of the enveloping square's lower left corner.
:param y_pos:
The y-coordinate of the enveloping square's lower left corner.
:param sz:
The side length of the enveloping square.
:param rad:
The corner radius.
:return:
A list of graphics operators.
"""
c_off = (4 * (math.sqrt(2) - 1) / 3) * rad
result = []
def fmt(x, y):
px = rd(x_pos + x)
py = rd(y_pos + y)
result.append(f"{px} {py} ".encode('ascii'))
def op(pts, opc: str):
for x, y in pts:
fmt(x, y)
result.append(opc.encode('ascii'))
def uop(x, y, opc):
op([(x, y)], opc)
uop(rad, 0, "m")
uop(sz - rad, 0, "l")
op([(sz - c_off, 0), (sz, c_off), (sz, rad)], "c")
uop(sz, sz - rad, "l")
op([(sz, sz - c_off), (sz - c_off, sz), (sz - rad, sz)], "c")
uop(rad, sz, "l")
op([(c_off, sz), (0, sz - c_off), (0, sz - rad)], "c")
uop(0, rad, "l")
op([(0, c_off), (c_off, 0), (rad, 0)], "c")
result.append(b"h")
return result