Exemplo n.º 1
0
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'"
            )
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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')
Exemplo n.º 5
0
    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,
Exemplo n.º 6
0
        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
Exemplo n.º 7
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)