class QRPosition(enum.Enum): """ QR positioning constants, with the corresponding default content layout rule. """ LEFT_OF_TEXT = layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MIN, y_align=layout.AxisAlignment.ALIGN_MID, ) RIGHT_OF_TEXT = layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MAX, y_align=layout.AxisAlignment.ALIGN_MID, ) ABOVE_TEXT = layout.SimpleBoxLayoutRule( y_align=layout.AxisAlignment.ALIGN_MAX, x_align=layout.AxisAlignment.ALIGN_MID, ) BELOW_TEXT = layout.SimpleBoxLayoutRule( y_align=layout.AxisAlignment.ALIGN_MIN, x_align=layout.AxisAlignment.ALIGN_MID, ) @property def horizontal_flow(self): return self in (QRPosition.LEFT_OF_TEXT, QRPosition.RIGHT_OF_TEXT) @classmethod def from_config(cls, config_str) -> 'QRPosition': """ Convert from a configuration string. :param config_str: A string: 'left', 'right', 'top', 'bottom' :return: An :class:`.QRPosition` value. :raise ConfigurationError: on unexpected string inputs. """ try: return { 'left': QRPosition.LEFT_OF_TEXT, 'right': QRPosition.RIGHT_OF_TEXT, 'top': QRPosition.ABOVE_TEXT, 'bottom': QRPosition.BELOW_TEXT }[config_str.lower()] except KeyError: raise ConfigurationError( f"'{config_str}' is not a valid QR position setting; valid " f"values are 'left', 'right', 'top', 'bottom'" )
class BaseStampStyle(ConfigurableMixin): """ Base class for stamp styles. """ border_width: int = 3 """ Border width in user units (for the stamp, not the text box). """ background: content.PdfContent = None """ :class:`~.pdf_utils.content.PdfContent` instance that will be used to render the stamp's background. """ background_layout: layout.SimpleBoxLayoutRule = layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MID, margins=layout.Margins.uniform(5) ) """ Layout rule to render the background inside the stamp's bounding box. Only used if the background has a fully specified :attr:`PdfContent.box`. Otherwise, the renderer will position the cursor at ``(left_margin, bottom_margin)`` and render the content as-is. """ background_opacity: float = 0.6 """ Opacity value to render the background at. This should be a floating-point number between `0` and `1`. """ @classmethod def process_entries(cls, config_dict): """ This implementation of :meth:`process_entries` processes the :attr:`background` configuration value. This can either be a path to an image file, in which case it will be turned into an instance of :class:`~.pdf_utils.images.PdfImage`, or the special value ``__stamp__``, which is an alias for :const:`~pyhanko.stamp.STAMP_ART_CONTENT`. """ super().process_entries(config_dict) bg_spec = None try: bg_spec = config_dict['background'] except KeyError: pass if bg_spec is not None: config_dict['background'] = _get_background_content(bg_spec) def create_stamp(self, writer: BasePdfFileWriter, box: layout.BoxConstraints, text_params: dict) \ -> 'BaseStamp': raise NotImplementedError
def _arabic_text_page(stream_xrefs): w = empty_page(stream_xrefs=stream_xrefs) style = TextStampStyle( stamp_text='اَلْفُصْحَىٰ', text_box_style=TextBoxStyle( font=GlyphAccumulatorFactory(NOTO_SANS_ARABIC), ), inner_content_layout=layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MID, inner_content_scaling=layout.InnerScaling.STRETCH_TO_FIT, margins=layout.Margins.uniform(5))) ts = TextStamp(writer=w, style=style, box=layout.BoxConstraints(width=300, height=200)) ts.apply(0, x=10, y=60) return w
def test_read_qr_config(): from pyhanko.pdf_utils.font import SimpleFontEngineFactory from pyhanko.pdf_utils.font.opentype import GlyphAccumulatorFactory from pyhanko_tests.test_text import NOTO_SERIF_JP config_string = f""" stamp-styles: default: text-box-style: font: {NOTO_SERIF_JP} type: qr background: __stamp__ qr-position: right inner-content-layout: y-align: bottom x-align: mid margins: left: 10 right: 10 alternative1: text-box-style: font: {NOTO_SERIF_JP} background: pyhanko_tests/data/img/stamp-indexed.png type: qr alternative2: type: qr background: pyhanko_tests/data/pdf/pdf-background-test.pdf alternative3: type: text wrong-position: type: qr qr-position: bleh """ cli_config: config.CLIConfig = config.parse_cli_config(config_string) default_qr_style = cli_config.get_stamp_style() assert isinstance(default_qr_style, QRStampStyle) assert default_qr_style.background is stamp.STAMP_ART_CONTENT assert isinstance(default_qr_style.text_box_style.font, GlyphAccumulatorFactory) assert default_qr_style.qr_position == stamp.QRPosition.RIGHT_OF_TEXT expected_layout = layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MIN, margins=layout.Margins(left=10, right=10)) assert default_qr_style.inner_content_layout == expected_layout alternative1 = cli_config.get_stamp_style('alternative1') assert isinstance(alternative1, QRStampStyle) assert isinstance(alternative1.background, PdfImage) assert isinstance(alternative1.text_box_style.font, GlyphAccumulatorFactory) assert alternative1.qr_position == stamp.QRPosition.LEFT_OF_TEXT alternative2 = cli_config.get_stamp_style('alternative2') assert isinstance(alternative2, QRStampStyle) assert isinstance(alternative2.background, ImportedPdfPage) assert isinstance(alternative2.text_box_style.font, SimpleFontEngineFactory) alternative3 = cli_config.get_stamp_style('alternative3') assert isinstance(alternative3, TextStampStyle) assert alternative3.background is None assert isinstance(alternative3.text_box_style.font, SimpleFontEngineFactory) with pytest.raises(ConfigurationError, match='not a valid QR position'): cli_config.get_stamp_style('wrong-position') with pytest.raises(ConfigurationError): cli_config.get_stamp_style('theresnosuchstyle')
cli_config = config.parse_cli_config(f""" pkcs12-setups: foo: pfx-file: '{TESTING_CA_DIR}/interm/signer1.pfx' other-certs: '{TESTING_CA_DIR}/ca-chain.cert.pem' pfx-passphrase: "this passphrase is wrong" """) setup = cli_config.get_pkcs12_config('foo') with pytest.raises(ConfigurationError): setup.instantiate() @pytest.mark.parametrize('cfg_str,expected_result', [ ("x-align: left", layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MIN, y_align=layout.AxisAlignment.ALIGN_MID, )), ("y-align: bottom", layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MIN, )), (f""" y-align: bottom x-align: mid margins: left: 10 right: 10 """, layout.SimpleBoxLayoutRule(x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MIN,
try: fc = config_dict['font'] if not isinstance(fc, str) or \ not (fc.endswith('.otf') or fc.endswith('.ttf')): raise ConfigurationError( "'font' must be a path to an OpenType or " "TrueType font file.") from pyhanko.pdf_utils.font.opentype import GlyphAccumulatorFactory config_dict['font'] = GlyphAccumulatorFactory(fc) except KeyError: pass DEFAULT_BOX_LAYOUT = layout.SimpleBoxLayoutRule( x_align=layout.AxisAlignment.ALIGN_MID, y_align=layout.AxisAlignment.ALIGN_MID, ) DEFAULT_TEXT_BOX_MARGIN = 10 @dataclass(frozen=True) class TextBoxStyle(TextStyle): """Extension of :class:`.TextStyle` for use in text boxes.""" border_width: int = 0 """ Border width, if applicable. """ box_layout_rule: layout.SimpleBoxLayoutRule = None
def _inner_layout_natural_size(self): text_commands, (text_width, text_height) \ = super()._inner_layout_natural_size() qr_ref, natural_qr_size = self._qr_xobject() self.set_resource( category=content.ResourceType.XOBJECT, name=pdf_name('/QR'), value=qr_ref ) style = self.style stamp_box = self.box # To size the QR code, we proceed as follows: # - If qr_inner_size is not None, use it # - If the stamp has a fully defined bbox already, # make sure it fits within the innseps, and it's not too much smaller # than the text box # - Else, scale down by DEFAULT_QR_SCALE and use that value # # Note: if qr_inner_size is defined AND the stamp bbox is available # already, scaling might still take effect depending on the inner layout # rule. innsep = style.innsep if style.qr_inner_size is not None: qr_size = style.qr_inner_size elif stamp_box.width_defined and stamp_box.height_defined: # ensure that the QR code doesn't shrink too much if the text # box is too tall. min_dim = min( max(stamp_box.height, text_height), max(stamp_box.width, text_width) ) qr_size = min_dim - 2 * innsep else: qr_size = int(round(DEFAULT_QR_SCALE * natural_qr_size)) qr_innunits_scale = qr_size / natural_qr_size qr_padded = qr_size + 2 * innsep # Next up: put the QR code and the text box together to get the # inner layout bounding box if style.qr_position.horizontal_flow: inn_width = qr_padded + text_width inn_height = max(qr_padded, text_height) else: inn_width = max(qr_padded, text_width) inn_height = qr_padded + text_height # grab the base layout rule from the QR position setting default_layout: layout.SimpleBoxLayoutRule = style.qr_position.value # Fill in the margins qr_layout_rule = layout.SimpleBoxLayoutRule( x_align=default_layout.x_align, y_align=default_layout.y_align, margins=layout.Margins.uniform(innsep), # There's no point in scaling here, the inner content canvas # is always big enough inner_content_scaling=layout.InnerScaling.NO_SCALING ) inner_box = layout.BoxConstraints(inn_width, inn_height) qr_inn_pos = qr_layout_rule.fit(inner_box, qr_size, qr_size) # we still need to take the axis reversal into account # (which also implies an adjustment in the y displacement) draw_qr_command = b'q %g 0 0 %g %g %g cm /QR Do Q' % ( qr_inn_pos.x_scale * qr_innunits_scale, -qr_inn_pos.y_scale * qr_innunits_scale, qr_inn_pos.x_pos, qr_inn_pos.y_pos + qr_size, ) # Time to put in the text box now if style.qr_position == QRPosition.LEFT_OF_TEXT: tb_margins = layout.Margins( left=qr_padded, right=0, top=0, bottom=0 ) elif style.qr_position == QRPosition.RIGHT_OF_TEXT: tb_margins = layout.Margins( right=qr_padded, left=0, top=0, bottom=0 ) elif style.qr_position == QRPosition.BELOW_TEXT: tb_margins = layout.Margins( bottom=qr_padded, right=0, left=0, top=0 ) else: tb_margins = layout.Margins( top=qr_padded, right=0, left=0, bottom=0 ) tb_layout_rule = layout.SimpleBoxLayoutRule( # flip around the alignment conventions of the default layout # to position the text box on the other side x_align=default_layout.x_align.flipped, y_align=default_layout.y_align.flipped, margins=tb_margins, inner_content_scaling=layout.InnerScaling.NO_SCALING ) # position the text box text_inn_pos = tb_layout_rule.fit(inner_box, text_width, text_height) commands = [draw_qr_command, b'q', text_inn_pos.as_cm()] commands.extend(text_commands) commands.append(b'Q') return commands, (inn_width, inn_height)