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/decoder.py ADDED
@@ -0,0 +1,398 @@
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
+ """Decoder classes."""
16
+
17
+ import base64
18
+ import json
19
+ import numpy as np
20
+ import png
21
+ import queue
22
+ import zlib
23
+
24
+ from . import color
25
+ from . import exceptions
26
+
27
+
28
+ class BaseDecoder:
29
+ """Abstract decoder class."""
30
+
31
+ def decode(
32
+ self,
33
+ data: bytes,
34
+ palette: color.ColorPalette | None = None,
35
+ all_blank: bool = False,
36
+ horizontal: bool = False,
37
+ ):
38
+ raise NotImplementedError("subclasses must implement decode method")
39
+
40
+
41
+ class FlateDecoder(BaseDecoder):
42
+ """Decoder for SN_ASA_COMPRESS protocol."""
43
+
44
+ COLORCODE_BLACK = 0x0000
45
+ COLORCODE_BACKGROUND = 0xFFFF
46
+ COLORCODE_DARK_GRAY = 0x2104
47
+ COLORCODE_GRAY = 0xE1E2
48
+
49
+ INTERNAL_PAGE_HEIGHT = 1888
50
+ INTERNAL_PAGE_WIDTH = 1404
51
+
52
+ def decode(
53
+ self,
54
+ data,
55
+ page_width,
56
+ page_height,
57
+ palette=None,
58
+ all_blank=False,
59
+ horizontal=False,
60
+ ):
61
+ """Uncompress bitmap data.
62
+
63
+ Parameters
64
+ ----------
65
+ data : bytes
66
+ compressed bitmap data
67
+ page_width : int
68
+ page width
69
+ page_height : int
70
+ page height
71
+
72
+ Returns
73
+ -------
74
+ bytes
75
+ uncompressed bitmap data
76
+ tuple(int, int)
77
+ bitmap size (width, height)
78
+ int
79
+ bit per pixel
80
+ """
81
+ uncompressed = zlib.decompress(data)
82
+ bitmap = np.frombuffer(uncompressed, dtype=np.uint16)
83
+ bitmap = np.reshape(
84
+ bitmap, (self.INTERNAL_PAGE_WIDTH, self.INTERNAL_PAGE_HEIGHT)
85
+ )
86
+ bitmap = np.rot90(bitmap, -1) # rotate 90 degrees clockwise
87
+ bitmap = np.delete(bitmap, slice(-16, None), axis=0) # delete bottom 16 lines
88
+
89
+ # change colors
90
+ if palette is None:
91
+ palette = color.DEFAULT_COLORPALETTE
92
+ if palette.mode == color.MODE_RGB:
93
+ bit_per_pixel = 32
94
+ bitmap = bitmap.astype(">u4")
95
+ alpha = 0xFF
96
+ bitmap[bitmap == self.COLORCODE_BLACK] = (palette.black << 8) | alpha
97
+ bitmap[bitmap == self.COLORCODE_DARK_GRAY] = (palette.darkgray << 8) | alpha
98
+ bitmap[bitmap == self.COLORCODE_GRAY] = (palette.gray << 8) | alpha
99
+ bitmap[bitmap == self.COLORCODE_BACKGROUND] = (palette.white << 8) | alpha
100
+ else:
101
+ bit_per_pixel = 8
102
+ bitmap[bitmap == self.COLORCODE_BLACK] = palette.black
103
+ bitmap[bitmap == self.COLORCODE_DARK_GRAY] = palette.darkgray
104
+ bitmap[bitmap == self.COLORCODE_GRAY] = palette.gray
105
+ bitmap[bitmap == self.COLORCODE_BACKGROUND] = palette.white
106
+ bitmap = bitmap.astype(np.uint8)
107
+ return bitmap.tobytes(), (page_width, page_height), bit_per_pixel
108
+
109
+
110
+ class RattaRleDecoder(BaseDecoder):
111
+ """Decoder for RATTA_RLE protocol."""
112
+
113
+ COLORCODE_BLACK = 0x61
114
+ COLORCODE_BACKGROUND = 0x62
115
+ COLORCODE_DARK_GRAY = 0x63
116
+ COLORCODE_GRAY = 0x64
117
+ COLORCODE_WHITE = 0x65
118
+ COLORCODE_MARKER_BLACK = 0x66
119
+ COLORCODE_MARKER_DARK_GRAY = 0x67
120
+ COLORCODE_MARKER_GRAY = 0x68
121
+
122
+ SPECIAL_LENGTH_MARKER = 0xFF
123
+ SPECIAL_LENGTH = 0x4000
124
+ SPECIAL_LENGTH_FOR_BLANK = 0x400
125
+
126
+ def decode(
127
+ self,
128
+ data,
129
+ page_width,
130
+ page_height,
131
+ palette=None,
132
+ all_blank=False,
133
+ horizontal=False,
134
+ ):
135
+ """Uncompress bitmap data.
136
+
137
+ Parameters
138
+ ----------
139
+ data : bytes
140
+ compressed bitmap data
141
+ page_width : int
142
+ page width
143
+ page_height : int
144
+ page height
145
+
146
+ Returns
147
+ -------
148
+ bytes
149
+ uncompressed bitmap data
150
+ tuple(int, int)
151
+ bitmap size (width, height)
152
+ int
153
+ bit per pixel
154
+ """
155
+ if palette is None:
156
+ palette = color.DEFAULT_COLORPALETTE
157
+
158
+ if palette.mode == color.MODE_RGB:
159
+ bit_per_pixel = 24
160
+ else:
161
+ bit_per_pixel = 8
162
+
163
+ colormap = self._create_colormap(palette)
164
+
165
+ if horizontal:
166
+ page_height, page_width = (page_width, page_height) # swap width and height
167
+
168
+ expected_length = page_height * page_width * int(bit_per_pixel / 8)
169
+
170
+ uncompressed = bytearray()
171
+ bin = iter(data)
172
+ try:
173
+ holder = ()
174
+ waiting = queue.Queue()
175
+ while True:
176
+ colorcode = next(bin)
177
+ length = next(bin)
178
+ data_pushed = False
179
+
180
+ if len(holder) > 0:
181
+ (prev_colorcode, prev_length) = holder
182
+ holder = ()
183
+ if colorcode == prev_colorcode:
184
+ length = 1 + length + (((prev_length & 0x7F) + 1) << 7)
185
+ waiting.put((colorcode, length))
186
+ data_pushed = True
187
+ else:
188
+ prev_length = ((prev_length & 0x7F) + 1) << 7
189
+ waiting.put((prev_colorcode, prev_length))
190
+
191
+ if not data_pushed:
192
+ if length == self.SPECIAL_LENGTH_MARKER:
193
+ if all_blank:
194
+ length = self.SPECIAL_LENGTH_FOR_BLANK
195
+ else:
196
+ length = self.SPECIAL_LENGTH
197
+ waiting.put((colorcode, length))
198
+ data_pushed = True
199
+ elif length & 0x80 != 0:
200
+ holder = (colorcode, length)
201
+ # holded data are processed at next loop
202
+ else:
203
+ length += 1
204
+ waiting.put((colorcode, length))
205
+ data_pushed = True
206
+
207
+ while not waiting.empty():
208
+ (colorcode, length) = waiting.get()
209
+ uncompressed += self._create_color_bytearray(
210
+ palette.mode, colormap, colorcode, length
211
+ )
212
+ except StopIteration:
213
+ if len(holder) > 0:
214
+ (colorcode, length) = holder
215
+ length = self._adjust_tail_length(
216
+ length, len(uncompressed), expected_length
217
+ )
218
+ if length > 0:
219
+ uncompressed += self._create_color_bytearray(
220
+ palette.mode, colormap, colorcode, length
221
+ )
222
+
223
+ if len(uncompressed) != expected_length:
224
+ raise exceptions.DecoderException(
225
+ f"uncompressed bitmap length = {len(uncompressed)}, expected = {expected_length}"
226
+ )
227
+
228
+ return bytes(uncompressed), (page_width, page_height), bit_per_pixel
229
+
230
+ def _create_colormap(self, palette):
231
+ colormap = {
232
+ self.COLORCODE_BLACK: palette.black,
233
+ self.COLORCODE_BACKGROUND: palette.transparent,
234
+ self.COLORCODE_DARK_GRAY: palette.darkgray,
235
+ self.COLORCODE_GRAY: palette.gray,
236
+ self.COLORCODE_WHITE: palette.white,
237
+ self.COLORCODE_MARKER_BLACK: palette.black,
238
+ self.COLORCODE_MARKER_DARK_GRAY: palette.darkgray,
239
+ self.COLORCODE_MARKER_GRAY: palette.gray,
240
+ }
241
+ return colormap
242
+
243
+ def _create_color_bytearray(self, mode, colormap, color_code, length):
244
+ if mode == color.MODE_RGB:
245
+ c = colormap.get(color_code)
246
+ r, g, b = color.get_rgb(c)
247
+ return (
248
+ bytearray(
249
+ (
250
+ r,
251
+ g,
252
+ b,
253
+ )
254
+ )
255
+ * length
256
+ )
257
+ else:
258
+ c = colormap.get(color_code)
259
+ return bytearray((c,)) * length
260
+
261
+ def _adjust_tail_length(self, tail_length, current_length, total_length):
262
+ gap = total_length - current_length
263
+ for i in reversed(range(8)):
264
+ length = ((tail_length & 0x7F) + 1) << i
265
+ if length <= gap:
266
+ return length
267
+ return 0
268
+
269
+
270
+ class RattaRleX2Decoder(RattaRleDecoder):
271
+ """Decoder for RATTA_RLE protocol of X2-series."""
272
+
273
+ # 4 color codes were changed from X-series
274
+ COLORCODE_DARK_GRAY = 0x9D
275
+ COLORCODE_GRAY = 0xC9
276
+ COLORCODE_MARKER_DARK_GRAY = 0x9E
277
+ COLORCODE_MARKER_GRAY = 0xCA
278
+ # color codes for X-series compatibility
279
+ COLORCODE_DARK_GRAY_COMPAT = 0x63
280
+ COLORCODE_GRAY_COMPAT = 0x64
281
+
282
+ def _create_colormap(self, palette):
283
+ colormap = {
284
+ self.COLORCODE_BLACK: palette.black,
285
+ self.COLORCODE_BACKGROUND: palette.transparent,
286
+ self.COLORCODE_DARK_GRAY: palette.darkgray,
287
+ self.COLORCODE_GRAY: palette.gray,
288
+ self.COLORCODE_WHITE: palette.white,
289
+ self.COLORCODE_MARKER_BLACK: palette.black,
290
+ self.COLORCODE_MARKER_DARK_GRAY: palette.darkgray,
291
+ self.COLORCODE_MARKER_GRAY: palette.gray,
292
+ self.COLORCODE_DARK_GRAY_COMPAT: palette.darkgray_compat,
293
+ self.COLORCODE_GRAY_COMPAT: palette.gray_compat,
294
+ }
295
+ return colormap
296
+
297
+ def _create_color_bytearray(self, mode, colormap, color_code, length):
298
+ if mode == color.MODE_RGB:
299
+ c = colormap.get(color_code)
300
+ if c is not None:
301
+ r, g, b = color.get_rgb(c)
302
+ else: # if the color_code is not included in colormap, use the value as color directly
303
+ r, g, b = (color_code, color_code, color_code)
304
+ return (
305
+ bytearray(
306
+ (
307
+ r,
308
+ g,
309
+ b,
310
+ )
311
+ )
312
+ * length
313
+ )
314
+ else:
315
+ c = colormap.get(color_code)
316
+ if c is None:
317
+ c = color_code
318
+ return bytearray((c,)) * length
319
+
320
+
321
+ class PngDecoder(BaseDecoder):
322
+ """Decoder for PNG."""
323
+
324
+ def decode(
325
+ self,
326
+ data,
327
+ page_width,
328
+ page_height,
329
+ palette=None,
330
+ all_blank=False,
331
+ horizontal=False,
332
+ ):
333
+ """Uncompress bitmap data.
334
+
335
+ Parameters
336
+ ----------
337
+ data : bytes
338
+ png data
339
+ page_width : int
340
+ page width
341
+ page_height : int
342
+ page height
343
+
344
+ Returns
345
+ -------
346
+ bytes
347
+ uncompressed bitmap data
348
+ tuple(int, int)
349
+ bitmap size (width, height)
350
+ int
351
+ bit per pixel
352
+ """
353
+ r = png.Reader(bytes=data)
354
+ (width, height, rows, info) = r.asRGBA()
355
+ if width != page_width or height != page_height:
356
+ raise exceptions.DecoderException(
357
+ f"invalid size = ({width}, {height}), expected = ({page_width}, {page_height})"
358
+ )
359
+ values = [x for row in rows for x in row] # flatten rows
360
+ depth = info["bitdepth"]
361
+ greyscale = info["greyscale"]
362
+ alpha = info["alpha"]
363
+ ch = 1 if greyscale else 3
364
+ if alpha:
365
+ ch = ch + 1
366
+ bit_per_pixel = depth * ch
367
+ return bytes(values), (page_width, page_height), bit_per_pixel
368
+
369
+
370
+ class TextDecoder(BaseDecoder):
371
+ """Decoder for text."""
372
+
373
+ def decode(
374
+ self, data: bytes, palette=None, all_blank=False, horizontal=False
375
+ ) -> list[str] | None:
376
+ """Extract text from a realtime recognition data.
377
+
378
+ Parameters
379
+ ----------
380
+ data : bytes
381
+ recognition text data (base64 encoded)
382
+
383
+ Returns
384
+ -------
385
+ list of string
386
+ list of recognized text
387
+ """
388
+ if data is None:
389
+ return None
390
+ recogn_json = base64.b64decode(data).decode("utf-8")
391
+ recogn = json.loads(recogn_json)
392
+ elements = recogn.get("elements")
393
+ return list(
394
+ map(
395
+ lambda e: e.get("label"),
396
+ filter(lambda e: e.get("type") == "Text", elements),
397
+ )
398
+ )
@@ -0,0 +1,43 @@
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
+ """Library-specific exception classes."""
16
+
17
+
18
+ class SupernoteLibException(Exception):
19
+ """Base class of all Supernote library exceptions."""
20
+
21
+
22
+ class ParserException(SupernoteLibException):
23
+ """Base class of parser exceptions."""
24
+
25
+
26
+ class UnsupportedFileFormat(ParserException):
27
+ """Raised if file format is unsupported."""
28
+
29
+
30
+ class DecoderException(SupernoteLibException):
31
+ """Base class of decoder exceptions."""
32
+
33
+
34
+ class UnknownDecodeProtocol(DecoderException):
35
+ """Raised if decode protocol is unknown."""
36
+
37
+
38
+ class ManipulatorException(SupernoteLibException):
39
+ """Base class of manipulator exceptions."""
40
+
41
+
42
+ class GeneratedFileValidationException(ManipulatorException):
43
+ """Raised if generated file is invalid."""