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/fileformat.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
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
|
+
"""Classes for Suernote file format."""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
|
|
19
|
+
ParamsBlock = dict[str, str | list[str]]
|
|
20
|
+
|
|
21
|
+
#### Constants
|
|
22
|
+
|
|
23
|
+
PAGE_HEIGHT = 1872
|
|
24
|
+
PAGE_WIDTH = 1404
|
|
25
|
+
A5X2_PAGE_HEIGHT = 2560
|
|
26
|
+
A5X2_PAGE_WIDTH = 1920
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ADDRESS_SIZE = 4
|
|
30
|
+
LENGTH_FIELD_SIZE = 4
|
|
31
|
+
|
|
32
|
+
KEY_TYPE = "__type__"
|
|
33
|
+
KEY_SIGNATURE = "__signature__"
|
|
34
|
+
KEY_HEADER = "__header__"
|
|
35
|
+
KEY_FOOTER = "__footer__"
|
|
36
|
+
KEY_PAGES = "__pages__"
|
|
37
|
+
KEY_LAYERS = "__layers__"
|
|
38
|
+
KEY_KEYWORDS = "__keywords__"
|
|
39
|
+
KEY_TITLES = "__titles__"
|
|
40
|
+
KEY_LINKS = "__links__"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SupernoteMetadata:
|
|
44
|
+
"""Represents Supernote file structure."""
|
|
45
|
+
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.__note = {
|
|
48
|
+
KEY_TYPE: None,
|
|
49
|
+
KEY_SIGNATURE: None,
|
|
50
|
+
KEY_HEADER: None,
|
|
51
|
+
KEY_FOOTER: None,
|
|
52
|
+
KEY_PAGES: None,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def type(self) -> str | None:
|
|
57
|
+
return self.__note[KEY_TYPE]
|
|
58
|
+
|
|
59
|
+
@type.setter
|
|
60
|
+
def type(self, value: str | None):
|
|
61
|
+
self.__note[KEY_TYPE] = value
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def signature(self) -> str | None:
|
|
65
|
+
return self.__note[KEY_SIGNATURE]
|
|
66
|
+
|
|
67
|
+
@signature.setter
|
|
68
|
+
def signature(self, value: str | None):
|
|
69
|
+
self.__note[KEY_SIGNATURE] = value
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def header(self) -> dict[str, str] | None:
|
|
73
|
+
return self.__note[KEY_HEADER]
|
|
74
|
+
|
|
75
|
+
@header.setter
|
|
76
|
+
def header(self, value: dict[str, str] | None):
|
|
77
|
+
self.__note[KEY_HEADER] = value
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def footer(self) -> dict[str, str] | None:
|
|
81
|
+
return self.__note[KEY_FOOTER]
|
|
82
|
+
|
|
83
|
+
@footer.setter
|
|
84
|
+
def footer(self, value: dict[str, str] | None):
|
|
85
|
+
self.__note[KEY_FOOTER] = value
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def pages(self) -> list[ParamsBlock] | None:
|
|
89
|
+
return self.__note[KEY_PAGES]
|
|
90
|
+
|
|
91
|
+
@pages.setter
|
|
92
|
+
def pages(self, value: list[ParamsBlock] | None):
|
|
93
|
+
self.__note[KEY_PAGES] = value
|
|
94
|
+
|
|
95
|
+
def get_total_pages(self) -> int:
|
|
96
|
+
"""Returns total page number.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
int
|
|
101
|
+
total page number
|
|
102
|
+
"""
|
|
103
|
+
return len(self.__note[KEY_PAGES])
|
|
104
|
+
|
|
105
|
+
def is_layer_supported(self, page_number: int) -> bool:
|
|
106
|
+
"""Returns true if the page supports layer.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
page_number : int
|
|
111
|
+
page number to check
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
bool
|
|
116
|
+
true if the page supports layer.
|
|
117
|
+
"""
|
|
118
|
+
if page_number < 0 or page_number >= self.get_total_pages():
|
|
119
|
+
raise IndexError(f"page number out of range: {page_number}")
|
|
120
|
+
return self.__note[KEY_PAGES][page_number].get(KEY_LAYERS) is not None
|
|
121
|
+
|
|
122
|
+
def to_json(self, indent: int = None) -> str:
|
|
123
|
+
"""Returns file structure as JSON format string.
|
|
124
|
+
|
|
125
|
+
Parameters
|
|
126
|
+
----------
|
|
127
|
+
indent : int
|
|
128
|
+
optional indent level
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
str
|
|
133
|
+
JSON format string
|
|
134
|
+
"""
|
|
135
|
+
return json.dumps(self.__note, indent=indent, ensure_ascii=False)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Notebook:
|
|
139
|
+
def __init__(self, metadata: SupernoteMetadata) -> None:
|
|
140
|
+
self.metadata = metadata
|
|
141
|
+
self.page_width = PAGE_WIDTH
|
|
142
|
+
self.page_height = PAGE_HEIGHT
|
|
143
|
+
if self.metadata.header.get("APPLY_EQUIPMENT") == "N5":
|
|
144
|
+
self.page_width = A5X2_PAGE_WIDTH
|
|
145
|
+
self.page_height = A5X2_PAGE_HEIGHT
|
|
146
|
+
self.type = metadata.type
|
|
147
|
+
self.signature = metadata.signature
|
|
148
|
+
self.cover = Cover()
|
|
149
|
+
self.keywords: list[Keyword] = []
|
|
150
|
+
has_keywords = metadata.footer.get(KEY_KEYWORDS) is not None
|
|
151
|
+
if has_keywords:
|
|
152
|
+
for k in metadata.footer.get(KEY_KEYWORDS):
|
|
153
|
+
self.keywords.append(Keyword(k))
|
|
154
|
+
self.titles: list[Title] = []
|
|
155
|
+
has_titles = metadata.footer.get(KEY_TITLES) is not None
|
|
156
|
+
if has_titles:
|
|
157
|
+
for t in metadata.footer.get(KEY_TITLES):
|
|
158
|
+
self.titles.append(Title(t))
|
|
159
|
+
self.links: list[Link] = []
|
|
160
|
+
has_links = metadata.footer.get(KEY_LINKS) is not None
|
|
161
|
+
if has_links:
|
|
162
|
+
for link in metadata.footer.get(KEY_LINKS):
|
|
163
|
+
self.links.append(Link(link))
|
|
164
|
+
self.pages: list[Page] = []
|
|
165
|
+
total = metadata.get_total_pages()
|
|
166
|
+
for i in range(total):
|
|
167
|
+
self.pages.append(Page(metadata.pages[i]))
|
|
168
|
+
|
|
169
|
+
def get_metadata(self) -> SupernoteMetadata:
|
|
170
|
+
return self.metadata
|
|
171
|
+
|
|
172
|
+
def get_width(self) -> int:
|
|
173
|
+
return self.page_width
|
|
174
|
+
|
|
175
|
+
def get_height(self) -> int:
|
|
176
|
+
return self.page_height
|
|
177
|
+
|
|
178
|
+
def get_type(self) -> str | None:
|
|
179
|
+
return self.type
|
|
180
|
+
|
|
181
|
+
def get_signature(self) -> str | None:
|
|
182
|
+
return self.signature
|
|
183
|
+
|
|
184
|
+
def get_total_pages(self) -> int:
|
|
185
|
+
return len(self.pages)
|
|
186
|
+
|
|
187
|
+
def get_page(self, number: int) -> "Page":
|
|
188
|
+
if number < 0 or number >= len(self.pages):
|
|
189
|
+
raise IndexError(f"page number out of range: {number}")
|
|
190
|
+
return self.pages[number]
|
|
191
|
+
|
|
192
|
+
def get_cover(self) -> "Cover":
|
|
193
|
+
return self.cover
|
|
194
|
+
|
|
195
|
+
def get_keywords(self) -> list["Keyword"]:
|
|
196
|
+
return self.keywords
|
|
197
|
+
|
|
198
|
+
def get_titles(self) -> list["Title"]:
|
|
199
|
+
return self.titles
|
|
200
|
+
|
|
201
|
+
def get_links(self):
|
|
202
|
+
return self.links
|
|
203
|
+
|
|
204
|
+
def get_fileid(self):
|
|
205
|
+
return self.metadata.header.get("FILE_ID")
|
|
206
|
+
|
|
207
|
+
def is_realtime_recognition(self):
|
|
208
|
+
return self.metadata.header.get("FILE_RECOGN_TYPE") == "1"
|
|
209
|
+
|
|
210
|
+
def supports_highres_grayscale(self):
|
|
211
|
+
return int(self.signature[-8:]) >= 20230015
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Cover:
|
|
215
|
+
def __init__(self) -> None:
|
|
216
|
+
self.content: bytes | None = None
|
|
217
|
+
|
|
218
|
+
def set_content(self, content: bytes | None):
|
|
219
|
+
self.content = content
|
|
220
|
+
|
|
221
|
+
def get_content(self) -> bytes | None:
|
|
222
|
+
return self.content
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class Keyword:
|
|
226
|
+
def __init__(self, keyword_info: dict[str, str]) -> None:
|
|
227
|
+
self.metadata = keyword_info
|
|
228
|
+
self.content: bytes | None = None
|
|
229
|
+
self.page_number = int(self.metadata["KEYWORDPAGE"]) - 1
|
|
230
|
+
|
|
231
|
+
def set_content(self, content: bytes | None):
|
|
232
|
+
self.content = content
|
|
233
|
+
|
|
234
|
+
def get_content(self) -> bytes | None:
|
|
235
|
+
return self.content
|
|
236
|
+
|
|
237
|
+
def get_page_number(self) -> int:
|
|
238
|
+
return self.page_number
|
|
239
|
+
|
|
240
|
+
def get_position_string(self) -> str:
|
|
241
|
+
(left, top, width, height) = self.metadata["KEYWORDRECTORI"].split(",")
|
|
242
|
+
return f"{int(top):04d}"
|
|
243
|
+
|
|
244
|
+
def get_keyword(self) -> str | None:
|
|
245
|
+
return (
|
|
246
|
+
None if self.metadata["KEYWORD"] is None else str(self.metadata["KEYWORD"])
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def get_rect(self) -> tuple[int, int, int, int]:
|
|
250
|
+
(left, top, width, height) = self.metadata["KEYWORDRECT"].split(",")
|
|
251
|
+
return (int(left), int(top), int(left) + int(width), int(top) + int(height))
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class Title:
|
|
255
|
+
def __init__(self, title_info: dict[str, str]) -> None:
|
|
256
|
+
self.metadata = title_info
|
|
257
|
+
self.content: bytes | None = None
|
|
258
|
+
self.page_number = 0
|
|
259
|
+
|
|
260
|
+
def set_content(self, content: bytes | None):
|
|
261
|
+
self.content = content
|
|
262
|
+
|
|
263
|
+
def get_content(self) -> bytes | None:
|
|
264
|
+
return self.content
|
|
265
|
+
|
|
266
|
+
def set_page_number(self, page_number: int) -> None:
|
|
267
|
+
self.page_number = page_number
|
|
268
|
+
|
|
269
|
+
def get_page_number(self) -> int:
|
|
270
|
+
return self.page_number
|
|
271
|
+
|
|
272
|
+
def get_position_string(self) -> str:
|
|
273
|
+
(left, top, width, height) = self.metadata["TITLERECTORI"].split(",")
|
|
274
|
+
return f"{int(top):04d}{int(left):04d}"
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
class Link:
|
|
278
|
+
TYPE_PAGE_LINK = 0
|
|
279
|
+
TYPE_FILE_LINK = 1
|
|
280
|
+
TYPE_WEB_LINK = 4
|
|
281
|
+
|
|
282
|
+
DIRECTION_OUT = 0
|
|
283
|
+
DIRECTION_IN = 1
|
|
284
|
+
|
|
285
|
+
def __init__(self, link_info: dict[str, str]) -> None:
|
|
286
|
+
self.metadata = link_info
|
|
287
|
+
self.content: bytes | None = None
|
|
288
|
+
self.page_number = 0
|
|
289
|
+
|
|
290
|
+
def set_content(self, content: bytes | None):
|
|
291
|
+
self.content = content
|
|
292
|
+
|
|
293
|
+
def get_content(self) -> bytes | None:
|
|
294
|
+
return self.content
|
|
295
|
+
|
|
296
|
+
def set_page_number(self, page_number: int) -> None:
|
|
297
|
+
self.page_number = page_number
|
|
298
|
+
|
|
299
|
+
def get_page_number(self) -> int:
|
|
300
|
+
return self.page_number
|
|
301
|
+
|
|
302
|
+
def get_type(self) -> int:
|
|
303
|
+
return int(self.metadata["LINKTYPE"])
|
|
304
|
+
|
|
305
|
+
def get_inout(self) -> int:
|
|
306
|
+
return int(self.metadata["LINKINOUT"])
|
|
307
|
+
|
|
308
|
+
def get_position_string(self) -> str:
|
|
309
|
+
(left, top, width, height) = self.metadata["LINKRECT"].split(",")
|
|
310
|
+
return f"{int(top):04d}{int(left):04d}{int(height):04d}{int(width):04d}"
|
|
311
|
+
|
|
312
|
+
def get_rect(self) -> tuple[int, int, int, int]:
|
|
313
|
+
(left, top, width, height) = self.metadata["LINKRECT"].split(",")
|
|
314
|
+
return (int(left), int(top), int(left) + int(width), int(top) + int(height))
|
|
315
|
+
|
|
316
|
+
def get_timestamp(self) -> str:
|
|
317
|
+
return self.metadata["LINKTIMESTAMP"]
|
|
318
|
+
|
|
319
|
+
def get_filepath(self) -> str:
|
|
320
|
+
return self.metadata["LINKFILE"] # Base64-encoded file path or URL
|
|
321
|
+
|
|
322
|
+
def get_fileid(self) -> str | None:
|
|
323
|
+
return (
|
|
324
|
+
None
|
|
325
|
+
if self.metadata["LINKFILEID"] == "none"
|
|
326
|
+
else self.metadata["LINKFILEID"]
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
def get_pageid(self) -> str | None:
|
|
330
|
+
return None if self.metadata["PAGEID"] == "none" else self.metadata["PAGEID"]
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class Page:
|
|
334
|
+
RECOGNSTATUS_NONE = 0
|
|
335
|
+
RECOGNSTATUS_DONE = 1
|
|
336
|
+
RECOGNSTATUS_RUNNING = 2
|
|
337
|
+
|
|
338
|
+
ORIENTATION_VERTICAL = "1000"
|
|
339
|
+
ORIENTATION_HORIZONTAL = "1090"
|
|
340
|
+
|
|
341
|
+
def __init__(self, page_info: ParamsBlock) -> None:
|
|
342
|
+
self.metadata = page_info
|
|
343
|
+
self.content: bytes | None = None
|
|
344
|
+
self.totalpath = None
|
|
345
|
+
self.recogn_file = None
|
|
346
|
+
self.recogn_text = None
|
|
347
|
+
self.layers: list[Layer] = []
|
|
348
|
+
layer_supported = page_info.get(KEY_LAYERS) is not None
|
|
349
|
+
if layer_supported:
|
|
350
|
+
for i in range(5):
|
|
351
|
+
self.layers.append(Layer(self.metadata[KEY_LAYERS][i]))
|
|
352
|
+
|
|
353
|
+
def set_content(self, content: bytes) -> None:
|
|
354
|
+
self.content = content
|
|
355
|
+
|
|
356
|
+
def get_content(self) -> bytes | None:
|
|
357
|
+
return self.content
|
|
358
|
+
|
|
359
|
+
def is_layer_supported(self) -> bool:
|
|
360
|
+
"""Returns True if this page supports layer.
|
|
361
|
+
|
|
362
|
+
Returns
|
|
363
|
+
-------
|
|
364
|
+
bool
|
|
365
|
+
True if this page supports layer.
|
|
366
|
+
"""
|
|
367
|
+
return self.metadata.get(KEY_LAYERS) is not None
|
|
368
|
+
|
|
369
|
+
def get_layers(self):
|
|
370
|
+
return self.layers
|
|
371
|
+
|
|
372
|
+
def get_layer(self, number):
|
|
373
|
+
if number < 0 or number >= len(self.layers):
|
|
374
|
+
raise IndexError(f"layer number out of range: {number}")
|
|
375
|
+
return self.layers[number]
|
|
376
|
+
|
|
377
|
+
def get_protocol(self):
|
|
378
|
+
if self.is_layer_supported():
|
|
379
|
+
# currently MAINLAYER is only supported
|
|
380
|
+
protocol = self.get_layer(0).metadata.get("LAYERPROTOCOL")
|
|
381
|
+
else:
|
|
382
|
+
protocol = self.metadata.get("PROTOCOL")
|
|
383
|
+
return protocol
|
|
384
|
+
|
|
385
|
+
def get_style(self):
|
|
386
|
+
return self.metadata.get("PAGESTYLE")
|
|
387
|
+
|
|
388
|
+
def get_style_hash(self):
|
|
389
|
+
hashcode = self.metadata.get("PAGESTYLEMD5")
|
|
390
|
+
if hashcode == "0":
|
|
391
|
+
return ""
|
|
392
|
+
return hashcode
|
|
393
|
+
|
|
394
|
+
def get_layer_info(self):
|
|
395
|
+
info = self.metadata.get("LAYERINFO")
|
|
396
|
+
if info is None or info == "none":
|
|
397
|
+
return None
|
|
398
|
+
return info.replace("#", ":")
|
|
399
|
+
|
|
400
|
+
def get_layer_order(self):
|
|
401
|
+
seq = self.metadata.get("LAYERSEQ")
|
|
402
|
+
if seq is None:
|
|
403
|
+
return []
|
|
404
|
+
order = seq.split(",")
|
|
405
|
+
return order
|
|
406
|
+
|
|
407
|
+
def set_totalpath(self, totalpath):
|
|
408
|
+
self.totalpath = totalpath
|
|
409
|
+
|
|
410
|
+
def get_totalpath(self):
|
|
411
|
+
return self.totalpath
|
|
412
|
+
|
|
413
|
+
def get_pageid(self):
|
|
414
|
+
return self.metadata.get("PAGEID")
|
|
415
|
+
|
|
416
|
+
def get_recogn_status(self):
|
|
417
|
+
return int(self.metadata.get("RECOGNSTATUS"))
|
|
418
|
+
|
|
419
|
+
def set_recogn_file(self, recogn_file):
|
|
420
|
+
self.recogn_file = recogn_file
|
|
421
|
+
|
|
422
|
+
def get_recogn_file(self):
|
|
423
|
+
return self.recogn_file
|
|
424
|
+
|
|
425
|
+
def set_recogn_text(self, recogn_text):
|
|
426
|
+
self.recogn_text = recogn_text
|
|
427
|
+
|
|
428
|
+
def get_recogn_text(self):
|
|
429
|
+
return self.recogn_text
|
|
430
|
+
|
|
431
|
+
def get_orientation(self):
|
|
432
|
+
return self.metadata.get("ORIENTATION", self.ORIENTATION_VERTICAL)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class Layer:
|
|
436
|
+
def __init__(self, layer_info):
|
|
437
|
+
self.metadata = layer_info
|
|
438
|
+
self.content = None
|
|
439
|
+
|
|
440
|
+
def set_content(self, content):
|
|
441
|
+
self.content = content
|
|
442
|
+
|
|
443
|
+
def get_content(self):
|
|
444
|
+
return self.content
|
|
445
|
+
|
|
446
|
+
def get_name(self):
|
|
447
|
+
return self.metadata.get("LAYERNAME")
|
|
448
|
+
|
|
449
|
+
def get_protocol(self):
|
|
450
|
+
return self.metadata.get("LAYERPROTOCOL")
|
|
451
|
+
|
|
452
|
+
def get_type(self):
|
|
453
|
+
return self.metadata.get("LAYERTYPE")
|