supernote 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- supernote/__init__.py +13 -0
- supernote/cmds/__init__.py +0 -0
- supernote/cmds/supernote_tool.py +327 -0
- supernote/color.py +93 -0
- supernote/converter.py +543 -0
- supernote/decoder.py +398 -0
- supernote/exceptions.py +43 -0
- supernote/fileformat.py +453 -0
- supernote/manipulator.py +423 -0
- supernote/parser.py +738 -0
- supernote/utils.py +49 -0
- supernote-0.1.0.dist-info/METADATA +34 -0
- supernote-0.1.0.dist-info/RECORD +17 -0
- supernote-0.1.0.dist-info/WHEEL +5 -0
- supernote-0.1.0.dist-info/entry_points.txt +2 -0
- supernote-0.1.0.dist-info/licenses/LICENSE +201 -0
- supernote-0.1.0.dist-info/top_level.txt +1 -0
supernote/converter.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
# Copyright (c) 2020 jya
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Converter classes."""
|
|
16
|
+
|
|
17
|
+
import base64
|
|
18
|
+
import json
|
|
19
|
+
import potrace
|
|
20
|
+
import svgwrite
|
|
21
|
+
|
|
22
|
+
from enum import Enum, auto
|
|
23
|
+
from io import BytesIO
|
|
24
|
+
|
|
25
|
+
from PIL import Image
|
|
26
|
+
|
|
27
|
+
from reportlab.lib.pagesizes import A4, portrait, landscape
|
|
28
|
+
from reportlab.pdfgen import canvas
|
|
29
|
+
|
|
30
|
+
from . import color
|
|
31
|
+
from . import decoder as Decoder
|
|
32
|
+
from . import exceptions
|
|
33
|
+
from . import fileformat
|
|
34
|
+
from . import utils
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class VisibilityOverlay(Enum):
|
|
38
|
+
DEFAULT = auto()
|
|
39
|
+
VISIBLE = auto()
|
|
40
|
+
INVISIBLE = auto()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def build_visibility_overlay(
|
|
44
|
+
background=VisibilityOverlay.DEFAULT,
|
|
45
|
+
main=VisibilityOverlay.DEFAULT,
|
|
46
|
+
layer1=VisibilityOverlay.DEFAULT,
|
|
47
|
+
layer2=VisibilityOverlay.DEFAULT,
|
|
48
|
+
layer3=VisibilityOverlay.DEFAULT,
|
|
49
|
+
):
|
|
50
|
+
return {
|
|
51
|
+
"BGLAYER": background,
|
|
52
|
+
"MAINLAYER": main,
|
|
53
|
+
"LAYER1": layer1,
|
|
54
|
+
"LAYER2": layer2,
|
|
55
|
+
"LAYER3": layer3,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ImageConverter:
|
|
60
|
+
SPECIAL_WHITE_STYLE_BLOCK_SIZE = 0x140E
|
|
61
|
+
|
|
62
|
+
def __init__(self, notebook, palette=None):
|
|
63
|
+
self.note = notebook
|
|
64
|
+
self.palette = palette
|
|
65
|
+
|
|
66
|
+
def convert(self, page_number, visibility_overlay=None):
|
|
67
|
+
"""Returns an image of the given page.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
page_number : int
|
|
72
|
+
page number to convert
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
PIL.Image.Image
|
|
77
|
+
an image object
|
|
78
|
+
"""
|
|
79
|
+
page = self.note.get_page(page_number)
|
|
80
|
+
if page.is_layer_supported():
|
|
81
|
+
highres_grayscale = self.note.supports_highres_grayscale()
|
|
82
|
+
converted_img = self._convert_layered_page(
|
|
83
|
+
page, self.palette, visibility_overlay, highres_grayscale
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
converted_img = self._convert_nonlayered_page(
|
|
87
|
+
page, self.palette, visibility_overlay
|
|
88
|
+
)
|
|
89
|
+
if (
|
|
90
|
+
visibility_overlay is not None
|
|
91
|
+
and visibility_overlay.get("BGLAYER") == VisibilityOverlay.INVISIBLE
|
|
92
|
+
):
|
|
93
|
+
converted_img = self._make_transparent(converted_img)
|
|
94
|
+
return converted_img
|
|
95
|
+
|
|
96
|
+
def _convert_nonlayered_page(
|
|
97
|
+
self, page, palette=None, visibility_overlay=None, highres_grayscale=False
|
|
98
|
+
):
|
|
99
|
+
binary = page.get_content()
|
|
100
|
+
if binary is None:
|
|
101
|
+
return Image.new(
|
|
102
|
+
"L",
|
|
103
|
+
(self.note.get_width(), self.note.get_height()),
|
|
104
|
+
color=color.TRANSPARENT,
|
|
105
|
+
)
|
|
106
|
+
decoder = self.find_decoder(page)
|
|
107
|
+
return self._create_image_from_decoder(decoder, binary, palette=palette)
|
|
108
|
+
|
|
109
|
+
def _convert_layered_page(
|
|
110
|
+
self, page, palette=None, visibility_overlay=None, highres_grayscale=False
|
|
111
|
+
):
|
|
112
|
+
default_palette = color.DEFAULT_COLORPALETTE
|
|
113
|
+
page = utils.WorkaroundPageWrapper.from_page(page)
|
|
114
|
+
imgs = {}
|
|
115
|
+
layers = page.get_layers()
|
|
116
|
+
for layer in layers:
|
|
117
|
+
layer_name = layer.get_name()
|
|
118
|
+
binary = layer.get_content()
|
|
119
|
+
if binary is None:
|
|
120
|
+
imgs[layer_name] = None
|
|
121
|
+
continue
|
|
122
|
+
binary_size = len(binary)
|
|
123
|
+
decoder = self.find_decoder(layer, highres_grayscale)
|
|
124
|
+
page_style = page.get_style()
|
|
125
|
+
all_blank = (
|
|
126
|
+
layer_name == "BGLAYER"
|
|
127
|
+
and page_style is not None
|
|
128
|
+
and page_style == "style_white"
|
|
129
|
+
and binary_size == self.SPECIAL_WHITE_STYLE_BLOCK_SIZE
|
|
130
|
+
)
|
|
131
|
+
custom_bg = (
|
|
132
|
+
layer_name == "BGLAYER"
|
|
133
|
+
and page_style is not None
|
|
134
|
+
and page_style.startswith("user_")
|
|
135
|
+
)
|
|
136
|
+
if custom_bg:
|
|
137
|
+
decoder = Decoder.PngDecoder()
|
|
138
|
+
horizontal = (
|
|
139
|
+
page.get_orientation() == fileformat.Page.ORIENTATION_HORIZONTAL
|
|
140
|
+
)
|
|
141
|
+
plt = default_palette if layer_name == "BGLAYER" else palette
|
|
142
|
+
img = self._create_image_from_decoder(
|
|
143
|
+
decoder,
|
|
144
|
+
binary,
|
|
145
|
+
palette=plt,
|
|
146
|
+
blank_hint=all_blank,
|
|
147
|
+
horizontal=horizontal,
|
|
148
|
+
)
|
|
149
|
+
imgs[layer_name] = img
|
|
150
|
+
return self._flatten_layers(page, imgs, visibility_overlay)
|
|
151
|
+
|
|
152
|
+
def _flatten_layers(self, page, imgs, visibility_overlay=None):
|
|
153
|
+
"""flatten all layers if any"""
|
|
154
|
+
|
|
155
|
+
def flatten(fg, bg):
|
|
156
|
+
mask = fg.copy().convert("L")
|
|
157
|
+
mask = mask.point(lambda x: 0 if x == color.TRANSPARENT else 1, mode="1")
|
|
158
|
+
return Image.composite(fg, bg, mask)
|
|
159
|
+
|
|
160
|
+
horizontal = page.get_orientation() == fileformat.Page.ORIENTATION_HORIZONTAL
|
|
161
|
+
page_width, page_height = (self.note.get_width(), self.note.get_height())
|
|
162
|
+
if horizontal:
|
|
163
|
+
page_height, page_width = (page_width, page_height)
|
|
164
|
+
flatten_img = Image.new(
|
|
165
|
+
"RGB", (page_width, page_height), color=color.RGB_TRANSPARENT
|
|
166
|
+
)
|
|
167
|
+
visibility = self._get_layer_visibility(page)
|
|
168
|
+
layer_order = page.get_layer_order()
|
|
169
|
+
for name in reversed(layer_order):
|
|
170
|
+
is_visible = visibility.get(name)
|
|
171
|
+
if visibility_overlay is not None:
|
|
172
|
+
overlay = visibility_overlay.get(name)
|
|
173
|
+
if overlay == VisibilityOverlay.INVISIBLE or (
|
|
174
|
+
overlay == VisibilityOverlay.DEFAULT and not is_visible
|
|
175
|
+
):
|
|
176
|
+
continue
|
|
177
|
+
else:
|
|
178
|
+
if not is_visible:
|
|
179
|
+
continue
|
|
180
|
+
img_layer = imgs.get(name)
|
|
181
|
+
if img_layer is not None:
|
|
182
|
+
if name == "BGLAYER":
|
|
183
|
+
# convert transparent to white for custom template
|
|
184
|
+
img_layer = self._whiten_transparent(img_layer)
|
|
185
|
+
flatten_img = flatten(img_layer, flatten_img)
|
|
186
|
+
return flatten_img
|
|
187
|
+
|
|
188
|
+
def _whiten_transparent(self, img):
|
|
189
|
+
img = img.convert("RGBA")
|
|
190
|
+
newImg = Image.new("RGBA", img.size, color.RGB_WHITE)
|
|
191
|
+
newImg.paste(img, mask=img)
|
|
192
|
+
return newImg
|
|
193
|
+
|
|
194
|
+
def _make_transparent(self, img):
|
|
195
|
+
transparent_img = Image.new("RGBA", img.size, (255, 255, 255, 0))
|
|
196
|
+
mask = img.copy().convert("L")
|
|
197
|
+
mask = mask.point(lambda x: 1 if x == color.TRANSPARENT else 0, mode="1")
|
|
198
|
+
img = img.convert("RGBA")
|
|
199
|
+
return Image.composite(transparent_img, img, mask)
|
|
200
|
+
|
|
201
|
+
def _create_image_from_decoder(
|
|
202
|
+
self, decoder, binary, palette=None, blank_hint=False, horizontal=False
|
|
203
|
+
):
|
|
204
|
+
page_width = self.note.get_width()
|
|
205
|
+
page_height = self.note.get_height()
|
|
206
|
+
bitmap, size, bpp = decoder.decode(
|
|
207
|
+
binary,
|
|
208
|
+
page_width,
|
|
209
|
+
page_height,
|
|
210
|
+
palette=palette,
|
|
211
|
+
all_blank=blank_hint,
|
|
212
|
+
horizontal=horizontal,
|
|
213
|
+
)
|
|
214
|
+
if bpp == 32:
|
|
215
|
+
img = Image.frombytes("RGBA", size, bitmap)
|
|
216
|
+
elif bpp == 24:
|
|
217
|
+
img = Image.frombytes("RGB", size, bitmap)
|
|
218
|
+
elif bpp == 16 and isinstance(decoder, Decoder.PngDecoder):
|
|
219
|
+
img = Image.frombytes("LA", size, bitmap)
|
|
220
|
+
elif bpp == 16:
|
|
221
|
+
img = Image.frombytes("I;16", size, bitmap)
|
|
222
|
+
else:
|
|
223
|
+
img = Image.frombytes("L", size, bitmap)
|
|
224
|
+
return img
|
|
225
|
+
|
|
226
|
+
def _get_layer_visibility(self, page):
|
|
227
|
+
visibility = {}
|
|
228
|
+
info = page.get_layer_info()
|
|
229
|
+
if info is None:
|
|
230
|
+
# pass to the process of getting visibility for mark file
|
|
231
|
+
return self._get_mark_layer_visibility(page)
|
|
232
|
+
info_array = json.loads(info)
|
|
233
|
+
for layer in info_array:
|
|
234
|
+
is_bg_layer = layer.get("isBackgroundLayer")
|
|
235
|
+
layer_id = layer.get("layerId")
|
|
236
|
+
is_main_layer = (layer_id == 0) and (not is_bg_layer)
|
|
237
|
+
is_visible = layer.get("isVisible")
|
|
238
|
+
if is_bg_layer:
|
|
239
|
+
visibility["BGLAYER"] = is_visible
|
|
240
|
+
elif is_main_layer:
|
|
241
|
+
visibility["MAINLAYER"] = is_visible
|
|
242
|
+
else:
|
|
243
|
+
visibility["LAYER" + str(layer_id)] = is_visible
|
|
244
|
+
# some old files don't include MAINLAYER info, so we set MAINLAYER visible
|
|
245
|
+
if visibility.get("MAINLAYER") is None:
|
|
246
|
+
visibility["MAINLAYER"] = True
|
|
247
|
+
return visibility
|
|
248
|
+
|
|
249
|
+
def _get_mark_layer_visibility(self, page):
|
|
250
|
+
visibility = {}
|
|
251
|
+
layers = page.get_layers()
|
|
252
|
+
for layer in layers:
|
|
253
|
+
name = layer.get_name()
|
|
254
|
+
visibility[name] = layer.get_type() == "MARK"
|
|
255
|
+
return visibility
|
|
256
|
+
|
|
257
|
+
def find_decoder(self, page, highres_grayscale=False):
|
|
258
|
+
"""Returns a proper decoder for the given page.
|
|
259
|
+
|
|
260
|
+
Parameters
|
|
261
|
+
----------
|
|
262
|
+
page : Page
|
|
263
|
+
page object
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
subclass of BaseDecoder
|
|
268
|
+
a decoder
|
|
269
|
+
"""
|
|
270
|
+
protocol = page.get_protocol()
|
|
271
|
+
if protocol == "SN_ASA_COMPRESS":
|
|
272
|
+
return Decoder.FlateDecoder()
|
|
273
|
+
elif protocol == "RATTA_RLE":
|
|
274
|
+
if highres_grayscale:
|
|
275
|
+
return Decoder.RattaRleX2Decoder()
|
|
276
|
+
else:
|
|
277
|
+
return Decoder.RattaRleDecoder()
|
|
278
|
+
else:
|
|
279
|
+
raise exceptions.UnknownDecodeProtocol(
|
|
280
|
+
f"unknown decode protocol: {protocol}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class SvgConverter:
|
|
285
|
+
def __init__(self, notebook, palette=None):
|
|
286
|
+
self.note = notebook
|
|
287
|
+
self.palette = palette if palette is not None else color.DEFAULT_COLORPALETTE
|
|
288
|
+
self.image_converter = ImageConverter(
|
|
289
|
+
notebook, palette=color.DEFAULT_COLORPALETTE
|
|
290
|
+
) # use default palette
|
|
291
|
+
|
|
292
|
+
def convert(self, page_number, visibility_overlay=None):
|
|
293
|
+
"""Returns SVG string of the given page.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
page_number : int
|
|
298
|
+
page number to convert
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
-------
|
|
302
|
+
string
|
|
303
|
+
an SVG string
|
|
304
|
+
"""
|
|
305
|
+
page = self.note.get_page(page_number)
|
|
306
|
+
horizontal = page.get_orientation() == fileformat.Page.ORIENTATION_HORIZONTAL
|
|
307
|
+
page_width, page_height = (self.note.get_width(), self.note.get_height())
|
|
308
|
+
if horizontal:
|
|
309
|
+
page_height, page_width = (page_width, page_height)
|
|
310
|
+
dwg = svgwrite.Drawing(
|
|
311
|
+
"dummy.svg", profile="full", size=(page_width, page_height)
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
bg_is_invisible = (
|
|
315
|
+
visibility_overlay is not None
|
|
316
|
+
and visibility_overlay.get("BGLAYER") == VisibilityOverlay.INVISIBLE
|
|
317
|
+
)
|
|
318
|
+
if not bg_is_invisible:
|
|
319
|
+
vo_only_bg = build_visibility_overlay(
|
|
320
|
+
background=VisibilityOverlay.VISIBLE,
|
|
321
|
+
main=VisibilityOverlay.INVISIBLE,
|
|
322
|
+
layer1=VisibilityOverlay.INVISIBLE,
|
|
323
|
+
layer2=VisibilityOverlay.INVISIBLE,
|
|
324
|
+
layer3=VisibilityOverlay.INVISIBLE,
|
|
325
|
+
)
|
|
326
|
+
bg_img = self.image_converter.convert(
|
|
327
|
+
page_number, visibility_overlay=vo_only_bg
|
|
328
|
+
)
|
|
329
|
+
buffer = BytesIO()
|
|
330
|
+
bg_img.save(buffer, format="png")
|
|
331
|
+
bg_b64str = base64.b64encode(buffer.getvalue()).decode("ascii")
|
|
332
|
+
dwg.add(
|
|
333
|
+
dwg.image(
|
|
334
|
+
"data:image/png;base64," + bg_b64str,
|
|
335
|
+
insert=(0, 0),
|
|
336
|
+
size=(page_width, page_height),
|
|
337
|
+
)
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
vo_except_bg = build_visibility_overlay(background=VisibilityOverlay.INVISIBLE)
|
|
341
|
+
img = self.image_converter.convert(page_number, visibility_overlay=vo_except_bg)
|
|
342
|
+
|
|
343
|
+
def generate_color_mask(img, c):
|
|
344
|
+
mask = img.copy().convert("L")
|
|
345
|
+
return mask.point(lambda x: 0 if x == c else 1, mode="1")
|
|
346
|
+
|
|
347
|
+
default_palette = color.DEFAULT_COLORPALETTE
|
|
348
|
+
default_color_list = [
|
|
349
|
+
default_palette.black,
|
|
350
|
+
default_palette.darkgray,
|
|
351
|
+
default_palette.gray,
|
|
352
|
+
default_palette.white,
|
|
353
|
+
]
|
|
354
|
+
user_color_list = [
|
|
355
|
+
self.palette.black,
|
|
356
|
+
self.palette.darkgray,
|
|
357
|
+
self.palette.gray,
|
|
358
|
+
self.palette.white,
|
|
359
|
+
]
|
|
360
|
+
for i, c in enumerate(default_color_list):
|
|
361
|
+
user_color = user_color_list[i]
|
|
362
|
+
mask = generate_color_mask(img, c)
|
|
363
|
+
# create a bitmap from the array
|
|
364
|
+
bmp = potrace.Bitmap(mask)
|
|
365
|
+
# trace the bitmap to a path
|
|
366
|
+
path = bmp.trace()
|
|
367
|
+
# iterate over path curves
|
|
368
|
+
if len(path) > 0:
|
|
369
|
+
svgpath = dwg.path(
|
|
370
|
+
fill=color.web_string(user_color, mode=self.palette.mode)
|
|
371
|
+
)
|
|
372
|
+
for curve in path:
|
|
373
|
+
start = curve.start_point
|
|
374
|
+
svgpath.push("M", start.x, start.y)
|
|
375
|
+
for segment in curve:
|
|
376
|
+
end = segment.end_point
|
|
377
|
+
if segment.is_corner:
|
|
378
|
+
c = segment.c
|
|
379
|
+
svgpath.push("L", c.x, c.y)
|
|
380
|
+
svgpath.push("L", end.x, end.y)
|
|
381
|
+
else:
|
|
382
|
+
c1 = segment.c1
|
|
383
|
+
c2 = segment.c2
|
|
384
|
+
svgpath.push("C", c1.x, c1.y, c2.x, c2.y, end.x, end.y)
|
|
385
|
+
svgpath.push("Z")
|
|
386
|
+
dwg.add(svgpath)
|
|
387
|
+
return dwg.tostring()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class PdfConverter:
|
|
391
|
+
def __init__(self, notebook, palette=None):
|
|
392
|
+
self.note = notebook
|
|
393
|
+
self.palette = palette
|
|
394
|
+
self.pagesize = A4
|
|
395
|
+
|
|
396
|
+
def convert(self, page_number, enable_link=False, enable_keyword=False):
|
|
397
|
+
"""Returns PDF data of the given page.
|
|
398
|
+
|
|
399
|
+
Parameters
|
|
400
|
+
----------
|
|
401
|
+
page_number : int
|
|
402
|
+
page number to convert
|
|
403
|
+
enable_link : bool
|
|
404
|
+
enable page links and web links
|
|
405
|
+
enable_keyword : bool
|
|
406
|
+
enable page link where keyword has been identified
|
|
407
|
+
|
|
408
|
+
Returns
|
|
409
|
+
-------
|
|
410
|
+
data : bytes
|
|
411
|
+
bytes of PDF data
|
|
412
|
+
"""
|
|
413
|
+
converter = ImageConverter(self.note, self.palette)
|
|
414
|
+
renderer_class = PdfConverter.ImgPageRenderer
|
|
415
|
+
imglist = self._create_image_list(converter, page_number)
|
|
416
|
+
pdf_data = BytesIO()
|
|
417
|
+
self._create_pdf(pdf_data, imglist, renderer_class, enable_link, enable_keyword)
|
|
418
|
+
return pdf_data.getvalue()
|
|
419
|
+
|
|
420
|
+
def _create_image_list(self, converter, page_number):
|
|
421
|
+
imglist = []
|
|
422
|
+
if page_number < 0:
|
|
423
|
+
# convert all pages
|
|
424
|
+
total = self.note.get_total_pages()
|
|
425
|
+
for i in range(total):
|
|
426
|
+
img = converter.convert(i)
|
|
427
|
+
imglist.append(img)
|
|
428
|
+
else:
|
|
429
|
+
img = converter.convert(page_number)
|
|
430
|
+
imglist.append(img)
|
|
431
|
+
return imglist
|
|
432
|
+
|
|
433
|
+
def _create_pdf(self, buf, imglist, renderer_class, enable_link, enable_keyword):
|
|
434
|
+
c = canvas.Canvas(buf, pagesize=self.pagesize)
|
|
435
|
+
keywords = self.note.get_keywords()
|
|
436
|
+
for n, img in enumerate(imglist):
|
|
437
|
+
page = self.note.get_page(n)
|
|
438
|
+
pageid = page.get_pageid()
|
|
439
|
+
horizontal = (
|
|
440
|
+
page.get_orientation() == fileformat.Page.ORIENTATION_HORIZONTAL
|
|
441
|
+
)
|
|
442
|
+
pagesize = (
|
|
443
|
+
landscape(self.pagesize) if horizontal else portrait(self.pagesize)
|
|
444
|
+
)
|
|
445
|
+
c.setPageSize(pagesize)
|
|
446
|
+
renderer = renderer_class(img, pagesize)
|
|
447
|
+
renderer.draw(c)
|
|
448
|
+
if enable_keyword:
|
|
449
|
+
found = []
|
|
450
|
+
for keyword in keywords:
|
|
451
|
+
if keyword.get_page_number() == n:
|
|
452
|
+
found.append(keyword)
|
|
453
|
+
for i in found:
|
|
454
|
+
try:
|
|
455
|
+
c.bookmarkPage(pageid)
|
|
456
|
+
scaled_rect = self._calc_link_rect(
|
|
457
|
+
i.get_rect(), renderer.get_scale()
|
|
458
|
+
)
|
|
459
|
+
c.textAnnotation(i.get_keyword(), scaled_rect)
|
|
460
|
+
except Exception:
|
|
461
|
+
continue
|
|
462
|
+
if enable_link:
|
|
463
|
+
pageid = page.get_pageid()
|
|
464
|
+
if pageid is not None:
|
|
465
|
+
c.bookmarkPage(pageid)
|
|
466
|
+
self._add_links(c, n, renderer.get_scale())
|
|
467
|
+
c.showPage()
|
|
468
|
+
c.save()
|
|
469
|
+
|
|
470
|
+
def _add_links(self, cvs, page_number, scale):
|
|
471
|
+
links = self.note.get_links()
|
|
472
|
+
for link in links:
|
|
473
|
+
if link.get_page_number() != page_number:
|
|
474
|
+
continue
|
|
475
|
+
if link.get_inout() == fileformat.Link.DIRECTION_IN:
|
|
476
|
+
# ignore income link
|
|
477
|
+
continue
|
|
478
|
+
link_type = link.get_type()
|
|
479
|
+
is_internal_link = link.get_fileid() == self.note.get_fileid()
|
|
480
|
+
if link_type == fileformat.Link.TYPE_PAGE_LINK and is_internal_link:
|
|
481
|
+
tag = link.get_pageid()
|
|
482
|
+
scaled_rect = self._calc_link_rect(link.get_rect(), scale)
|
|
483
|
+
cvs.linkAbsolute("Link", tag, scaled_rect)
|
|
484
|
+
elif link_type == fileformat.Link.TYPE_WEB_LINK:
|
|
485
|
+
encoded_url = link.get_filepath()
|
|
486
|
+
url = base64.b64decode(encoded_url).decode()
|
|
487
|
+
scaled_rect = self._calc_link_rect(link.get_rect(), scale)
|
|
488
|
+
cvs.linkURL(url, scaled_rect)
|
|
489
|
+
|
|
490
|
+
def _calc_link_rect(self, rect, scale):
|
|
491
|
+
(left, top, right, bottom) = rect
|
|
492
|
+
(scale_x, scale_y) = scale
|
|
493
|
+
(w, h) = self.pagesize
|
|
494
|
+
return (
|
|
495
|
+
left * scale_x,
|
|
496
|
+
h - top * scale_y,
|
|
497
|
+
right * scale_x,
|
|
498
|
+
h - bottom * scale_y,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
class ImgPageRenderer:
|
|
502
|
+
def __init__(self, img: Image.Image, pagesize):
|
|
503
|
+
self.img = img
|
|
504
|
+
self.pagesize = pagesize
|
|
505
|
+
|
|
506
|
+
def get_scale(self):
|
|
507
|
+
(w, h) = self.pagesize
|
|
508
|
+
return (w / self.img.width, h / self.img.height)
|
|
509
|
+
|
|
510
|
+
def draw(self, cvs):
|
|
511
|
+
(w, h) = self.pagesize
|
|
512
|
+
cvs.drawInlineImage(self.img, 0, 0, width=w, height=h)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class TextConverter:
|
|
516
|
+
def __init__(self, notebook: fileformat.Notebook, palette=None) -> None:
|
|
517
|
+
self.note = notebook
|
|
518
|
+
self.palette = palette
|
|
519
|
+
|
|
520
|
+
def convert(self, page_number: int) -> str | None:
|
|
521
|
+
"""Returns text of the given page if available.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
page_number : int
|
|
526
|
+
page number to convert
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
string
|
|
531
|
+
a recognized text if available, otherwise None
|
|
532
|
+
"""
|
|
533
|
+
if not self.note.is_realtime_recognition():
|
|
534
|
+
return None
|
|
535
|
+
page = self.note.get_page(page_number)
|
|
536
|
+
if page.get_recogn_status() != fileformat.Page.RECOGNSTATUS_DONE:
|
|
537
|
+
return None
|
|
538
|
+
binary = page.get_recogn_text()
|
|
539
|
+
decoder = Decoder.TextDecoder()
|
|
540
|
+
text_list = decoder.decode(binary)
|
|
541
|
+
if text_list is None:
|
|
542
|
+
return None
|
|
543
|
+
return " ".join(text_list)
|