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/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)