class PdfParseError(PdfError):
    """Exception parsing PDF file"""
    pass class PdfArray(PdfType, UserList):
    """PDF list type"""
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        UserList.__init__(self, *args, **kwargs)
    def pdf_encode(self):
        return b'['+b' '.join(i.pdf_encode() for i in self)+b']' class PdfDict(PdfType, UserDict):
    """PDF dict type"""
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        UserDict.__init__(self, *args, **kwargs)

    def __getattr__(self, name):
        try:
            return self[name].parsed_object
        except AttributeError:
            return self[name]
        except KeyError:
            raise AttributeError('Object has no attribute "{}"'.format(name))
    def pdf_encode(self):
        return b'<<'+b' '.join(k.pdf_encode()+b' '+v.pdf_encode()
                               for k, v in six.iteritems(self))+b'>>' class PdfObjectReference(PdfType):
    """PDF indirect object reference"""
    def __init__(self, object_number, generation, document=None):
        super(PdfObjectReference, self).__init__()
        self._object_number = object_number
        self._generation    = generation
        self._document      = document

        if   not isinstance(self._object_number, int) \
          or not isinstance(self._generation,    int) \
          or self._object_number <= 0 or self._generation < 0:
            raise ValueError('Invalid indirect object identifier')

    def get_object(self, document=None):
        if not document and not self._document:
            raise PdfError('Evaluating indirect references requires a document')
        obj_id = (self._object_number, self._generation)
        return (document if document else self._document).get_object(*obj_id)
    @property
    def value(self):
        return self.get_object().value
    @property
    def parsed_object(self):
        return self.get_object().parsed_object

    def __str__(self):
        return 'PdfObjectReference({0}, {1})'.format(self._object_number,
                                                     self._generation)
    def __repr__(self):
        return str(self)
    def pdf_encode(self):
        return '{0} {1} R'.format(self._object_number,
                                  self._generation).encode() class PdfIndirectObject(PdfType):
    """PDF indirect object definition"""
    def __init__(self, object_number, generation, obj, document):
        super(PdfIndirectObject, self).__init__()
        self._object_number = object_number
        self._generation    = generation
        self._object        = obj
        self._document      = document
        self._parsed_obj    = None

    @property
    def object_key(self):
        return (self._object_number, self._generation)
    @property
    def value(self):
        return self._object
    @property
    def parsed_object(self):
        """The PdfElement corresponding to the object

        TODO: Move most of this somewhere more sane."""
        from .. import pdf_elements
        obj_types = {'Page'          : pdf_elements.PdfPage,
                     'Pages'         : pdf_elements.PdfPageNode,
                     'Font'          : pdf_elements.PdfFont,
                     #'XObject'       : PdfXObject,   #TODO
                     'FontDescriptor': pdf_elements.FontDescriptor,
                     'Encoding'      : pdf_elements.FontEncoding,
                     #'ObjStm'        : ObjectStream, #TODO
                     #'XRef'          : XRef
                     'Catalog'       : pdf_elements.PdfCatalog
                    }
        if self._parsed_obj is not None:
            return self._parsed_obj
        val = self.value
        if isinstance(val, PdfDict):
            try:
                self._parsed_obj = obj_types[val['Type']]\
                                          .from_object(val, self.object_key)
                return self._parsed_obj
            except KeyError:
                return val
        return val class PdfLiteralString(str, PdfString):
    """PDF Literal strings"""
    def __new__(cls, data):
        try:
            string = cls._decode_bytes(cls.parse_bytes(data))
        except UnicodeDecodeError:
            string = codecs.encode(cls.parse_bytes(data), 'hex_codec').decode()
        obj = str.__new__(cls, string)
        obj.__init__(data)
        return obj

    def __init__(self, data):
        PdfString.__init__(self, data)
        str.__init__(self)

    @staticmethod
    def _decode_bytes(data):
        """Detect the encoding method and return the decoded string"""
        # Are we UTF-16BE?  Good.
        if data[:2] == '\xFE\xFF':
            return data.decode('utf_16_be')
        # If the string isn't UTF-16BE, it follows PDF standard encoding
        # described in Appendix D of the reference.
        return data.decode('pdf_doc')

    ESCAPES = {b'n'   : b'\n',
               b'r'   : b'\r',
               b't'   : b'\t',
               b'b'   : b'\b',
               b'f'   : b'\f',
               b'('   : b'(',
               b')'   : b')',
               b'\n'  : b'',
               b'\r'  : b''}

    @staticmethod
    def _parse_escape(data):
        r"""Handle escape sequences in literal PDF strings.  This should be
        pretty straightforward except that there are line continuations, so
        \\n, \\r\n, and \\r are ignored. Moreover, actual newline characters
        are also valid.  It's very stupid. See pp. 53-56 in the Reference if
        you want to be annoyed.

        Arguments:
            data - io.BytesIO-like object

        Returns the unescaped bytes"""
        e_str = data.read(1)
        try:
            val = PdfLiteralString.ESCAPES[e_str]
        except KeyError:
            # Not a normal escape, hopefully it's an octal
            if not e_str.isdigit():
                print(e_str)
                raise PdfParseError('Invalid escape sequence in literal string')
        else:
            # Handle \\r\n by skipping the next character
            if e_str == b'\r' and data.peek(1)[:1] == b'\n':
                data.seek(1,1)
            return val
        # If it's not one of the above, it must be an octal of
        # length at most 3
        for i in range(2):
            e_str += data.read(1)
            if not e_str.isdigit():
                data.seek(-1, 1)
                return bytes((min(int(e_str[:-1], 8),255),))
        return bytes((min(int(e_str, 8),255),))

    @staticmethod
    def parse_bytes(data):
        """Extract a PDF escaped string into a nice python bytes object."""

        iodata  = io.BufferedReader(io.BytesIO(data))
        result  = io.BytesIO()
        char = iodata.read(1)
        while char:
            if char == b'\\':
                result.write(PdfLiteralString._parse_escape(iodata))
            else:
                result.write(char)
            char = iodata.read(1)
        return bytes(result.getvalue()) class PdfHexString(PdfString):
    """Hex strings, mostly used for ID values"""
    def __init__(self, data):
        super(PdfHexString, self).__init__(data)
    @property
    def _text(self):
        return '0x'+binascii.hexlify(self._parsed_bytes).decode()
    @staticmethod
    def parse_bytes(token):
        hstr = token.decode()
        if len(hstr) % 2:
            hstr += '0'
        return codecs.decode(hstr, 'hex_codec')
    def __repr__(self):
        return str(self)
    def __str__(self):
        return self._text
    def pdf_encode(self):
        return b'<'+self.raw_bytes+b'>' class PdfName(PdfType, str):
    """PDF name objects, mostly use for dict keys"""
    def __new__(cls, *args, **kwargs):
        return str.__new__(cls, *args, **kwargs)
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        str.__init__(self)
    @classmethod
    def from_token(cls, token):
        """Parse names by stripping the leading / and replacing instances of
        #YY with the character b'\\xYY' and decoding to unicode."""
        try:
            name = bytearray(token[token[0] == '/':].encode())
        except AttributeError:
            # Hopefully this means that it's bytes
            name = bytearray(token[token[:1] == b'/':])
        hash_pos = name.find(b'#')
        while hash_pos > 0:
            try:
                new_char = bytes((int(name[hash_pos+1:hash_pos+3], 16),))
            except ValueError:
                msg = 'Invalid hex code in name {} ({})'
                raise PdfError(msg.format(token, name[hash_pos:hash_pos+3]))
            name[hash_pos:hash_pos+3] = new_char
            hash_pos = name.find(b'#', hash_pos)
        return cls(bytes(name).decode()) class PdfComment(PdfType, str):
    """Comments"""
    def __new__(cls, obj):
        if isinstance(obj, (bytes, bytearray)):
            obj = obj.decode(errors='surrogateescape')
        return str.__new__(cls, obj)
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        str.__init__(self)
    def pdf_encode(self):
        return b'%' + bytes(self.parsed_object) class PdfStream(PdfType):
    """PDF stream type"""
    def __init__(self, header, data):
        super(PdfStream, self).__init__()
        self._header  = header
        self._objects = None

        # This is obnoxious, but the PDF standard allows the stream header to
        # to specify another file with the data, ignoring the stream data.
        # Also, for some reason, some header keys change when that happens.
        try:
            with open(header['F'], 'rb') as f:
                data = f.read()
        except KeyError:
            self._data     = data
            self._filedata = False
        else:
            self._filedata = True

        if self._filter_key in header:
            self._decoded  = False
        else:
            self._decoded  = True
            self._decoded_data = self._data
    @property
    def header(self):
        """Stream header"""
        return self._header
    @property
    def _filter_key(self):
        return 'FFilter'      if self._filedata else 'Filter'
    @property
    def _params_key(self):
        return 'FDecodeParms' if self._filedata else 'DecodeParms'

    def decode(self):
        """Decode the data in the stream by sequentially applying the
        filters with their parameters"""
        if self._decoded:
            return self._decoded_data
        # Need to use self._filter_key because, for some reason beyond my
        # grasp, the key changes when the stream data is external
        # Also, since these may be lists, let's make that happen
        filters = ensure_list(self._header.get(self._filter_key, []))
        params  = ensure_list(self._header.get(self._params_key, []))
        if not params:
            params = [{} for f in filters]
        composed_filters = chain_funcs((partial(StreamFilter[f].decode, **p)
                                        for f, p in zip(filters, params)))
        decoded_data = composed_filters(self._data)
        self._decoded      = True
        self._decoded_data = decoded_data
        return self._decoded_data
    @property
    def data(self):
        return self.decode() class PdfRaw(PdfType, bytes):
    """Raw PDF token"""
    def __new__(cls, *args, **kwargs):
        val =  bytes.__new__(cls, *args, **kwargs)
        val.__init__(*args, **kwargs)
        return val
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        bytes.__init__(self)
    def pdf_encode(self):
        return self class PdfRawData(PdfType, bytes):
    """Raw, unparsed PDF data that should be treated as data, not a token."""
    def __new__(cls, *args, **kwargs):
        val =  bytes.__new__(cls, *args, **kwargs)
        val.__init__(*args, **kwargs)
        return val
    def __init__(self, *args, **kwargs):
        PdfType.__init__(self)
        bytes.__init__(self)
    def pdf_encode(self):
        return self class BlackHole(object):
    """The ultimate NOP object.  Stick it just about anywhere,
    and it will successfully do nothing."""
    def __get__(self, instance, owner):  return BlackHole()
    def __set__(self, instance, value):  pass
    def __delete__(self, instance):      pass
    def __getitem__(self, key):          return BlackHole()
    def __setitem__(self, key, value):   pass
    def __delitem__(self, key):          pass
    def __contains__(self, item):        return False
    def __iter__(self):                  return iter([])
    def __next__(self):                  raise StopIteration
    def __bool__(self):                  return False
    def __repr__(self):                  return ''
    def __str__(self):                   return ''
    def __bytes__(self):                 return b''
    def __len__(self):                   return 0
    def __int__(self):                   return 0
    def __float__(self):                 return 0.0
    def __complex__(self):               return complex()
    def __round__(self, n=0):            return round(self.float, n)
    def __call__(self, *args, **kwargs): pass
    def __add__(self, other):            return other
    def __sub__(self, other):            return other
    def __mul__(self, other):            return other
    def __matmul__(self, other):         return other
    def __truediv__(self, other):        return other
    def __floordiv__(self, other):       return other
    def __mod__(self, other):            return other
    def __divmod__(self, other):         return other
    def __pow__(self, other):            return other
    def __lshift__(self, other):         return other
    def __rshift__(self, other):         return other
    def __and__(self, other):            return other
    def __xor__(self, other):            return other
    def __or__(self, other):             return other
    def __getattr__(self, name):         return BlackHole()
    def __setattr__(self, name, value):  pass def buffer_data(data):
    """Wrap the data in a BufferedReader if we need to."""
    if _is_buffered_bytesio(data):
        return data
    elif isinstance(data, io.BytesIO):
        return io.BufferedReader(data)
    elif isinstance(data, (bytes, bytearray)):
        return io.BufferedReader(io.BytesIO(data))
    else:
        try:
            return io.BufferedReader(io.BytesIO(bytes(data)))
        except TypeError:
            raise TypeError('Data to be parsed must be either bytes, '
                            'bytesarray, or a read()able stream.') def consume_whitespace(data, whitespace=WHITESPACE):
    """Reads buffered io object data until a non-whitespace byte is encountered
    and leaves the file position at that character."""
    whitespace = set(whitespace)
    for c in whitespace:
        break
    while c and c in whitespace:
        c = data.read(1)
    if c and c not in whitespace:
        data.seek(-1, 1) # Rewind 1 byte EOLS       = frozenset((b'\r', b'\n', b'\r\n')) WHITESPACE = frozenset((b' ', b'\t', b'\r', b'\n', b'\f', b'\x00'))
