)),
 ("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,
                             margins=layout.Margins(left=10, right=10))),
 (f"""
     y-align: bottom
     x-align: mid
     margins: [10, 10, 0, 0]
     """,
  layout.SimpleBoxLayoutRule(x_align=layout.AxisAlignment.ALIGN_MID,
                             y_align=layout.AxisAlignment.ALIGN_MIN,
                             margins=layout.Margins(left=10, right=10))),
 (f"""
     y-align: bottom
     x-align: mid
     inner-content-scaling: none
     """,
  layout.SimpleBoxLayoutRule(
      x_align=layout.AxisAlignment.ALIGN_MID,
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')
Exemple #3
0
    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)