1#!/usr/local/bin/python3.8
2# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
3
4
5__license__   = 'GPL v3'
6__copyright__ = '2012, Kovid Goyal <kovid at kovidgoyal.net>'
7__docformat__ = 'restructuredtext en'
8
9import os
10
11from calibre.ebooks.pdf.render.common import Array, Name, Dictionary, String, UTF16String, current_log
12from polyglot.builtins import iteritems
13from polyglot.urllib import unquote, urlparse
14
15
16class Destination(Array):
17
18    def __init__(self, start_page, pos, get_pageref):
19        pnum = start_page + max(0, pos['column'])
20        q = pnum
21        while q > -1:
22            try:
23                pref = get_pageref(q)
24                break
25            except IndexError:
26                pos['left'] = pos['top'] = 0
27                q -= 1
28        if q != pnum:
29            current_log().warn('Could not find page {} for link destination, using page {} instead'.format(pnum, q))
30        super().__init__([
31            pref, Name('XYZ'), pos['left'], pos['top'], None
32        ])
33
34
35class Links:
36
37    def __init__(self, pdf, mark_links, page_size):
38        self.anchors = {}
39        self.links = []
40        self.start = {'top':page_size[1], 'column':0, 'left':0}
41        self.pdf = pdf
42        self.mark_links = mark_links
43
44    def add(self, base_path, start_page, links, anchors):
45        path = os.path.normcase(os.path.abspath(base_path))
46        self.anchors[path] = a = {}
47        a[None] = Destination(start_page, self.start, self.pdf.get_pageref)
48        for anchor, pos in iteritems(anchors):
49            a[anchor] = Destination(start_page, pos, self.pdf.get_pageref)
50        for link in links:
51            href, page, rect = link
52            p, frag = href.partition('#')[0::2]
53            try:
54                pref = self.pdf.get_pageref(page).obj
55            except IndexError:
56                try:
57                    pref = self.pdf.get_pageref(page-1).obj
58                except IndexError:
59                    self.pdf.debug('Unable to find page for link: %r, ignoring it' % link)
60                    continue
61                self.pdf.debug('The link %s points to non-existent page, moving it one page back' % href)
62            self.links.append(((path, p, frag or None), pref, Array(rect)))
63
64    def add_links(self):
65        for link in self.links:
66            path, href, frag = link[0]
67            page, rect = link[1:]
68            combined_path = os.path.normcase(os.path.abspath(os.path.join(os.path.dirname(path), *unquote(href).split('/'))))
69            is_local = not href or combined_path in self.anchors
70            annot = Dictionary({
71                'Type':Name('Annot'), 'Subtype':Name('Link'),
72                'Rect':rect, 'Border':Array([0,0,0]),
73            })
74            if self.mark_links:
75                annot.update({'Border':Array([16, 16, 1]), 'C':Array([1.0, 0,
76                                                                      0])})
77            if is_local:
78                path = combined_path if href else path
79                try:
80                    annot['Dest'] = self.anchors[path][frag]
81                except KeyError:
82                    try:
83                        annot['Dest'] = self.anchors[path][None]
84                    except KeyError:
85                        pass
86            else:
87                url = href + (('#'+frag) if frag else '')
88                try:
89                    purl = urlparse(url)
90                except Exception:
91                    self.pdf.debug('Ignoring unparsable URL: %r' % url)
92                    continue
93                if purl.scheme and purl.scheme != 'file':
94                    action = Dictionary({
95                        'Type':Name('Action'), 'S':Name('URI'),
96                    })
97                    # Do not try to normalize/quote/unquote this URL as if it
98                    # has a query part, it will get corrupted
99                    action['URI'] = String(url)
100                    annot['A'] = action
101            if 'A' in annot or 'Dest' in annot:
102                if 'Annots' not in page:
103                    page['Annots'] = Array()
104                page['Annots'].append(self.pdf.objects.add(annot))
105            else:
106                self.pdf.debug('Could not find destination for link: %s in file %s'%
107                               (href, path))
108
109    def add_outline(self, toc):
110        parent = Dictionary({'Type':Name('Outlines')})
111        parentref = self.pdf.objects.add(parent)
112        self.process_children(toc, parentref, parent_is_root=True)
113        self.pdf.catalog.obj['Outlines'] = parentref
114
115    def process_children(self, toc, parentref, parent_is_root=False):
116        childrefs = []
117        for child in toc:
118            childref = self.process_toc_item(child, parentref)
119            if childref is None:
120                continue
121            if childrefs:
122                childrefs[-1].obj['Next'] = childref
123                childref.obj['Prev'] = childrefs[-1]
124            childrefs.append(childref)
125
126            if len(child) > 0:
127                self.process_children(child, childref)
128        if childrefs:
129            parentref.obj['First'] = childrefs[0]
130            parentref.obj['Last'] = childrefs[-1]
131            if not parent_is_root:
132                parentref.obj['Count'] = -len(childrefs)
133
134    def process_toc_item(self, toc, parentref):
135        path = toc.abspath or None
136        frag = toc.fragment or None
137        if path is None:
138            return
139        path = os.path.normcase(os.path.abspath(path))
140        if path not in self.anchors:
141            return None
142        a = self.anchors[path]
143        dest = a.get(frag, a[None])
144        item = Dictionary({'Parent':parentref, 'Dest':dest,
145                           'Title':UTF16String(toc.text or _('Unknown'))})
146        return self.pdf.objects.add(item)
147