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.
@@ -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")