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/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
|
+
)
|
supernote/exceptions.py
ADDED
|
@@ -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."""
|