summaryrefslogtreecommitdiff
path: root/src/pikepdf/objects.py
blob: a888b9767449ef5ea8428d24ef34236161976643 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (C) 2017, James R. Barlow (https://github.com/jbarlow83/)

"""Provide classes to stand in for PDF objects

The purpose of these is to provide nice-looking classes to allow explicit
construction of PDF objects and more pythonic idioms and facilitate discovery
by documentation generators.

It's also a place to narrow the scope of input types to those more easily
converted to C++.

In reality all of these return objects of class pikepdf.Object or rather
QPDFObjectHandle which is a generic type.

"""

from . import _qpdf
from ._qpdf import Object, ObjectType

# pylint: disable=unused-import
from ._qpdf import Operator


class _ObjectMeta(type):
    """Supports instance checking"""

    def __instancecheck__(cls, instance):
        if type(instance) != Object:
            return False
        return cls.object_type == instance._type_code


class _NameObjectMeta(_ObjectMeta):
    """Supports usage pikepdf.Name.Whatever -> Name('/Whatever')"""

    def __getattr__(self, attr):
        return Name('/' + attr)

    def __setattr__(self, name, value):
        raise TypeError("Attributes may not be set on pikepdf.Name")

    def __getitem__(self, item):
        if item.startswith('/'):
            item = item[1:]
        raise TypeError(
            (
                "pikepdf.Name is not subscriptable. You probably meant:\n"
                "    pikepdf.Name.{}\n"
                "or\n"
                "    pikepdf.Name('/{}')\n"
            ).format(item, item)
        )


class Name(metaclass=_NameObjectMeta):
    """Constructs a PDF Name object

    Names can be constructed with two notations:

        1. ``Name.Resources``

        2. ``Name('/Resources')``

    The two are semantically equivalent. The former is preferred for names
    that are normally expected to be in a PDF. The latter is preferred for
    dynamic names and attributes.
    """
    object_type = ObjectType.name

    def __new__(cls, name):
        # QPDF_Name::unparse ensures that names are always saved in a UTF-8
        # compatible way, so we only need to guard the input.
        if isinstance(name, bytes):
            raise TypeError("Name should be str")
        return _qpdf._new_name(name)


class String(metaclass=_ObjectMeta):
    """Constructs a PDF String object"""
    object_type = ObjectType.string

    def __new__(cls, s):
        """
        Args:
            s (str or bytes): The string to use. String will be encoded for
                PDF, bytes will be constructed without encoding.

        Returns:
            pikepdf.Object
        """
        if isinstance(s, bytes):
            return _qpdf._new_string(s)
        return _qpdf._new_string_utf8(s)


class Array(metaclass=_ObjectMeta):
    """Constructs a PDF Array object"""
    object_type = ObjectType.array

    def __new__(cls, a=None):
        """
        Args:
            a (iterable): A list of objects. All objects must be either
                `pikepdf.Object` or convertible to `pikepdf.Object`.

        Returns:
            pikepdf.Object
        """

        if isinstance(a, (str, bytes)):
            raise TypeError('Strings cannot be converted to arrays of chars')
        if a is None:
            a = []
        return _qpdf._new_array(a)


class Dictionary(metaclass=_ObjectMeta):
    """Constructs a PDF Dictionary object"""
    object_type = ObjectType.dictionary

    def __new__(cls, d=None, **kwargs):
        """
        Constructs a PDF Dictionary from either a Python ``dict`` or keyword
        arguments.

        These two examples are equivalent:

        .. code-block:: python

            pikepdf.Dictionary({'/NameOne': 1, '/NameTwo': 'Two'})

            pikepdf.Dictionary(NameOne=1, NameTwo='Two')

        In either case, the keys must be strings, and the strings
        correspond to the desired Names in the PDF Dictionary. The values
        must all be convertible to `pikepdf.Object`.

        Returns:
            pikepdf.Object
        """
        if kwargs and d is not None:
            raise ValueError('Unsupported parameters')
        if kwargs:
            # Add leading slash
            # Allows Dictionary(MediaBox=(0,0,1,1), Type=Name('/Page')...
            return _qpdf._new_dictionary(
                {('/' + k) : v for k, v in kwargs.items()})
        if not d:
            d = {}
        return _qpdf._new_dictionary(d)


class Stream(metaclass=_ObjectMeta):
    """Constructs a PDF Stream object"""
    object_type = ObjectType.stream

    def __new__(cls, owner, obj):
        """
        Args:
            owner (pikepdf.Pdf): The Pdf to which this stream shall be attached.
            obj (bytes or list): If ``bytes``, the data bytes for the stream.
                If ``list``, a list of ``(operands, operator)`` tuples such
                as returned by :func:`pikepdf.parse_content_stream`.

        Returns:
            pikepdf.Object
        """
        return _qpdf._new_stream(owner, obj)