import io
from .exc           import PdfParseError
from .pdf_types     import PdfRaw, PdfRawData, PdfDict, PdfObjectReference,\
                           PdfLiteralString, PdfHexString, PdfComment, \
                           PdfIndirectObject, PdfArray, PdfName, PdfStream
from .misc          import BlackHole, buffer_data, consume_whitespace
from .pdf_constants import EOLS, WHITESPACE
        from .pdf_doc import PdfDocument
            token = self._get_next_token(data, disallowed=disallowed)
            if not token: continue
            attrs.append(self._process_token(data,token,BlackHole, True))
        yield PdfDict({attrs[i]:attrs[i+1] for i in range(0,len(attrs)-1,2)})
        data.read(1)
        img = io.BytesIO()
        buf = bytes(2)
        while buf != b'EI':
            buf = buf[1:]+data.read(1)
            img.write(buf[1:])
        yield PdfRawData(img.getvalue()[:-2]) # This is such an ugly hack
        yield PdfRaw(b'EI')

    @staticmethod
    def _peek(data, n=1):
        """Peek ahead, returning the requested number of characters.  If peek()
        doesn't yield enough data, read and backup."""
        if n <= 0: return b''
        res = data.peek(n)[:n]
        if len(res) == n:
            return res
        res += data.read(n-len(res))
        data.seek(-len(res), 1)
        return res

    @classmethod
    def _get_next_token(cls, data, closer=None, disallowed=frozenset()):
        """Get the next token in the stream, data.  Closer is an optional
        argument specifying the ending token of the current data structure,
        e.g., >> for dicts."""
        clen  = len(closer) if closer is not None else None
        token = io.BytesIO()
        consume_whitespace(data, WHITESPACE)

        while data.peek(1) and (token.getvalue() != closer) \
           and not cls._is_token(data, token.getvalue(),
                                 closer, clen, disallowed):
            token.write(data.read(1))
        return token.getvalue()

    @classmethod
    def _is_token(cls, data, value, closer=None, clen=None,
                  disallowed=frozenset()):
        """Is this a token?"""
        if closer and not clen:
            clen = len(closer)

        if   not data.peek(1): return True
        elif not value:        return False

        next_char = cls._peek(data, 1)
        not_obj   = (value+next_char) not in cls.obj_types

        if value in cls.obj_types and not_obj and value not in disallowed:
            return True
        elif closer and cls._peek(data, clen) == closer \
            and value+cls._peek(data, clen-len(value)) != closer: #Last clause covers an issue with
            return True                                       #a dict as the last element of a dict
        elif next_char in cls.ENDERS and not_obj:
            return True
        return False

    def _process_token(self, data, token, objects, allow_invalid=False):
        """Process the data at the current position in the stream data into the
        data type indicated by token.

        Optional arguments:
            objects       - A list of objects already known.
            allow_invalid - Don't raise an exception when an invalid token is
                            encountered, instead returning a PdfRaw object."""
        try:
            return self.obj_types[token](self, data, objects)
        except KeyError:
            try:
                return self.parse_literal(token)
            except PdfParseError:
                #This lets us use this parse Content Streams
                if allow_invalid: return PdfRaw(token)
                else:             raise

    def parse_reference(self, data, objects):
        """References an indirect object, which may or may not have already
        been defined."""
        generation = objects.pop()
        obj_no     = objects.pop()
        return PdfObjectReference(obj_no, generation, self._doc)

    def parse_dict(self, data, objects):
        """A dict is just represented as a differently delimited array, so
        we'll call that to get the elements"""
        elems = self.parse_array(data, objects, b'>>')
        return PdfDict(zip(elems[::2], elems[1::2]))

    def parse_hex_string(self, data, objects):
        """Extract a PdfHexString from raw data"""
        token = io.BytesIO(data.read(1))
        token.seek(0, 2)
        while data.peek(1) and token.getvalue()[-1:] != b'>':
            token.write(data.read(1))
        return PdfHexString(token.getvalue()[:-1])

    def parse_literal_string(self, data, objects):
        """Extract a PdfLiteralString from raw data"""
        token   = io.BytesIO()
        parens  = 0
        escaped = False
        while data.peek(1):
            b = data.read(1)
            if  escaped:
                escaped = False
            elif b == b'\\':
                escaped = True
            elif b == b'(':
                parens += 1
            elif b == b')':
                if parens == 0:
                    return PdfLiteralString(token.getvalue())
                else:
                    parens -= 1
            token.write(b)

        raise PdfParseError('Unterminated string literal')