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/parser.py ADDED
@@ -0,0 +1,738 @@
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
+ """Parser classes."""
16
+
17
+ import os
18
+ import re
19
+ from typing import Protocol
20
+
21
+ from . import exceptions
22
+ from . import fileformat
23
+
24
+ ParamsBlock = fileformat.ParamsBlock
25
+
26
+
27
+ class FileObj(Protocol):
28
+ """Protocol for file-like object."""
29
+
30
+ def seek(self, offset: int, whence: int) -> None:
31
+ """Seeks to the given offset."""
32
+ ...
33
+
34
+ def read(self, size: int) -> bytes:
35
+ """Reads bytes from the stream."""
36
+ ...
37
+
38
+
39
+ def parse_metadata(stream: FileObj, policy="strict"):
40
+ """Parses a supernote binary stream and returns metadata object.
41
+
42
+ Policy:
43
+ - 'strict': raise exception for unknown signature (default)
44
+ - 'loose': try to parse for unknown signature
45
+
46
+ Parameters
47
+ ----------
48
+ stream : file-like object
49
+ supernote binary stream
50
+ policy : str
51
+ signature check policy
52
+
53
+ Returns
54
+ -------
55
+ SupernoteMetadata
56
+ metadata object
57
+ """
58
+ try:
59
+ parser = SupernoteXParser()
60
+ metadata = parser.parse_stream(stream, policy)
61
+ except exceptions.UnsupportedFileFormat:
62
+ # ignore this exception and try next parser
63
+ pass
64
+ else:
65
+ return metadata
66
+
67
+ try:
68
+ parser = SupernoteParser()
69
+ metadata = parser.parse_stream(stream, policy)
70
+ except exceptions.UnsupportedFileFormat:
71
+ # ignore this exception and try next parser
72
+ pass
73
+ else:
74
+ return metadata
75
+
76
+ # we cannot parse the file with any our parser.
77
+ raise exceptions.UnsupportedFileFormat("unsupported file format")
78
+
79
+
80
+ def load(
81
+ stream: FileObj,
82
+ metadata: fileformat.SupernoteMetadata | None = None,
83
+ policy: str = "strict",
84
+ ) -> fileformat.Notebook:
85
+ """Creates a Notebook object from the supernote binary stream.
86
+
87
+ Policy:
88
+ - 'strict': raise exception for unknown signature (default)
89
+ - 'loose': try to parse for unknown signature
90
+
91
+ Parameters
92
+ ----------
93
+ stream : file-like object
94
+ supernote binary stream
95
+ metadata : SupernoteMetadata
96
+ metadata object
97
+ policy : str
98
+ signature check policy
99
+
100
+ Returns
101
+ -------
102
+ Notebook
103
+ notebook object
104
+ """
105
+ if metadata is None:
106
+ metadata = parse_metadata(stream, policy)
107
+
108
+ note = fileformat.Notebook(metadata)
109
+
110
+ cover_address = _get_cover_address(metadata)
111
+ if cover_address > 0:
112
+ content = _get_content_at_address(stream, cover_address)
113
+ note.get_cover().set_content(content)
114
+ # store keyword data to notebook object
115
+ for keyword in note.get_keywords():
116
+ address = _get_keyword_address(keyword)
117
+ content = _get_content_at_address(stream, address)
118
+ keyword.set_content(content)
119
+ # store title data to notebook object
120
+ page_numbers = _get_page_number_from_footer_property(
121
+ note.get_metadata().footer, "TITLE_"
122
+ )
123
+ for i, title in enumerate(note.get_titles()):
124
+ address = _get_title_address(title)
125
+ content = _get_content_at_address(stream, address)
126
+ title.set_content(content)
127
+ title.set_page_number(page_numbers[i])
128
+ # store link data to notebook object
129
+ page_numbers = _get_page_number_from_footer_property(
130
+ note.get_metadata().footer, "LINK"
131
+ )
132
+ for i, link in enumerate(note.get_links()):
133
+ address = _get_link_address(link)
134
+ content = _get_content_at_address(stream, address)
135
+ link.set_content(content)
136
+ link.set_page_number(page_numbers[i])
137
+ page_total = metadata.get_total_pages()
138
+ for p in range(page_total):
139
+ addresses = _get_bitmap_address(metadata, p)
140
+ if len(addresses) == 1: # the page has no layers
141
+ content = _get_content_at_address(stream, addresses[0])
142
+ note.get_page(p).set_content(content)
143
+ else:
144
+ for layer, addr in enumerate(addresses):
145
+ content = _get_content_at_address(stream, addr)
146
+ note.get_page(p).get_layer(layer).set_content(content)
147
+ # store path data to notebook object
148
+ totalpath_address = _get_totalpath_address(metadata, p)
149
+ if totalpath_address > 0:
150
+ content = _get_content_at_address(stream, totalpath_address)
151
+ note.get_page(p).set_totalpath(content)
152
+ # store recogn file data to notebook object
153
+ recogn_file_address = _get_recogn_file_address(metadata, p)
154
+ if recogn_file_address > 0:
155
+ content = _get_content_at_address(stream, recogn_file_address)
156
+ note.get_page(p).set_recogn_file(content)
157
+ # store recogn text data to notebook object
158
+ recogn_text_address = _get_recogn_text_address(metadata, p)
159
+ if recogn_text_address > 0:
160
+ content = _get_content_at_address(stream, recogn_text_address)
161
+ note.get_page(p).set_recogn_text(content)
162
+ return note
163
+
164
+
165
+ def load_notebook(file_name, metadata=None, policy="strict"):
166
+ """Creates a Notebook object from the supernote file.
167
+
168
+ Policy:
169
+ - 'strict': raise exception for unknown signature (default)
170
+ - 'loose': try to parse for unknown signature
171
+
172
+ Parameters
173
+ ----------
174
+ file_name : str
175
+ file path string
176
+ metadata : SupernoteMetadata
177
+ metadata object
178
+ policy : str
179
+ signature check policy
180
+
181
+ Returns
182
+ -------
183
+ Notebook
184
+ notebook object
185
+ """
186
+ with open(file_name, "rb") as f:
187
+ note = load(f, metadata, policy)
188
+ return note
189
+
190
+
191
+ def _get_content_at_address(fobj, address):
192
+ content = None
193
+ if address != 0:
194
+ fobj.seek(address, os.SEEK_SET)
195
+ block_length = int.from_bytes(fobj.read(fileformat.LENGTH_FIELD_SIZE), "little")
196
+ content = fobj.read(block_length)
197
+ return content
198
+
199
+
200
+ def _get_cover_address(metadata):
201
+ """Returns cover address.
202
+
203
+ Returns
204
+ -------
205
+ int
206
+ cover address
207
+ """
208
+ if "COVER_2" in metadata.footer:
209
+ address = int(metadata.footer["COVER_2"])
210
+ elif "COVER_1" in metadata.footer:
211
+ address = int(metadata.footer["COVER_1"])
212
+ else:
213
+ address = 0
214
+ return address
215
+
216
+
217
+ def _get_keyword_address(keyword):
218
+ """Returns keyword content address.
219
+
220
+ Returns
221
+ -------
222
+ int
223
+ keyword content address
224
+ """
225
+ return int(keyword.metadata["KEYWORDSITE"])
226
+
227
+
228
+ def _get_title_address(title):
229
+ """Returns title content address.
230
+
231
+ Returns
232
+ -------
233
+ int
234
+ title content address
235
+ """
236
+ return int(title.metadata["TITLEBITMAP"])
237
+
238
+
239
+ def _get_link_address(link):
240
+ """Returns link content address.
241
+
242
+ Returns
243
+ -------
244
+ int
245
+ link content address
246
+ """
247
+ return int(link.metadata["LINKBITMAP"])
248
+
249
+
250
+ def _get_bitmap_address(metadata, page_number):
251
+ """Returns bitmap address of the given page number.
252
+
253
+ Returns
254
+ -------
255
+ list of int
256
+ bitmap address
257
+ """
258
+ addresses = []
259
+ layer_supported = metadata.is_layer_supported(page_number)
260
+ if layer_supported:
261
+ for layer in range(5): # TODO: use constant
262
+ address = metadata.pages[page_number][fileformat.KEY_LAYERS][layer].get(
263
+ "LAYERBITMAP"
264
+ )
265
+ addresses.append(0 if address is None else int(address))
266
+ else:
267
+ addresses.append(int(metadata.pages[page_number]["DATA"]))
268
+ return addresses
269
+
270
+
271
+ def _get_totalpath_address(
272
+ metadata: fileformat.SupernoteMetadata, page_number: int
273
+ ) -> int:
274
+ """Returns total path address of the given page number.
275
+
276
+ Returns
277
+ -------
278
+ int
279
+ total path address
280
+ """
281
+ if "TOTALPATH" in metadata.pages[page_number]:
282
+ address = int(metadata.pages[page_number]["TOTALPATH"])
283
+ else:
284
+ address = 0
285
+ return address
286
+
287
+
288
+ def _get_recogn_file_address(metadata, page_number):
289
+ """Returns recogn file address of the given page number.
290
+
291
+ Returns
292
+ -------
293
+ int
294
+ recogn file address
295
+ """
296
+ if "RECOGNFILE" in metadata.pages[page_number]:
297
+ address = int(metadata.pages[page_number]["RECOGNFILE"])
298
+ else:
299
+ address = 0
300
+ return address
301
+
302
+
303
+ def _get_recogn_text_address(metadata, page_number):
304
+ """Returns recogn text address of the given page number.
305
+
306
+ Returns
307
+ -------
308
+ int
309
+ recogn text address
310
+ """
311
+ if "RECOGNTEXT" in metadata.pages[page_number]:
312
+ address = int(metadata.pages[page_number]["RECOGNTEXT"])
313
+ else:
314
+ address = 0
315
+ return address
316
+
317
+
318
+ def _get_page_number_from_footer_property(footer, prefix):
319
+ keys = filter(lambda k: k.startswith(prefix), footer.keys())
320
+ page_numbers = []
321
+ for k in keys:
322
+ if type(footer[k]) is list:
323
+ for _ in range(len(footer[k])):
324
+ page_numbers.append(int(k[6:10]) - 1)
325
+ else:
326
+ page_numbers.append(
327
+ int(k[6:10]) - 1
328
+ ) # e.g. get '0123' from 'TITLE_01234567'
329
+ return page_numbers
330
+
331
+
332
+ class SupernoteParser:
333
+ """Parser for original Supernote."""
334
+
335
+ SN_SIGNATURE_OFFSET = 0
336
+ SN_SIGNATURE_PATTERN = r"SN_FILE_ASA_\d{8}"
337
+ SN_SIGNATURES = ["SN_FILE_ASA_20190529"]
338
+
339
+ def parse(
340
+ self, file_name: str, policy: str = "strict"
341
+ ) -> fileformat.SupernoteMetadata:
342
+ """Parses a Supernote file and returns SupernoteMetadata object.
343
+
344
+ Policy:
345
+ - 'strict': raise exception for unknown signature (default)
346
+ - 'loose': try to parse for unknown signature
347
+
348
+ Parameters
349
+ ----------
350
+ file_name : str
351
+ file path string
352
+ policy : str
353
+ signature check policy
354
+
355
+ Returns
356
+ -------
357
+ SupernoteMetadata
358
+ metadata of the file
359
+ """
360
+ with open(file_name, "rb") as f:
361
+ metadata = self.parse_stream(f, policy)
362
+ return metadata
363
+
364
+ def parse_stream(
365
+ self, stream: FileObj, policy: str = "strict"
366
+ ) -> fileformat.SupernoteMetadata:
367
+ """Parses a Supernote file stream and returns SupernoteMetadata object.
368
+
369
+ Policy:
370
+ - 'strict': raise exception for unknown signature (default)
371
+ - 'loose': try to parse for unknown signature
372
+
373
+ Parameters
374
+ ----------
375
+ file_name : str
376
+ file path string
377
+ policy : str
378
+ signature check policy
379
+
380
+ Returns
381
+ -------
382
+ SupernoteMetadata
383
+ metadata of the file
384
+ """
385
+ # parse file type
386
+ filetype = self._parse_filetype(stream)
387
+ # check file signature
388
+ signature = self._find_matching_signature(stream)
389
+ if signature is None:
390
+ compatible = self._check_signature_compatible(stream)
391
+ if policy != "loose" or not compatible:
392
+ raise exceptions.UnsupportedFileFormat(
393
+ f"unknown signature: {signature}"
394
+ )
395
+ else:
396
+ signature = self.SN_SIGNATURES[
397
+ -1
398
+ ] # treat as latest supported signature
399
+ # parse footer block
400
+ stream.seek(
401
+ -fileformat.ADDRESS_SIZE, os.SEEK_END
402
+ ) # footer address is located at last 4-byte
403
+ footer_address = int.from_bytes(stream.read(fileformat.ADDRESS_SIZE), "little")
404
+ footer = self._parse_footer_block(stream, footer_address)
405
+ # parse header block
406
+ header_address = self._get_header_address(footer)
407
+ header = self._parse_metadata_block(stream, header_address)
408
+ # parse page blocks
409
+ page_addresses = self._get_page_addresses(footer)
410
+ pages = list(
411
+ map(lambda addr: self._parse_page_block(stream, addr), page_addresses)
412
+ )
413
+
414
+ metadata = fileformat.SupernoteMetadata()
415
+ metadata.type = filetype
416
+ metadata.signature = signature
417
+ metadata.header = header
418
+ metadata.footer = footer
419
+ metadata.pages = pages
420
+ return metadata
421
+
422
+ def _parse_filetype(self, fobj: FileObj) -> str:
423
+ fobj.seek(0, os.SEEK_SET)
424
+ filetype = fobj.read(4).decode()
425
+ return filetype
426
+
427
+ def _find_matching_signature(self, fobj: FileObj) -> str | None:
428
+ """Reads signature from file object and returns matching signature.
429
+
430
+ Parameters
431
+ ----------
432
+ fobj : file
433
+ file object
434
+
435
+ Returns
436
+ -------
437
+ string
438
+ matching signature or None if not found
439
+ """
440
+ for sig in self.SN_SIGNATURES:
441
+ try:
442
+ fobj.seek(self.SN_SIGNATURE_OFFSET, os.SEEK_SET)
443
+ signature = fobj.read(len(sig)).decode()
444
+ except UnicodeDecodeError:
445
+ # try next signature
446
+ continue
447
+ if signature == sig:
448
+ return signature
449
+ return None
450
+
451
+ def _check_signature_compatible(self, fobj: FileObj) -> bool:
452
+ latest_signature = self.SN_SIGNATURES[-1]
453
+ try:
454
+ fobj.seek(0, os.SEEK_SET)
455
+ signature = fobj.read(len(latest_signature)).decode()
456
+ except Exception:
457
+ return False
458
+ else:
459
+ if re.match(self.SN_SIGNATURE_PATTERN, signature):
460
+ return True
461
+ else:
462
+ return False
463
+
464
+ def _parse_footer_block(self, fobj: FileObj, address: int) -> ParamsBlock:
465
+ return self._parse_metadata_block(fobj, address)
466
+
467
+ def _get_header_address(self, footer: ParamsBlock) -> int:
468
+ """Returns header address.
469
+
470
+ Parameters
471
+ ----------
472
+ footer : dict
473
+ footer parameters
474
+
475
+ Returns
476
+ -------
477
+ int
478
+ header address
479
+ """
480
+ header_address = int(footer.get("FILE_FEATURE"))
481
+ return header_address
482
+
483
+ def _get_page_addresses(self, footer: ParamsBlock) -> list[int]:
484
+ """Returns list of page addresses.
485
+
486
+ Parameters
487
+ ----------
488
+ footer : dict
489
+ footer parameters
490
+
491
+ Returns
492
+ -------
493
+ list of int
494
+ list of page address
495
+ """
496
+ if type(footer.get("PAGE")) is list:
497
+ page_addresses = list(map(lambda a: int(a), footer.get("PAGE")))
498
+ else:
499
+ page_addresses = [int(footer.get("PAGE"))]
500
+ return page_addresses
501
+
502
+ def _parse_page_block(self, fobj: FileObj, address: int) -> ParamsBlock:
503
+ """Returns parameters in a page block.
504
+
505
+ Parameters
506
+ ----------
507
+ fobj : file
508
+ file object
509
+ address : int
510
+ page block address
511
+
512
+ Returns
513
+ -------
514
+ dict
515
+ parameters in the page block
516
+ """
517
+ return self._parse_metadata_block(fobj, address)
518
+
519
+ def _parse_metadata_block(self, fobj: FileObj, address: int) -> ParamsBlock:
520
+ """Converts metadata block into dict of parameters.
521
+
522
+ Returns empty dict if address equals to 0.
523
+
524
+ Parameters
525
+ ----------
526
+ fobj : file
527
+ file object
528
+ address : int
529
+ metadata block address
530
+
531
+ Returns
532
+ -------
533
+ dict
534
+ extracted parameters
535
+ """
536
+ if address == 0:
537
+ return {}
538
+ fobj.seek(address, os.SEEK_SET)
539
+ block_length = int.from_bytes(fobj.read(fileformat.LENGTH_FIELD_SIZE), "little")
540
+ contents = fobj.read(block_length)
541
+ params = self._extract_parameters(contents.decode())
542
+ return params
543
+
544
+ def _extract_parameters(self, metadata: str) -> ParamsBlock:
545
+ """Returns dict of parameters extracted from metadata.
546
+
547
+ metadata is a repetition of key-value style parameter like
548
+ `<KEY1:VALUE1><KEY2:VALUE2>...`.
549
+
550
+ Parameters
551
+ ----------
552
+ metadata : str
553
+ metadata string
554
+
555
+ Returns
556
+ -------
557
+ dict
558
+ extracted parameters
559
+ """
560
+ pattern = r"<([^:<>]+):([^:<>]*)>"
561
+ result = re.finditer(pattern, metadata)
562
+ params = {}
563
+ for m in result:
564
+ key = m[1]
565
+ value = m[2]
566
+ if params.get(key):
567
+ # the key is duplicate.
568
+ if type(params.get(key)) is not list:
569
+ # To store duplicate parameters, we transform data structure
570
+ # from {key: value} to {key: [value1, value2, ...]}
571
+ first_value = params.pop(key)
572
+ params[key] = [first_value, value]
573
+ else:
574
+ # Data structure have already been transformed.
575
+ # We simply append new value to the list.
576
+ params[key].append(value)
577
+ else:
578
+ params[key] = value
579
+ return params
580
+
581
+
582
+ class SupernoteXParser(SupernoteParser):
583
+ """Parser for Supernote X-series."""
584
+
585
+ SN_SIGNATURE_OFFSET = 4
586
+ SN_SIGNATURE_PATTERN = r"SN_FILE_VER_\d{8}"
587
+ SN_SIGNATURES = [
588
+ "SN_FILE_VER_20200001", # Firmware version C.053
589
+ "SN_FILE_VER_20200005", # Firmware version C.077
590
+ "SN_FILE_VER_20200006", # Firmware version C.130
591
+ "SN_FILE_VER_20200007", # Firmware version C.159
592
+ "SN_FILE_VER_20200008", # Firmware version C.237
593
+ "SN_FILE_VER_20210009", # Firmware version C.291
594
+ "SN_FILE_VER_20210010", # Firmware version Chauvet 2.1.6
595
+ "SN_FILE_VER_20220011", # Firmware version Chauvet 2.5.17
596
+ "SN_FILE_VER_20220013", # Firmware version Chauvet 2.6.19
597
+ "SN_FILE_VER_20230014", # Firmware version Chauvet 2.10.25
598
+ "SN_FILE_VER_20230015", # Firmware version Chauvet 3.14.27
599
+ ]
600
+ LAYER_KEYS = ["MAINLAYER", "LAYER1", "LAYER2", "LAYER3", "BGLAYER"]
601
+
602
+ def _parse_footer_block(self, fobj, address):
603
+ footer = super()._parse_metadata_block(fobj, address)
604
+ # parse keywords
605
+ keyword_addresses = self._get_keyword_addresses(footer)
606
+ keywords = list(
607
+ map(lambda addr: self._parse_keyword_block(fobj, addr), keyword_addresses)
608
+ )
609
+ if keywords:
610
+ footer[fileformat.KEY_KEYWORDS] = keywords
611
+ # parse titles
612
+ title_addresses = self._get_title_addresses(footer)
613
+ titles = list(
614
+ map(lambda addr: self._parse_title_block(fobj, addr), title_addresses)
615
+ )
616
+ if titles:
617
+ footer[fileformat.KEY_TITLES] = titles
618
+ # parse links
619
+ link_addresses = self._get_link_addresses(footer)
620
+ links = list(
621
+ map(lambda addr: self._parse_link_block(fobj, addr), link_addresses)
622
+ )
623
+ if links:
624
+ footer[fileformat.KEY_LINKS] = links
625
+ return footer
626
+
627
+ def _get_keyword_addresses(self, footer):
628
+ keyword_keys = filter(lambda k: k.startswith("KEYWORD_"), footer.keys())
629
+ keyword_addresses = []
630
+ for k in keyword_keys:
631
+ if type(footer[k]) is list:
632
+ keyword_addresses.extend(list(map(int, footer[k])))
633
+ else:
634
+ keyword_addresses.append(int(footer[k]))
635
+ return keyword_addresses
636
+
637
+ def _parse_keyword_block(self, fobj, address):
638
+ return self._parse_metadata_block(fobj, address)
639
+
640
+ def _get_title_addresses(self, footer):
641
+ title_keys = filter(lambda k: k.startswith("TITLE_"), footer.keys())
642
+ title_addresses = []
643
+ for k in title_keys:
644
+ if type(footer[k]) is list:
645
+ title_addresses.extend(list(map(int, footer[k])))
646
+ else:
647
+ title_addresses.append(int(footer[k]))
648
+ return title_addresses
649
+
650
+ def _parse_title_block(self, fobj, address):
651
+ return self._parse_metadata_block(fobj, address)
652
+
653
+ def _get_link_addresses(self, footer):
654
+ link_keys = filter(lambda k: k.startswith("LINK"), footer.keys())
655
+ link_addresses = []
656
+ for k in link_keys:
657
+ if type(footer[k]) is list:
658
+ link_addresses.extend(list(map(int, footer[k])))
659
+ else:
660
+ link_addresses.append(int(footer[k]))
661
+ return link_addresses
662
+
663
+ def _parse_link_block(self, fobj, address):
664
+ return self._parse_metadata_block(fobj, address)
665
+
666
+ def _get_page_addresses(self, footer: ParamsBlock) -> list[int]:
667
+ """Returns list of page addresses.
668
+
669
+ Parameters
670
+ ----------
671
+ footer : dict
672
+ footer parameters
673
+
674
+ Returns
675
+ -------
676
+ list of int
677
+ list of page address
678
+ """
679
+ page_keys = filter(lambda k: k.startswith("PAGE"), footer.keys())
680
+ page_addresses = list(map(lambda k: int(footer[k]), page_keys))
681
+ return page_addresses
682
+
683
+ def _parse_page_block(self, fobj, address):
684
+ """Returns parameters in a page block.
685
+
686
+ Parameters
687
+ ----------
688
+ fobj : file
689
+ file object
690
+ address : int
691
+ page block address
692
+
693
+ Returns
694
+ -------
695
+ dict
696
+ parameters in the page block
697
+ """
698
+ page_info = super()._parse_page_block(fobj, address)
699
+ layer_addresses = self._get_layer_addresses(page_info)
700
+ layers = list(
701
+ map(lambda addr: self._parse_layer_block(fobj, addr), layer_addresses)
702
+ )
703
+ page_info[fileformat.KEY_LAYERS] = layers
704
+ return page_info
705
+
706
+ def _get_layer_addresses(self, page_info):
707
+ """Returns list of layer addresses.
708
+
709
+ Parameters
710
+ ----------
711
+ page_info : dict
712
+ page parameters
713
+
714
+ Returns
715
+ -------
716
+ list of int
717
+ list of layer address
718
+ """
719
+ layer_keys = filter(lambda k: k in self.LAYER_KEYS, page_info)
720
+ layer_addresses = list(map(lambda k: int(page_info[k]), layer_keys))
721
+ return layer_addresses
722
+
723
+ def _parse_layer_block(self, fobj, address):
724
+ """Returns parameters in a layer block.
725
+
726
+ Parameters
727
+ ----------
728
+ fobj : file
729
+ file object
730
+ address : int
731
+ layer block address
732
+
733
+ Returns
734
+ -------
735
+ dict
736
+ parameters in the layer block
737
+ """
738
+ return self._parse_metadata_block(fobj, address)