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/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
File without changes
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
# Copyright (c) 2020 jya
|
|
4
|
+
#
|
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
# you may not use this file except in compliance with the License.
|
|
7
|
+
# You may obtain a copy of the License at
|
|
8
|
+
#
|
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
#
|
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
# See the License for the specific language governing permissions and
|
|
15
|
+
# limitations under the License.
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import io
|
|
19
|
+
import os
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
from colour import Color
|
|
23
|
+
|
|
24
|
+
import supernote as sn
|
|
25
|
+
from supernote.converter import (
|
|
26
|
+
ImageConverter,
|
|
27
|
+
SvgConverter,
|
|
28
|
+
PdfConverter,
|
|
29
|
+
TextConverter,
|
|
30
|
+
)
|
|
31
|
+
from supernote.converter import VisibilityOverlay
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def convert_all(converter, total, file_name, save_func, visibility_overlay):
|
|
35
|
+
basename, extension = os.path.splitext(file_name)
|
|
36
|
+
max_digits = len(str(total))
|
|
37
|
+
for i in range(total):
|
|
38
|
+
# append page number between filename and extension
|
|
39
|
+
numbered_filename = basename + "_" + str(i).zfill(max_digits) + extension
|
|
40
|
+
img = converter.convert(i, visibility_overlay)
|
|
41
|
+
save_func(img, numbered_filename)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def convert_and_concat_all(converter, total, file_name, save_func, separator):
|
|
45
|
+
data = []
|
|
46
|
+
for i in range(total):
|
|
47
|
+
data.append(converter.convert(i))
|
|
48
|
+
data = list(map(lambda x: "" if x is None else (x + "\n"), data))
|
|
49
|
+
if len(data) > 0:
|
|
50
|
+
alldata = ((separator + "\n") if separator else "").join(data)
|
|
51
|
+
save_func(alldata, file_name)
|
|
52
|
+
else:
|
|
53
|
+
print("no data")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def convert_to_png(args, notebook, palette):
|
|
57
|
+
converter = ImageConverter(notebook, palette=palette)
|
|
58
|
+
bg_visibility = (
|
|
59
|
+
VisibilityOverlay.INVISIBLE
|
|
60
|
+
if args.exclude_background
|
|
61
|
+
else VisibilityOverlay.DEFAULT
|
|
62
|
+
)
|
|
63
|
+
vo = sn.converter.build_visibility_overlay(background=bg_visibility)
|
|
64
|
+
|
|
65
|
+
def save(img, file_name):
|
|
66
|
+
img.save(file_name, format="PNG")
|
|
67
|
+
|
|
68
|
+
if args.all:
|
|
69
|
+
total = notebook.get_total_pages()
|
|
70
|
+
convert_all(converter, total, args.output, save, vo)
|
|
71
|
+
else:
|
|
72
|
+
img = converter.convert(args.number, visibility_overlay=vo)
|
|
73
|
+
save(img, args.output)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def convert_to_svg(args, notebook, palette):
|
|
77
|
+
converter = SvgConverter(notebook, palette=palette)
|
|
78
|
+
bg_visibility = (
|
|
79
|
+
VisibilityOverlay.INVISIBLE
|
|
80
|
+
if args.exclude_background
|
|
81
|
+
else VisibilityOverlay.DEFAULT
|
|
82
|
+
)
|
|
83
|
+
vo = sn.converter.build_visibility_overlay(background=bg_visibility)
|
|
84
|
+
|
|
85
|
+
def save(svg, file_name):
|
|
86
|
+
if svg is not None:
|
|
87
|
+
with open(file_name, "w") as f:
|
|
88
|
+
f.write(svg)
|
|
89
|
+
else:
|
|
90
|
+
print("no path data")
|
|
91
|
+
|
|
92
|
+
if args.all:
|
|
93
|
+
total = notebook.get_total_pages()
|
|
94
|
+
convert_all(converter, total, args.output, save, vo)
|
|
95
|
+
else:
|
|
96
|
+
svg = converter.convert(args.number, visibility_overlay=vo)
|
|
97
|
+
save(svg, args.output)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def convert_to_pdf(args, notebook, palette):
|
|
101
|
+
use_link = not args.no_link
|
|
102
|
+
use_keyword = args.add_keyword
|
|
103
|
+
converter = PdfConverter(notebook, palette=palette)
|
|
104
|
+
|
|
105
|
+
def save(data, file_name):
|
|
106
|
+
if data is not None:
|
|
107
|
+
with open(file_name, "wb") as f:
|
|
108
|
+
f.write(data)
|
|
109
|
+
else:
|
|
110
|
+
print("no data")
|
|
111
|
+
|
|
112
|
+
if args.all:
|
|
113
|
+
data = converter.convert(
|
|
114
|
+
-1, enable_link=use_link, enable_keyword=use_keyword
|
|
115
|
+
) # minus value means converting all pages
|
|
116
|
+
save(data, args.output)
|
|
117
|
+
else:
|
|
118
|
+
data = converter.convert(
|
|
119
|
+
args.number, enable_link=use_link, enable_keyword=use_keyword
|
|
120
|
+
)
|
|
121
|
+
save(data, args.output)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def convert_to_txt(args, notebook, palette):
|
|
125
|
+
converter = TextConverter(notebook, palette=palette)
|
|
126
|
+
|
|
127
|
+
def save(data, file_name):
|
|
128
|
+
if data is not None:
|
|
129
|
+
with open(file_name, "w") as f:
|
|
130
|
+
f.write(data)
|
|
131
|
+
else:
|
|
132
|
+
print("no data")
|
|
133
|
+
|
|
134
|
+
if args.all:
|
|
135
|
+
total = notebook.get_total_pages()
|
|
136
|
+
convert_and_concat_all(
|
|
137
|
+
converter, total, args.output, save, args.text_page_separator
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
data = converter.convert(args.number)
|
|
141
|
+
save(data, args.output)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def subcommand_convert(args):
|
|
145
|
+
notebook = sn.load_notebook(args.input, policy=args.policy)
|
|
146
|
+
palette = None
|
|
147
|
+
if args.color:
|
|
148
|
+
try:
|
|
149
|
+
colors = parse_color(args.color)
|
|
150
|
+
except ValueError as e:
|
|
151
|
+
print(e, file=sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
palette = sn.color.ColorPalette(sn.color.MODE_RGB, colors)
|
|
154
|
+
if args.type == "png":
|
|
155
|
+
convert_to_png(args, notebook, palette)
|
|
156
|
+
elif args.type == "svg":
|
|
157
|
+
convert_to_svg(args, notebook, palette)
|
|
158
|
+
elif args.type == "pdf":
|
|
159
|
+
convert_to_pdf(args, notebook, palette)
|
|
160
|
+
elif args.type == "txt":
|
|
161
|
+
convert_to_txt(args, notebook, palette)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def subcommand_analyze(args):
|
|
165
|
+
# show all metadata as JSON
|
|
166
|
+
with open(args.input, "rb") as f:
|
|
167
|
+
metadata = sn.parse_metadata(f, policy=args.policy)
|
|
168
|
+
print(metadata.to_json(indent=2))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def subcommand_merge(args):
|
|
172
|
+
num_input = len(args.input)
|
|
173
|
+
if num_input == 1: # reconstruct a note file
|
|
174
|
+
notebook = sn.load_notebook(args.input[0])
|
|
175
|
+
reconstructed_binary = sn.reconstruct(notebook)
|
|
176
|
+
with open(args.output, "wb") as f:
|
|
177
|
+
f.write(reconstructed_binary)
|
|
178
|
+
else: # merge multiple note files
|
|
179
|
+
with open(args.input[0], "rb") as f:
|
|
180
|
+
merged_binary = f.read()
|
|
181
|
+
for i in range(1, num_input):
|
|
182
|
+
stream = io.BytesIO(merged_binary)
|
|
183
|
+
merged_notebook = sn.load(stream)
|
|
184
|
+
next_notebook = sn.load_notebook(args.input[i])
|
|
185
|
+
merged_binary = sn.merge(merged_notebook, next_notebook)
|
|
186
|
+
with open(args.output, "wb") as f:
|
|
187
|
+
f.write(merged_binary)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def subcommand_reconstruct(args):
|
|
191
|
+
notebook = sn.load_notebook(args.input)
|
|
192
|
+
reconstructed_binary = sn.reconstruct(notebook)
|
|
193
|
+
with open(args.output, "wb") as f:
|
|
194
|
+
f.write(reconstructed_binary)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def parse_color(color_string):
|
|
198
|
+
colorcodes = color_string.split(",")
|
|
199
|
+
if len(colorcodes) != 4:
|
|
200
|
+
raise ValueError(f"few color codes, 4 colors are required: {color_string}")
|
|
201
|
+
black = int(Color(colorcodes[0]).hex_l[1:7], 16)
|
|
202
|
+
darkgray = int(Color(colorcodes[1]).hex_l[1:7], 16)
|
|
203
|
+
gray = int(Color(colorcodes[2]).hex_l[1:7], 16)
|
|
204
|
+
white = int(Color(colorcodes[3]).hex_l[1:7], 16)
|
|
205
|
+
return (black, darkgray, gray, white)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main():
|
|
209
|
+
parser = argparse.ArgumentParser(
|
|
210
|
+
prog="supernote-tool", description="Unofficial python tool for Ratta Supernote"
|
|
211
|
+
)
|
|
212
|
+
parser.add_argument(
|
|
213
|
+
"--version",
|
|
214
|
+
help="show version information and exit",
|
|
215
|
+
action="version",
|
|
216
|
+
version=f"%(prog)s {sn.__version__}",
|
|
217
|
+
)
|
|
218
|
+
subparsers = parser.add_subparsers()
|
|
219
|
+
|
|
220
|
+
# 'analyze' subcommand
|
|
221
|
+
parser_analyze = subparsers.add_parser("analyze", help="analyze note file")
|
|
222
|
+
parser_analyze.add_argument("input", type=str, help="input note file")
|
|
223
|
+
parser_analyze.add_argument(
|
|
224
|
+
"--policy",
|
|
225
|
+
choices=["strict", "loose"],
|
|
226
|
+
default="strict",
|
|
227
|
+
help="select parser policy",
|
|
228
|
+
)
|
|
229
|
+
parser_analyze.set_defaults(handler=subcommand_analyze)
|
|
230
|
+
|
|
231
|
+
# 'convert' subcommand
|
|
232
|
+
parser_convert = subparsers.add_parser("convert", help="image conversion")
|
|
233
|
+
parser_convert.add_argument("input", type=str, help="input note file")
|
|
234
|
+
parser_convert.add_argument("output", type=str, help="output image file")
|
|
235
|
+
parser_convert.add_argument(
|
|
236
|
+
"-n", "--number", type=int, default=0, help="page number to be converted"
|
|
237
|
+
)
|
|
238
|
+
parser_convert.add_argument(
|
|
239
|
+
"-a", "--all", action="store_true", default=False, help="convert all pages"
|
|
240
|
+
)
|
|
241
|
+
parser_convert.add_argument(
|
|
242
|
+
"-c",
|
|
243
|
+
"--color",
|
|
244
|
+
type=str,
|
|
245
|
+
help="colorize note with comma separated color codes in order of black, darkgray, gray and white.",
|
|
246
|
+
)
|
|
247
|
+
parser_convert.add_argument(
|
|
248
|
+
"-t",
|
|
249
|
+
"--type",
|
|
250
|
+
choices=["png", "svg", "pdf", "txt"],
|
|
251
|
+
default="png",
|
|
252
|
+
help="select conversion file type",
|
|
253
|
+
)
|
|
254
|
+
parser_convert.add_argument(
|
|
255
|
+
"--exclude-background",
|
|
256
|
+
action="store_true",
|
|
257
|
+
default=False,
|
|
258
|
+
help="exclude background and make it transparent (PNG and SVG are supported)",
|
|
259
|
+
)
|
|
260
|
+
parser_convert.add_argument(
|
|
261
|
+
"--pdf-type",
|
|
262
|
+
choices=["original"],
|
|
263
|
+
default="original",
|
|
264
|
+
help="select PDF conversion type",
|
|
265
|
+
)
|
|
266
|
+
parser_convert.add_argument(
|
|
267
|
+
"--no-link", action="store_true", default=False, help="disable links in PDF"
|
|
268
|
+
)
|
|
269
|
+
parser_convert.add_argument(
|
|
270
|
+
"--add-keyword",
|
|
271
|
+
action="store_true",
|
|
272
|
+
default=False,
|
|
273
|
+
help="enable keywords in PDF",
|
|
274
|
+
)
|
|
275
|
+
parser_convert.add_argument(
|
|
276
|
+
"--text-page-separator",
|
|
277
|
+
type=str,
|
|
278
|
+
default="",
|
|
279
|
+
help="page separator string for text conversion",
|
|
280
|
+
)
|
|
281
|
+
parser_convert.add_argument(
|
|
282
|
+
"--policy",
|
|
283
|
+
choices=["strict", "loose"],
|
|
284
|
+
default="strict",
|
|
285
|
+
help="select parser policy",
|
|
286
|
+
)
|
|
287
|
+
parser_convert.set_defaults(handler=subcommand_convert)
|
|
288
|
+
|
|
289
|
+
# 'merge' subcommand
|
|
290
|
+
description = """
|
|
291
|
+
(EXPERIMENTAL FEATURE)
|
|
292
|
+
This command merge multiple note files to one.
|
|
293
|
+
Backup your input files to save your data because you might get a corrupted output file.
|
|
294
|
+
"""
|
|
295
|
+
parser_merge = subparsers.add_parser(
|
|
296
|
+
"merge",
|
|
297
|
+
description=description,
|
|
298
|
+
help="merge multiple note files (EXPERIMENTAL FEATURE)",
|
|
299
|
+
)
|
|
300
|
+
parser_merge.add_argument("input", type=str, nargs="+", help="input note files")
|
|
301
|
+
parser_merge.add_argument("output", type=str, help="output note file")
|
|
302
|
+
parser_merge.set_defaults(handler=subcommand_merge)
|
|
303
|
+
|
|
304
|
+
# 'reconstruct' subcommand
|
|
305
|
+
description = """
|
|
306
|
+
(EXPERIMENTAL FEATURE)
|
|
307
|
+
This command disassemble and reconstruct a note file for debugging and testing.
|
|
308
|
+
Backup your input file to save your data because you might get a corrupted output file.
|
|
309
|
+
"""
|
|
310
|
+
parser_reconstruct = subparsers.add_parser(
|
|
311
|
+
"reconstruct",
|
|
312
|
+
description=description,
|
|
313
|
+
help="reconstruct a note file (EXPERIMENTAL FEATURE)",
|
|
314
|
+
)
|
|
315
|
+
parser_reconstruct.add_argument("input", type=str, help="input note file")
|
|
316
|
+
parser_reconstruct.add_argument("output", type=str, help="output note file")
|
|
317
|
+
parser_reconstruct.set_defaults(handler=subcommand_reconstruct)
|
|
318
|
+
|
|
319
|
+
args = parser.parse_args()
|
|
320
|
+
if hasattr(args, "handler"):
|
|
321
|
+
args.handler(args)
|
|
322
|
+
else:
|
|
323
|
+
parser.print_help()
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if __name__ == "__main__":
|
|
327
|
+
main()
|
supernote/color.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
"""Color classes."""
|
|
16
|
+
|
|
17
|
+
# color mode
|
|
18
|
+
MODE_GRAYSCALE = "grayscale"
|
|
19
|
+
MODE_RGB = "rgb"
|
|
20
|
+
|
|
21
|
+
# preset grayscale colors
|
|
22
|
+
BLACK = 0x00
|
|
23
|
+
DARK_GRAY = 0x9D
|
|
24
|
+
GRAY = 0xC9
|
|
25
|
+
WHITE = 0xFE
|
|
26
|
+
TRANSPARENT = 0xFF
|
|
27
|
+
DARK_GRAY_COMPAT = 0x30
|
|
28
|
+
GRAY_COMPAT = 0x50
|
|
29
|
+
|
|
30
|
+
# preset RGB colors
|
|
31
|
+
RGB_BLACK = 0x000000
|
|
32
|
+
RGB_DARK_GRAY = 0x9D9D9D
|
|
33
|
+
RGB_GRAY = 0xC9C9C9
|
|
34
|
+
RGB_WHITE = 0xFEFEFE
|
|
35
|
+
RGB_TRANSPARENT = 0xFFFFFF
|
|
36
|
+
RGB_DARK_GRAY_COMPAT = 0x303030
|
|
37
|
+
RGB_GRAY_COMPAT = 0x505050
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_rgb(value: int) -> tuple[int, int, int]:
|
|
41
|
+
r = (value & 0xFF0000) >> 16
|
|
42
|
+
g = (value & 0x00FF00) >> 8
|
|
43
|
+
b = value & 0x0000FF
|
|
44
|
+
return (r, g, b)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def web_string(value: int, mode: str = MODE_RGB) -> str:
|
|
48
|
+
if mode == MODE_GRAYSCALE:
|
|
49
|
+
return "#" + (format(value & 0xFF, "02x") * 3)
|
|
50
|
+
else:
|
|
51
|
+
r, g, b = get_rgb(value)
|
|
52
|
+
return (
|
|
53
|
+
"#"
|
|
54
|
+
+ format(r & 0xFF, "02x")
|
|
55
|
+
+ format(g & 0xFF, "02x")
|
|
56
|
+
+ format(b & 0xFF, "02x")
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ColorPalette:
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
mode: str = MODE_GRAYSCALE,
|
|
64
|
+
colors: tuple[int, int, int, int] = (BLACK, DARK_GRAY, GRAY, WHITE),
|
|
65
|
+
compat_colors: tuple[int, int] = (DARK_GRAY_COMPAT, GRAY_COMPAT),
|
|
66
|
+
) -> None:
|
|
67
|
+
if mode not in [MODE_GRAYSCALE, MODE_RGB]:
|
|
68
|
+
raise ValueError("mode must be MODE_GRAYSCALE or MODE_RGB")
|
|
69
|
+
if len(colors) != 4:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
"colors must have 4 color values (black, darkgray, gray, white)"
|
|
72
|
+
)
|
|
73
|
+
self.mode = mode
|
|
74
|
+
self.black = colors[0]
|
|
75
|
+
self.darkgray = colors[1]
|
|
76
|
+
self.gray = colors[2]
|
|
77
|
+
self.white = colors[3]
|
|
78
|
+
if mode == MODE_GRAYSCALE:
|
|
79
|
+
self.transparent = TRANSPARENT
|
|
80
|
+
else:
|
|
81
|
+
self.transparent = RGB_TRANSPARENT
|
|
82
|
+
self.darkgray_compat = compat_colors[0]
|
|
83
|
+
self.gray_compat = compat_colors[1]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
DEFAULT_COLORPALETTE = ColorPalette(
|
|
87
|
+
MODE_GRAYSCALE, (BLACK, DARK_GRAY, GRAY, WHITE), (DARK_GRAY_COMPAT, GRAY_COMPAT)
|
|
88
|
+
)
|
|
89
|
+
DEFAULT_RGB_COLORPALETTE = ColorPalette(
|
|
90
|
+
MODE_RGB,
|
|
91
|
+
(RGB_BLACK, RGB_DARK_GRAY, RGB_GRAY, RGB_WHITE),
|
|
92
|
+
(RGB_DARK_GRAY_COMPAT, RGB_GRAY_COMPAT),
|
|
93
|
+
)
|