jbpy.core
Utilities for reading and writing JBP files (NITF, NSIF)
The Field, Group, Subheaders, and Jbp classes have a dictionary-esque interface with key names directly copied from JBP-2025.1 where possible.
In JBP, the presence of optional fields is controlled by the values of preceding fields. This library attempts to mimic this behavior by adding or removing fields as necessary when a field is updated. For example adding image segments is accomplished by setting the NUMI field.
Setting the value of fields is done using the value property. value uses common python
types (int, str, etc...) and serializes to the BIIF format behind the scenes.
1"""Utilities for reading and writing JBP files (NITF, NSIF) 2 3The Field, Group, Subheaders, and Jbp classes have a dictionary-esque interface 4with key names directly copied from JBP-2025.1 where possible. 5 6In JBP, the presence of optional fields is controlled by the values of preceding 7fields. This library attempts to mimic this behavior by adding or removing fields 8as necessary when a field is updated. For example adding image segments is accomplished 9by setting the NUMI field. 10 11Setting the value of fields is done using the `value` property. `value` uses common python 12types (int, str, etc...) and serializes to the BIIF format behind the scenes. 13""" 14 15import abc 16import collections.abc 17import copy 18import datetime 19import importlib.metadata 20import io 21import json 22import logging 23import os 24import re 25from collections.abc import Callable, Iterable 26from typing import Any, Final, Iterator, Literal, Self 27 28logger = logging.getLogger(__name__) 29 30LRESH_MIN = 200 # minimum length of reserved extension subheader 31 32 33class BinaryFile_R: 34 """Binary file-like object supporting reading""" 35 36 @abc.abstractmethod 37 def seek(self, __offset: int, __whence: int = ...) -> int: ... 38 @abc.abstractmethod 39 def read(self, __length: int = ...) -> bytes: ... 40 41 42class BinaryFile_RW(BinaryFile_R): 43 """Binary file-like object supporting reading and writing""" 44 45 @abc.abstractmethod 46 def write(self, __data: bytes) -> int: ... 47 48 49class SubFile: 50 """File-like object mapping to a contiguous subset of another file-like object 51 52 Parameters 53 ---------- 54 file : file-like 55 An open file object. Must be binary. 56 start : int 57 Start byte offset of the subfile 58 length : int 59 Number of bytes to expose from the start 60 """ 61 62 def __init__(self, file: Any, start: int, length: int): 63 self._file = file 64 self._start = start 65 self._length = length 66 self._pos = 0 # position within the subfile 67 68 def seek(self, offset: int, whence: int = 0) -> int: 69 """ 70 Seek to a position within the subfile. 71 72 Parameters 73 ---------- 74 offset : int 75 Offset to seek 76 whence : int 77 0 (start), 1 (current), or 2 (end of subfile) 78 79 Returns 80 ------- 81 int 82 Current offset in the SubFile 83 """ 84 if whence == 0: 85 new_pos = offset 86 elif whence == 1: 87 new_pos = self._pos + offset 88 elif whence == 2: 89 new_pos = self._length + offset 90 else: 91 raise ValueError(f"whence value {whence} unsupported") 92 93 if new_pos < 0: 94 raise OSError("Seek before start of subfile.") 95 96 self._pos = new_pos 97 return self._pos 98 99 def tell(self) -> int: 100 """Return the current position within the subfile.""" 101 return self._pos 102 103 def read(self, size: int = -1) -> bytes: 104 """ 105 Read data from the subfile. 106 107 Parameters 108 ---------- 109 size : int 110 Number of bytes to read, or -1 for all remaining 111 """ 112 if self._pos >= self._length: 113 return b"" 114 115 read_len = ( 116 self._length - self._pos 117 if size < 0 118 else min(size, self._length - self._pos) 119 ) 120 self._file.seek(self._start + self._pos) 121 data = self._file.read(read_len) 122 self._pos += len(data) 123 return data 124 125 def readinto(self, b) -> int | None: 126 if self._pos >= self._length: 127 return 0 128 self._file.seek(self._start + self._pos) 129 bytes_remaining = self._length - self._pos 130 v = memoryview(b) 131 num_read = self._file.readinto(v[:bytes_remaining]) 132 if num_read is not None: 133 self._pos += num_read 134 return num_read 135 136 def readline(self, size=-1) -> bytes: 137 if self._pos >= self._length: 138 return b"" 139 self._file.seek(self._start + self._pos) 140 bytes_remaining = self._length - self._pos 141 _sz = bytes_remaining if size == -1 else min(bytes_remaining, size) 142 data = self._file.readline(_sz) 143 self._pos += len(data) 144 return data 145 146 def readlines(self, hint=-1) -> list[bytes]: 147 if self._pos >= self._length: 148 return [] 149 self._file.seek(self._start + self._pos) 150 line = self.readline() 151 n = len(line) 152 lines = [line] 153 154 while line and (n < hint or (hint <= 0 or hint is None)): 155 line = self.readline() 156 if line: 157 lines.append(line) 158 n += len(line) 159 return lines 160 161 def readable(self) -> bool: 162 return self._file.readable() 163 164 165class PythonConverter(abc.ABC): 166 """Abstract base class for converting between JBP field bytes and python types""" 167 168 def to_bytes(self, decoded_value: Any, size: int) -> bytes: 169 """Convert python type to bytes 170 171 Parameters 172 ---------- 173 decoded_value 174 Value to convert 175 size : int 176 Minimum field width in bytes 177 178 Returns 179 ------- 180 bytes 181 Encoded value 182 """ 183 return self.to_bytes_impl(decoded_value, size) 184 185 @abc.abstractmethod 186 def to_bytes_impl(self, decoded_value: Any, size: int) -> bytes: 187 """Convert python type to bytes""" 188 189 def from_bytes(self, encoded_value: bytes) -> Any: 190 """Convert bytes to python type""" 191 return self.from_bytes_impl(encoded_value) 192 193 @abc.abstractmethod 194 def from_bytes_impl(self, encoded_value) -> Any: 195 """Convert bytes to python type""" 196 197 198class StringUtf8(PythonConverter): 199 """Convert to/from UTF-8 str""" 200 201 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 202 return decoded_value.encode().ljust(size) 203 204 def from_bytes_impl(self, encoded_value: bytes) -> str: 205 return encoded_value.decode().rstrip(" ") 206 207 208class StringAscii(PythonConverter): 209 """Convert to/from ASCII str""" 210 211 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 212 return decoded_value.encode("ascii").ljust(size) 213 214 def from_bytes_impl(self, encoded_value: bytes) -> str: 215 return encoded_value.decode("ascii").rstrip(" ") 216 217 218class StringISO8859_1(PythonConverter): # noqa: N801 219 """Convert to/from an ISO 8859-1 str 220 221 Notes 222 ----- 223 JBP-2025.1 Table D-1 specifies the full ECS-A character set, which 224 happens to match ISO 8859 part 1. 225 """ 226 227 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 228 return decoded_value.encode("iso8859_1").ljust(size) 229 230 def from_bytes_impl(self, encoded_value: bytes) -> str: 231 return encoded_value.decode("iso8859_1").rstrip(" ") 232 233 234class IntPair(PythonConverter): 235 """convert to/from two int tuple""" 236 237 def to_bytes_impl(self, decoded_value: tuple[int, int], size: int) -> bytes: 238 if (size < 2) or (size % 2): 239 raise ValueError(f"invalid {size=}; must be positive and even") 240 length = size // 2 241 return f"{decoded_value[0]:0{length}d}{decoded_value[1]:0{length}d}".encode() 242 243 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int]: 244 length = len(encoded_value) // 2 245 return (int(encoded_value[0:length]), int(encoded_value[length:])) 246 247 248class Bytes(PythonConverter): 249 """Convert to/from bytes""" 250 251 def to_bytes_impl(self, decoded_value: bytes, size: int) -> bytes: 252 if len(decoded_value) < size: 253 raise ValueError(f"{len(decoded_value)=} must be at least {size=}") 254 return decoded_value 255 256 def from_bytes_impl(self, encoded_value: bytes) -> bytes: 257 return encoded_value 258 259 260class Integer(PythonConverter): 261 """Convert to/from int 262 263 Parameters 264 ---------- 265 sign : {'+', '-', space}, optional 266 When to encode with a sign. The meaning of ``sign`` is the same as the meaning of the sign option 267 in python's string format specification mini-language: 268 269 * '+': a sign should be used for positive and negative numbers 270 * '-': a sign should be used for negative numbers only 271 * space: a leading space should be used for positive and a minus sign on negative numbers 272 """ 273 274 def __init__(self, sign: Literal["+", "-", " "] = "-"): 275 self.sign = sign 276 277 def to_bytes_impl(self, decoded_value: int, size: int) -> bytes: 278 decoded_value = int(decoded_value) 279 return f"{decoded_value:{self.sign}0{size}}".encode() 280 281 def from_bytes_impl(self, encoded_value: bytes) -> int: 282 return int(encoded_value) 283 284 285class RGB(PythonConverter): 286 """convert to/from three int tuple""" 287 288 def to_bytes_impl(self, decoded_value: tuple[int, int, int], size: int) -> bytes: 289 assert size == 3 290 return ( 291 decoded_value[0].to_bytes(1, "big") 292 + decoded_value[1].to_bytes(1, "big") 293 + decoded_value[2].to_bytes(1, "big") 294 ) 295 296 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int, int]: 297 return (encoded_value[0], encoded_value[1], encoded_value[2]) 298 299 300# Character sets (see 4.6.3.1) 301# Extended Character Set (ECS) 302ECS = "\x20-\x7e\xa0-\xff\x0a\x0c\x0d" 303# Extended Character Set - Alphanumeric (ECS-A) 304ECSA = "\x20-\x7e\xa0-\xff" 305# Basic Character Set (BCS) 306BCS = "\x20-\x7e\x0a\x0c\x0d" 307# Basic Character Set - Alphanumeric (BCS-A) 308BCSA = "\x20-\x7e" 309# Basic Character Set - Numeric (BCS-N) 310BCSN = "\x30-\x39\x2b\x2d\x2e\x2f" 311# Basic Character Set - Numeric Integer (BCS-N integer) 312BCSN_I = "\x30-\x39\x2b\x2d" 313# Basic Character Set - Numeric Positive Integer (BCS-N positive integer) 314BCSN_PI = "\x30-\x39" 315# UTF-8 316U8 = "\x00-\xff" 317 318# All the spaces 319BCSA_SPACE = ECSA_SPACE = "\x20" 320 321 322class RangeCheck(abc.ABC): 323 """Base Class for checking the range of a JBP field""" 324 325 @abc.abstractmethod 326 def isvalid(self, value: Any) -> bool: 327 """Returns ``True`` if field satisfies range check.""" 328 329 330class AnyRange(RangeCheck): 331 """Field has no range restrictions""" 332 333 def isvalid(self, value: Any) -> bool: 334 return True 335 336 337class MinMax(RangeCheck): 338 """Field has a minimum and/or maximum value 339 340 Parameters 341 ---------- 342 minimum 343 Minimum value. A value of 'None' indicates no minimum. 344 maximum 345 Maximum value. A value of 'None' indicates no maximum. 346 """ 347 348 def __init__(self, minimum: int | float | None, maximum: int | float | None): 349 self.minimum = minimum 350 self.maximum = maximum 351 352 def isvalid(self, value: int | float) -> bool: 353 valid = True 354 if self.minimum is not None: 355 valid &= value >= self.minimum 356 if self.maximum is not None: 357 valid &= value <= self.maximum 358 return valid 359 360 361class Regex(RangeCheck): 362 """Field value is restricted by a regex""" 363 364 def __init__(self, pattern: str): 365 self.pattern = pattern 366 367 def isvalid(self, value: str) -> bool: 368 return bool(re.fullmatch(self.pattern, value)) 369 370 371class Constant(RangeCheck): 372 """Field value must be a constant""" 373 374 def __init__(self, const: Any): 375 self.const = const 376 377 def isvalid(self, value: Any) -> bool: 378 return value == self.const 379 380 381class Enum(RangeCheck): 382 """Field value must match one value of an Enumeration""" 383 384 def __init__(self, enumeration: Iterable): 385 self.enumeration = set(enumeration) 386 387 def isvalid(self, value: Any) -> bool: 388 return value in self.enumeration 389 390 391class AnyOf(RangeCheck): 392 """Field value must match at least one of many different RangeChecks 393 394 Parameters 395 ---------- 396 *ranges: RangeCheck 397 RangeCheck objects to check against 398 """ 399 400 def __init__(self, *ranges: RangeCheck): 401 self.ranges = ranges 402 403 def isvalid(self, value: Any) -> bool: 404 # Use any(generator) to ensure short circuit logic 405 return any(check.isvalid(value) for check in self.ranges) 406 407 408class AllOf(RangeCheck): 409 """Field value must match all of many different RangeChecks 410 411 Parameters 412 ---------- 413 *ranges: RangeCheck 414 RangeCheck objects to check against 415 """ 416 417 def __init__(self, *ranges: RangeCheck): 418 self.ranges = ranges 419 420 def isvalid(self, value: Any) -> bool: 421 # Use all(generator) to ensure short circuit logic 422 return all(check.isvalid(value) for check in self.ranges) 423 424 425class Not(RangeCheck): 426 """Negate a range check""" 427 428 def __init__(self, range_check: RangeCheck): 429 self.range_check = range_check 430 431 def isvalid(self, value: Any) -> bool: 432 return not self.range_check.isvalid(value) 433 434 435# Common Regex patterns 436PATTERN_CC = "[0-9]{2}" 437PATTERN_YY = "[0-9]{2}" 438PATTERN_MM = "(0[1-9]|1[0-2])" # MM 439PATTERN_DD = "(0[1-9]|[12][0-9]|3[0-1])" # DD 440PATTERN_HH = "([0-1][0-9]|2[0-3])" # hh 441PATTERN_mm = "([0-5][0-9])" # mm 442PATTERN_SS = "([0-5][0-9])" # ss 443DATETIME_REGEX = Regex( 444 f"({PATTERN_CC}|--)" 445 + f"({PATTERN_YY}|--)" 446 + f"({PATTERN_MM}|--)" 447 + f"({PATTERN_DD}|--)" 448 + f"({PATTERN_HH}|--)" 449 + f"({PATTERN_mm}|--)" 450 + f"({PATTERN_SS}|--)" 451) 452DATE_REGEX = Regex(PATTERN_CC + PATTERN_YY + PATTERN_MM + PATTERN_DD) 453 454 455class JbpIOComponent: 456 """Base Class for read/writable JBP components""" 457 458 def __init__(self, name: str): 459 self.name = name 460 self._parent: ComponentCollection | None = None 461 462 def load(self, fd: BinaryFile_R) -> Self: 463 """Read from a file descriptor 464 465 Parameters 466 ---------- 467 fd : file-like 468 Binary file-like object to read from 469 470 Returns 471 ------- 472 A reference to self 473 """ 474 try: 475 self._load_impl(fd) 476 return self 477 except Exception: 478 logger.error(f"Failed to read {self.name}") 479 raise 480 481 def dump(self, fd: BinaryFile_RW, seek_first: bool = False) -> int: 482 """Write to a file descriptor 483 484 Parameters 485 ---------- 486 fd : file-like 487 Binary file-like object to write to 488 seek_first : bool 489 Seek to the components offset before writing 490 491 Returns 492 ------- 493 int 494 Number of bytes written 495 """ 496 if seek_first: 497 fd.seek(self.get_offset(), os.SEEK_SET) 498 499 try: 500 return self._dump_impl(fd) 501 except Exception: 502 logger.error(f"Failed to write {self.name}") 503 raise 504 505 def _load_impl(self, fd: BinaryFile_R) -> None: 506 raise NotImplementedError() 507 508 def _dump_impl(self, fd: BinaryFile_RW) -> int: 509 raise NotImplementedError() 510 511 def get_offset(self) -> int: 512 """Return the offset from the start of the file to this component""" 513 offset = 0 514 if self._parent is not None: 515 offset = self._parent.get_offset_of(self) 516 return offset 517 518 def get_size(self) -> int: 519 """Size of this component in bytes""" 520 raise NotImplementedError() 521 522 def as_json(self, full: bool = False) -> str: 523 """Return a JSON representation of the component 524 525 Parameters 526 ---------- 527 full : bool 528 Include additional details such as offset and length 529 """ 530 return json.dumps(self, indent=2, cls=_JsonEncoder, full_details=full) 531 532 def as_text(self) -> str: 533 """Return a text representation of the component""" 534 buf = io.StringIO() 535 self.print(file=buf) 536 return buf.getvalue() 537 538 def print(self, *, file=None) -> None: 539 """Print information about the component to stdout""" 540 raise NotImplementedError() 541 542 def finalize(self): 543 """Perform any necessary final updates""" 544 545 def as_filelike(self, file: Any) -> SubFile: 546 """Create file object containing just this component 547 548 Parameters 549 ---------- 550 file : file-like 551 File object for entire file 552 553 Returns 554 ------- 555 SubFile 556 File like object for this component 557 """ 558 return SubFile(file, self.get_offset(), self.get_size()) 559 560 561class Field(JbpIOComponent): 562 """JBP Field containing a single value. 563 Intended to have 1:1 mapping to rows in JBP-2025.1 header tables. 564 565 Parameters 566 ---------- 567 name : str 568 Name of this field 569 description : str 570 Text description of the field 571 size : int 572 Size in bytes of the field 573 charset : str or None, optional 574 regex expression matching a single character. If ``None``, character set check is skipped. 575 encoded_range : RangeCheck or None, optional 576 Checker for the encoded value. If ``None``, encoded validation is skipped. 577 decoded_range : RangeCheck or None, optional 578 Checker for the decoded value. If ``None``, decoded validation is skipped. 579 converter : PythonConverter 580 Object to use for converting to/from python data types 581 default : any 582 Initial python value of the field 583 setter_callback : callable or None, optional 584 function to call if the field's value changes 585 nullable : bool, optional 586 ``True`` if BCS-A spaces are allowed for entire field (often denoted with "<>" in JBP Field Type). 587 When ``True``, charset, range checks, conversion, etc. are bypassed when the python-typed value is ``None``. 588 589 Attributes 590 ---------- 591 description: str 592 Text description of the field. For informational purposes only. 593 size: int 594 Field size in bytes 595 nullable: bool 596 ``True`` if BCS-A spaces are allowed for entire field 597 encoded_value: bytes 598 Field value as bytes 599 value 600 Field value as python type 601 """ 602 603 def __init__( 604 self, 605 name: str, 606 description: str, 607 size: int, 608 *, 609 charset: str | None = None, 610 encoded_range: RangeCheck | None = None, 611 decoded_range: RangeCheck | None = None, 612 converter: PythonConverter, 613 default: Any, 614 setter_callback: Callable | None = None, 615 nullable: bool = False, 616 ): 617 super().__init__(name) 618 self.description = description 619 self.nullable = nullable 620 self._size = size 621 self._charset = charset 622 self._encoded_range_check = encoded_range 623 self._decoded_range_check = decoded_range 624 self._converter = converter 625 self._setter_callback = setter_callback 626 627 encoded_default = self._encode(default) 628 if len(encoded_default) != size: 629 raise ValueError( 630 f"Field {name} {default=} does not encode to the proper {size=}" 631 ) 632 self._encoded_value = encoded_default 633 634 def __eq__(self, other): 635 if not isinstance(other, type(self)): 636 return NotImplemented 637 638 return ( 639 self.name == other.name 640 and self.description == other.description 641 and self._charset == other._charset 642 and self.encoded_value == other.encoded_value 643 ) 644 645 def _encode(self, val: Any) -> bytes: 646 if self.nullable and val is None: 647 return BCSA_SPACE.encode() * self.size 648 return self._converter.to_bytes(val, self.size) 649 650 def isnull(self) -> bool: 651 """Return True if Field is nullable and all bytes are BCS spaces""" 652 return self.nullable and self.encoded_value == BCSA_SPACE.encode() * len( 653 self.encoded_value 654 ) 655 656 def isvalid(self) -> bool: 657 """Check if the field value matches the required character set and range restrictions""" 658 if self.isnull(): 659 return True 660 661 if self._charset is not None: 662 valid_charset = bool( 663 re.fullmatch(f"[{self._charset}]*", self.encoded_value.decode()) 664 ) 665 if not valid_charset: 666 return False 667 668 if self._encoded_range_check is not None: 669 valid_encoding = self._encoded_range_check.isvalid(self.encoded_value) 670 if not valid_encoding: 671 return False 672 673 if self._decoded_range_check is not None: 674 valid_decoding = self._decoded_range_check.isvalid(self.value) 675 if not valid_decoding: 676 return False 677 678 return True 679 680 @property 681 def encoded_value(self) -> bytes: 682 return self._encoded_value 683 684 @encoded_value.setter 685 def encoded_value(self, value: bytes): 686 truncated = value[: self.size] 687 if len(truncated) < len(value): 688 logger.warning( 689 f"JBP header field {self.name} truncated to {self.size} characters.\n" 690 f" old: {value!r}" 691 f" new: {truncated!r}" 692 ) 693 self._encoded_value = truncated 694 695 try: 696 if not self.isvalid(): 697 logger.warning( 698 f"{self.name}: Invalid field value: {self.encoded_value!r}" 699 ) 700 except Exception: 701 logger.exception( 702 f"An exception occurred when trying to validate {self.name}:" 703 ) 704 705 @property 706 def size(self) -> int: 707 return self._size 708 709 @size.setter 710 def size(self, value: int): 711 old_value = self._size 712 self._size = value 713 714 if (old_value != self._size) and self._setter_callback: 715 self._setter_callback(self) 716 717 @property 718 def value(self) -> Any: 719 if self.isnull(): 720 return None 721 return self._converter.from_bytes(self.encoded_value) 722 723 @value.setter 724 def value(self, val: Any): 725 self._set_value(val, callback=self._setter_callback) 726 727 def _set_value(self, val, callback=None): 728 self.encoded_value = self._encode(val) 729 730 if callback: 731 callback(self) 732 733 def _load_impl(self, fd: BinaryFile_R) -> None: 734 self.encoded_value = fd.read(self.size) 735 736 if self._setter_callback: 737 self._setter_callback(self) 738 739 def _dump_impl(self, fd: BinaryFile_RW) -> int: 740 return fd.write(self.encoded_value) 741 742 def get_size(self) -> int: 743 return self.size 744 745 def print(self, *, file=None) -> None: 746 print( 747 f"{self.name:15}{self.size:11} @ {self.get_offset():11} {self.encoded_value!r}", 748 file=file, 749 ) 750 751 752class BinaryPlaceholder(JbpIOComponent): 753 """Represents a block of large binary data. 754 755 This class does not actually read, write or store data, only seek past it. 756 """ 757 758 def __init__(self, name: str, size: int): 759 super().__init__(name) 760 self._size = size 761 762 def __eq__(self, other): 763 if not isinstance(other, type(self)): 764 return NotImplemented 765 766 return self.name == other.name and self._size == other._size 767 768 @property 769 def size(self) -> int: 770 return self._size 771 772 @size.setter 773 def size(self, value: int): 774 self._size = value 775 776 def _load_impl(self, fd: BinaryFile_R): 777 fd.seek(self.size, os.SEEK_CUR) 778 779 def _dump_impl(self, fd: BinaryFile_RW) -> int: 780 if self.size: 781 fd.seek(self.size, os.SEEK_CUR) 782 return self.size 783 784 def get_size(self) -> int: 785 return self.size 786 787 def print(self, *, file=None) -> None: 788 print( 789 f"{self.name:15}{self.size:11} @ {self.get_offset():11} <Binary>", file=file 790 ) 791 792 793class ComponentCollection(JbpIOComponent): 794 """Base class for components with child sub-components""" 795 796 def __init__(self, name: str): 797 super().__init__(name) 798 self._children: Final[list[JbpIOComponent]] = [] 799 800 def __eq__(self, other): 801 if not isinstance(other, type(self)): 802 return NotImplemented 803 804 return len(self._children) == len(other._children) and all( 805 [left == right for left, right in zip(self._children, other._children)] 806 ) 807 808 def __len__(self) -> int: 809 return len(self._children) 810 811 def _contains(self, item): 812 in_children = item in self._children 813 is_parent_set = item._parent == self 814 assert in_children == is_parent_set 815 return in_children 816 817 def get_size(self) -> int: 818 size = 0 819 for child in self._children: 820 size += child.get_size() 821 return size 822 823 def _load_impl(self, fd: BinaryFile_R) -> None: 824 for child in self._children: 825 child.load(fd) 826 827 def _dump_impl(self, fd: BinaryFile_RW) -> int: 828 written = 0 829 for child in self._children: 830 written += child.dump(fd) 831 return written 832 833 def _append(self, field: JbpIOComponent) -> None: 834 field._parent = self 835 self._children.append(field) 836 837 def _extend(self, fields: Iterable[JbpIOComponent]) -> None: 838 for field in fields: 839 self._append(field) 840 841 def _replace(self, old_field: JbpIOComponent, new_field: JbpIOComponent) -> None: 842 if not self._contains(old_field): 843 raise ValueError("old_field must be in collection") 844 if new_field._parent is not None: 845 raise ValueError("new_field already has a parent") 846 self._children[self._children.index(old_field)] = new_field 847 new_field._parent = self 848 849 def get_offset_of(self, child_obj: JbpIOComponent) -> int: 850 offset = self.get_offset() 851 852 for child in self._children: 853 if child is child_obj: 854 return offset 855 else: 856 offset += child.get_size() 857 else: 858 raise ValueError(f"Could not find {child_obj.name}") 859 860 def print(self, *, file=None) -> None: 861 for child in self._children: 862 child.print(file=file) 863 864 def finalize(self): 865 for child in self._children: 866 child.finalize() 867 868 869class Group(ComponentCollection, collections.abc.Mapping): 870 """ 871 A Collection of JBP fields. Indexed by JBP short names. 872 873 Parameters 874 ---------- 875 name : str 876 Name to give the group of fields 877 """ 878 879 def __init__(self, name): 880 super().__init__(name) 881 882 def _child_names(self) -> list[str]: 883 return [child.name for child in self._children] 884 885 def __iter__(self): 886 return iter(self._child_names()) 887 888 def __getitem__(self, key: str): 889 try: 890 index = self._index(key) 891 except ValueError: 892 raise KeyError(key) 893 894 return self._children[index] 895 896 def _insert_after( 897 self, existing: JbpIOComponent, *field: JbpIOComponent 898 ) -> JbpIOComponent: 899 insert_pos = self._children.index(existing) + 1 900 self._children[insert_pos:insert_pos] = field 901 for f in field: 902 f._parent = self 903 return f 904 905 def find_all(self, pattern: str) -> Iterator[JbpIOComponent]: 906 """Find child components with names matching a regex pattern 907 908 Parameters 909 ---------- 910 pattern : str 911 Regex pattern 912 913 Yields 914 ------ 915 child with name matching `pattern` 916 """ 917 for child in self._children[:]: 918 if re.fullmatch(pattern, child.name): 919 yield child 920 921 def _remove_all(self, pattern: str) -> None: 922 for child in self.find_all(pattern): 923 self._children.remove(child) 924 925 def _index(self, name: str) -> int: 926 return self._child_names().index(name) 927 928 929class SegmentList(ComponentCollection, collections.abc.Sequence): 930 """A sequence of JBP segments""" 931 932 def __init__( 933 self, 934 name: str, 935 field_creator: Callable[[str], Group], 936 minimum: int = 0, 937 maximum: int = 1, 938 ): 939 super().__init__(name) 940 self.field_creator = field_creator 941 self.minimum = minimum 942 self.maximum = maximum 943 self.set_count(self.minimum) 944 945 def __getitem__(self, idx): 946 return self._children[idx] 947 948 def set_count(self, size: int) -> None: 949 if not self.minimum <= size <= self.maximum: 950 raise ValueError(f"Invalid {size=}") 951 for idx in range(len(self._children), size): 952 new_field = self.field_creator(str(idx + 1)) 953 self._append(new_field) 954 for _ in range(size, len(self._children)): 955 self._children.pop() 956 957 958class SecurityFields(Group): 959 """ 960 JBP security header/subheader fields 961 962 Parameters 963 ---------- 964 name : str 965 Name to give this component 966 x : str 967 Value to replace leading "x" of Short Name in fields 968 969 Notes 970 ----- 971 See JBP-2025.1 Table 5.10-1 and Table 5.10-2 972 """ 973 974 def __init__(self, name: str, x: str): 975 super().__init__(name) 976 self._append( 977 Field( 978 f"{x}SCLAS", 979 "Security Classification", 980 1, 981 charset=ECSA, 982 decoded_range=Enum(["T", "S", "C", "R", "U"]), 983 converter=StringISO8859_1(), 984 default="U", 985 ) 986 ) 987 self._append( 988 Field( 989 f"{x}SCLSY", 990 "Security Classification System", 991 2, 992 charset=ECSA, 993 converter=StringISO8859_1(), 994 default=None, 995 nullable=True, 996 ) 997 ) 998 self._append( 999 Field( 1000 f"{x}SCODE", 1001 "Codewords", 1002 11, 1003 charset=ECSA, 1004 converter=StringISO8859_1(), 1005 default=None, 1006 nullable=True, 1007 ) 1008 ) 1009 self._append( 1010 Field( 1011 f"{x}SCTLH", 1012 "Control and Handling", 1013 2, 1014 charset=ECSA, 1015 converter=StringISO8859_1(), 1016 default=None, 1017 nullable=True, 1018 ) 1019 ) 1020 self._append( 1021 Field( 1022 f"{x}SREL", 1023 "Releasing Instructions", 1024 20, 1025 charset=ECSA, 1026 converter=StringISO8859_1(), 1027 default=None, 1028 nullable=True, 1029 ) 1030 ) 1031 self._append( 1032 Field( 1033 f"{x}SDCTP", 1034 "Declassification Type", 1035 2, 1036 charset=ECSA, 1037 decoded_range=Enum(["DD", "DE", "GD", "GE", "O", "X"]), 1038 converter=StringISO8859_1(), 1039 default=None, 1040 nullable=True, 1041 ) 1042 ) 1043 self._append( 1044 Field( 1045 f"{x}SDCDT", 1046 "Declassification Date", 1047 8, 1048 charset=ECSA, 1049 decoded_range=DATE_REGEX, 1050 converter=StringISO8859_1(), 1051 default=None, 1052 nullable=True, 1053 ) 1054 ) 1055 self._append( 1056 Field( 1057 f"{x}SDCXM", 1058 "Declassification Exemption", 1059 4, 1060 charset=ECSA, 1061 converter=StringISO8859_1(), 1062 default=None, 1063 nullable=True, 1064 ) 1065 ) 1066 self._append( 1067 Field( 1068 f"{x}SDG", 1069 "Downgrade", 1070 1, 1071 charset=ECSA, 1072 decoded_range=Enum(["S", "C", "R"]), 1073 converter=StringISO8859_1(), 1074 default=None, 1075 nullable=True, 1076 ) 1077 ) 1078 self._append( 1079 Field( 1080 f"{x}SDGDT", 1081 "Downgrade Date", 1082 8, 1083 charset=ECSA, 1084 decoded_range=DATE_REGEX, 1085 converter=StringISO8859_1(), 1086 default=None, 1087 nullable=True, 1088 ) 1089 ) 1090 self._append( 1091 Field( 1092 f"{x}SCLTX", 1093 "Classification Text", 1094 43, 1095 charset=ECSA, 1096 converter=StringISO8859_1(), 1097 default=None, 1098 nullable=True, 1099 ) 1100 ) 1101 self._append( 1102 Field( 1103 f"{x}SCATP", 1104 "Classification Authority Type", 1105 1, 1106 charset=ECSA, 1107 decoded_range=Enum(["O", "D", "M"]), 1108 converter=StringISO8859_1(), 1109 default=None, 1110 nullable=True, 1111 ) 1112 ) 1113 self._append( 1114 Field( 1115 f"{x}SCAUT", 1116 "Classification Authority", 1117 40, 1118 charset=ECSA, 1119 converter=StringISO8859_1(), 1120 default=None, 1121 nullable=True, 1122 ) 1123 ) 1124 self._append( 1125 Field( 1126 f"{x}SCRSN", 1127 "Classification Reason", 1128 1, 1129 charset=ECSA, 1130 converter=StringISO8859_1(), 1131 default=None, 1132 nullable=True, 1133 ) 1134 ) 1135 self._append( 1136 Field( 1137 f"{x}SSRDT", 1138 "Security Source Date", 1139 8, 1140 charset=ECSA, 1141 decoded_range=DATE_REGEX, 1142 converter=StringISO8859_1(), 1143 default=None, 1144 nullable=True, 1145 ) 1146 ) 1147 self._append( 1148 Field( 1149 f"{x}SCTLN", 1150 "Security Control Number", 1151 15, 1152 charset=ECSA, 1153 converter=StringISO8859_1(), 1154 default=None, 1155 nullable=True, 1156 ) 1157 ) 1158 1159 1160class FileHeader(Group): 1161 """ 1162 JBP File Header 1163 1164 Parameters 1165 ---------- 1166 name : str 1167 Name to give the object 1168 numi_callback : callable 1169 Function to call when NUMI changes 1170 lin_callback : callable 1171 Function to call when LIn changes 1172 nums_callback : callable 1173 Function to call when NUMS changes 1174 lsn_callback : callable 1175 Function to call when LSn changes 1176 numt_callback : callable 1177 Function to call when NUMT changes 1178 ltn_callback : callable 1179 Function to call when LTn changes 1180 numdes_callback : callable 1181 Function to call when NUMDES changes 1182 ldn_callback : callable 1183 Function to call when LDn changes 1184 numres_callback : callable 1185 Function to call when NUMRES changes 1186 lreshn_callback : callable 1187 Function to call when LRESHn changes 1188 lren_callback : callable 1189 Function to call when LREn changes 1190 1191 Notes 1192 ----- 1193 See JBP-2025.1 Table 5.11-1 1194 """ 1195 1196 def __init__( 1197 self, 1198 name: str, 1199 numi_callback: Callable | None = None, 1200 lin_callback: Callable | None = None, 1201 nums_callback: Callable | None = None, 1202 lsn_callback: Callable | None = None, 1203 numt_callback: Callable | None = None, 1204 ltn_callback: Callable | None = None, 1205 numdes_callback: Callable | None = None, 1206 ldn_callback: Callable | None = None, 1207 numres_callback: Callable | None = None, 1208 lreshn_callback: Callable | None = None, 1209 lren_callback: Callable | None = None, 1210 ): 1211 super().__init__(name) 1212 self.numi_callback = numi_callback 1213 self.lin_callback = lin_callback 1214 self.nums_callback = nums_callback 1215 self.lsn_callback = lsn_callback 1216 self.numt_callback = numt_callback 1217 self.ltn_callback = ltn_callback 1218 self.numdes_callback = numdes_callback 1219 self.ldn_callback = ldn_callback 1220 self.numres_callback = numres_callback 1221 self.lreshn_callback = lreshn_callback 1222 self.lren_callback = lren_callback 1223 1224 # Initialize list with required fields 1225 self._append( 1226 Field( 1227 "FHDR", 1228 "File Profile Name", 1229 4, 1230 charset=BCSA, 1231 decoded_range=Enum(["NITF", "NSIF"]), 1232 converter=StringAscii(), 1233 default="NITF", 1234 ) 1235 ) 1236 self._append( 1237 Field( 1238 "FVER", 1239 "File Version", 1240 5, 1241 charset=BCSA, 1242 decoded_range=Enum(["02.10", "01.01"]), 1243 converter=StringAscii(), 1244 default="02.10", 1245 ) 1246 ) 1247 self._append( 1248 Field( 1249 "CLEVEL", 1250 "Complexity Level", 1251 2, 1252 charset=BCSN_PI, 1253 decoded_range=MinMax(1, 99), 1254 converter=Integer(), 1255 default=99, 1256 ) 1257 ) 1258 self._append( 1259 Field( 1260 "STYPE", 1261 "Standard Type", 1262 4, 1263 charset=BCSA, 1264 decoded_range=Constant("BF01"), 1265 converter=StringAscii(), 1266 default="BF01", 1267 ) 1268 ) 1269 self._append( 1270 Field( 1271 "OSTAID", 1272 "Originating Station ID", 1273 10, 1274 charset=BCSA, 1275 decoded_range=Not(Constant("")), 1276 converter=StringAscii(), 1277 default="unknown", 1278 ) 1279 ) 1280 self._append( 1281 Field( 1282 "FDT", 1283 "File Date and Time", 1284 14, 1285 charset=BCSN_I, 1286 decoded_range=DATETIME_REGEX, 1287 converter=StringAscii(), 1288 default="-" * 14, 1289 ) 1290 ) 1291 self._append( 1292 Field( 1293 "FTITLE", 1294 "File Title", 1295 80, 1296 charset=ECSA, 1297 converter=StringISO8859_1(), 1298 default=None, 1299 nullable=True, 1300 ) 1301 ) 1302 self._extend(SecurityFields("File Header Security Fields", "F").values()) 1303 self._append( 1304 Field( 1305 "FSCOP", 1306 "File Copy Number", 1307 5, 1308 charset=BCSN_PI, 1309 converter=Integer(), 1310 default=0, 1311 ) 1312 ) 1313 self._append( 1314 Field( 1315 "FSCPYS", 1316 "File Number of Copies", 1317 5, 1318 charset=BCSN_PI, 1319 converter=Integer(), 1320 default=0, 1321 ) 1322 ) 1323 self._append( 1324 Field( 1325 "ENCRYP", 1326 "Encryption", 1327 1, 1328 charset=BCSN_PI, 1329 decoded_range=Constant(0), 1330 converter=Integer(), 1331 default=0, 1332 ) 1333 ) 1334 self._append( 1335 Field( 1336 "FBKGC", 1337 "File Background Color", 1338 3, 1339 converter=RGB(), 1340 default=(0, 0, 0), 1341 ) 1342 ) 1343 self._append( 1344 Field( 1345 "ONAME", 1346 "Originator's Name", 1347 24, 1348 charset=ECSA, 1349 converter=StringISO8859_1(), 1350 default=None, 1351 nullable=True, 1352 ) 1353 ) 1354 self._append( 1355 Field( 1356 "OPHONE", 1357 "Originator's Phone Number", 1358 18, 1359 charset=ECSA, 1360 converter=StringISO8859_1(), 1361 default=None, 1362 nullable=True, 1363 ) 1364 ) 1365 self._append( 1366 Field( 1367 "FL", 1368 "File Length", 1369 12, 1370 charset=BCSN_PI, 1371 decoded_range=MinMax(388, 999_999_999_998), 1372 converter=Integer(), 1373 default=388, 1374 ) 1375 ) 1376 self._append( 1377 Field( 1378 "HL", 1379 "JBP File Header Length", 1380 6, 1381 charset=BCSN_PI, 1382 decoded_range=MinMax(388, 999_999), 1383 converter=Integer(), 1384 default=388, 1385 ) 1386 ) 1387 self._append( 1388 Field( 1389 "NUMI", 1390 "Number of Image Segments", 1391 3, 1392 charset=BCSN_PI, 1393 converter=Integer(), 1394 default=0, 1395 setter_callback=self._numi_handler, 1396 ) 1397 ) 1398 self._append( 1399 Field( 1400 "NUMS", 1401 "Number of Graphic Segments", 1402 3, 1403 charset=BCSN_PI, 1404 converter=Integer(), 1405 default=0, 1406 setter_callback=self._nums_handler, 1407 ) 1408 ) 1409 self._append( 1410 Field( 1411 "NUMX", 1412 "Reserved for Future Use", 1413 3, 1414 charset=BCSN_PI, 1415 decoded_range=Constant(0), 1416 converter=Integer(), 1417 default=0, 1418 ) 1419 ) 1420 self._append( 1421 Field( 1422 "NUMT", 1423 "Number of Text Segments", 1424 3, 1425 charset=BCSN_PI, 1426 converter=Integer(), 1427 default=0, 1428 setter_callback=self._numt_handler, 1429 ) 1430 ) 1431 self._append( 1432 Field( 1433 "NUMDES", 1434 "Number of Data Extension Segments", 1435 3, 1436 charset=BCSN_PI, 1437 converter=Integer(), 1438 default=0, 1439 setter_callback=self._numdes_handler, 1440 ) 1441 ) 1442 self._append( 1443 Field( 1444 "NUMRES", 1445 "Number of Reserved Extension Segments", 1446 3, 1447 charset=BCSN_PI, 1448 converter=Integer(), 1449 default=0, 1450 setter_callback=self._numres_handler, 1451 ) 1452 ) 1453 self._append( 1454 Field( 1455 "UDHDL", 1456 "User Defined Header Data Length", 1457 5, 1458 charset=BCSN_PI, 1459 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1460 converter=Integer(), 1461 default=0, 1462 setter_callback=self._udhdl_handler, 1463 ) 1464 ) 1465 self._append( 1466 Field( 1467 "XHDL", 1468 "Extended Header Data Length", 1469 5, 1470 charset=BCSN_PI, 1471 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1472 converter=Integer(), 1473 default=0, 1474 setter_callback=self._xhdl_handler, 1475 ) 1476 ) 1477 1478 def _numi_handler(self, field: Field) -> None: 1479 """Handle NUMI value change""" 1480 self._remove_all("LISH\\d+") 1481 self._remove_all("LI\\d+") 1482 after: JbpIOComponent = field 1483 for idx in range(1, field.value + 1): 1484 after = self._insert_after( 1485 after, 1486 Field( 1487 f"LISH{idx:03}", 1488 "Length of nth Image Subheader", 1489 6, 1490 charset=BCSN_PI, 1491 decoded_range=MinMax(439, 999_999), 1492 converter=Integer(), 1493 default=439, 1494 ), 1495 ) 1496 after = self._insert_after( 1497 after, 1498 Field( 1499 f"LI{idx:03}", 1500 "Length of nth Image Segment", 1501 10, 1502 charset=BCSN_PI, 1503 decoded_range=MinMax(1, 10**10 - 1), 1504 converter=Integer(), 1505 setter_callback=self._lin_handler, 1506 default=1, 1507 ), 1508 ) 1509 if self.numi_callback: 1510 self.numi_callback(field) 1511 1512 def _lin_handler(self, field: Field) -> None: 1513 """Handle LIN value change""" 1514 if self.lin_callback: 1515 self.lin_callback(field) 1516 1517 def _nums_handler(self, field: Field) -> None: 1518 self._remove_all("LSSH\\d+") 1519 self._remove_all("LS\\d+") 1520 after: JbpIOComponent = field 1521 for idx in range(1, field.value + 1): 1522 after = self._insert_after( 1523 after, 1524 Field( 1525 f"LSSH{idx:03}", 1526 "Length of nth Graphic Subheader", 1527 4, 1528 charset=BCSN_PI, 1529 decoded_range=MinMax(258, 999_999), 1530 converter=Integer(), 1531 default=258, 1532 ), 1533 ) 1534 after = self._insert_after( 1535 after, 1536 Field( 1537 f"LS{idx:03}", 1538 "Length of nth Graphic Segment", 1539 6, 1540 charset=BCSN_PI, 1541 decoded_range=MinMax(1, 10**10 - 1), 1542 converter=Integer(), 1543 setter_callback=self._lsn_handler, 1544 default=1, 1545 ), 1546 ) 1547 1548 if self.nums_callback: 1549 self.nums_callback(field) 1550 1551 def _lsn_handler(self, field: Field) -> None: 1552 if self.lsn_callback: 1553 self.lsn_callback(field) 1554 1555 def _numt_handler(self, field: Field) -> None: 1556 self._remove_all("LTSH\\d+") 1557 self._remove_all("LT\\d+") 1558 after: JbpIOComponent = field 1559 for idx in range(1, field.value + 1): 1560 after = self._insert_after( 1561 after, 1562 Field( 1563 f"LTSH{idx:03}", 1564 "Length of nth Text Subheader", 1565 4, 1566 charset=BCSN_PI, 1567 decoded_range=MinMax(282, 999_999), 1568 converter=Integer(), 1569 default=282, 1570 ), 1571 ) 1572 after = self._insert_after( 1573 after, 1574 Field( 1575 f"LT{idx:03}", 1576 "Length of nth Text Segment", 1577 5, 1578 charset=BCSN_PI, 1579 decoded_range=MinMax(1, 99_999), 1580 converter=Integer(), 1581 setter_callback=self._ltn_handler, 1582 default=1, 1583 ), 1584 ) 1585 1586 if self.numt_callback: 1587 self.numt_callback(field) 1588 1589 def _ltn_handler(self, field: Field) -> None: 1590 if self.ltn_callback: 1591 self.ltn_callback(field) 1592 1593 def _numdes_handler(self, field: Field) -> None: 1594 self._remove_all("LDSH\\d+") 1595 self._remove_all("LD\\d+") 1596 after: JbpIOComponent = field 1597 for idx in range(1, field.value + 1): 1598 after = self._insert_after( 1599 after, 1600 Field( 1601 f"LDSH{idx:03}", 1602 "Length of nth Data Extension Segment Subheader", 1603 4, 1604 charset=BCSN_PI, 1605 decoded_range=MinMax(200, 999_999), 1606 converter=Integer(), 1607 default=200, 1608 ), 1609 ) 1610 after = self._insert_after( 1611 after, 1612 Field( 1613 f"LD{idx:03}", 1614 "Length of nth Data Extension Segment", 1615 9, 1616 charset=BCSN_PI, 1617 decoded_range=MinMax(1, 10**9 - 1), 1618 converter=Integer(), 1619 setter_callback=self._ldn_handler, 1620 default=1, 1621 ), 1622 ) 1623 1624 if self.numdes_callback: 1625 self.numdes_callback(field) 1626 1627 def _ldn_handler(self, field: Field) -> None: 1628 if self.ldn_callback: 1629 self.ldn_callback(field) 1630 1631 def _numres_handler(self, field: Field) -> None: 1632 self._remove_all("LRESH\\d+") 1633 self._remove_all("LRE\\d+") 1634 after: JbpIOComponent = field 1635 for idx in range(1, field.value + 1): 1636 after = self._insert_after( 1637 after, 1638 Field( 1639 f"LRESH{idx:03}", 1640 "Length of nth Reserved Extension Segment Subheader", 1641 4, 1642 charset=BCSN_PI, 1643 decoded_range=MinMax(LRESH_MIN, 999_999), 1644 converter=Integer(), 1645 default=LRESH_MIN, 1646 setter_callback=self._lreshn_handler, 1647 ), 1648 ) 1649 after = self._insert_after( 1650 after, 1651 Field( 1652 f"LRE{idx:03}", 1653 "Length of nth Reserved Extension Segment", 1654 7, 1655 charset=BCSN_PI, 1656 decoded_range=MinMax(1, 10**7 - 1), 1657 converter=Integer(), 1658 default=1, 1659 setter_callback=self._lren_handler, 1660 ), 1661 ) 1662 1663 if self.numres_callback: 1664 self.numres_callback(field) 1665 1666 def _lreshn_handler(self, field: Field) -> None: 1667 if self.lreshn_callback: 1668 self.lreshn_callback(field) 1669 1670 def _lren_handler(self, field: Field) -> None: 1671 if self.lren_callback: 1672 self.lren_callback(field) 1673 1674 def _udhdl_handler(self, field: Field) -> None: 1675 self._remove_all("UDHOFL") 1676 self._remove_all("UDHD") 1677 after: JbpIOComponent = field 1678 if field.value: 1679 after = self._insert_after( 1680 after, 1681 Field( 1682 "UDHOFL", 1683 "User Defined Header Overflow", 1684 3, 1685 charset=BCSN_PI, 1686 converter=Integer(), 1687 default=0, 1688 ), 1689 ) 1690 if field.value > 3: 1691 after = self._insert_after(after, TreSequence("UDHD", field.value - 3)) 1692 1693 def _xhdl_handler(self, field: Field) -> None: 1694 self._remove_all("XHDLOFL") 1695 self._remove_all("XHD") 1696 after: JbpIOComponent = field 1697 if field.value: 1698 after = self._insert_after( 1699 after, 1700 Field( 1701 "XHDLOFL", 1702 "Extended Header Data Overflow", 1703 3, 1704 charset=BCSN_PI, 1705 converter=Integer(), 1706 default=0, 1707 ), 1708 ) 1709 if field.value > 3: 1710 after = self._insert_after(after, TreSequence("XHD", field.value - 3)) 1711 1712 def finalize(self) -> None: 1713 super().finalize() 1714 _update_tre_lengths(self, "UDHDL", "UDHOFL", "UDHD") 1715 _update_tre_lengths(self, "XHDL", "XHDLOFL", "XHD") 1716 # Other length fields are handled by the parent Jbp class 1717 1718 1719class ImageSubheader(Group): 1720 """ 1721 Image Subheader fields 1722 1723 Parameters 1724 ---------- 1725 name : str 1726 Name to give this component 1727 1728 Notes 1729 ----- 1730 See JBP-2025.1 Table 5.13-1 1731 """ 1732 1733 def __init__(self, name: str): 1734 super().__init__(name) 1735 1736 self._append( 1737 Field( 1738 "IM", 1739 "File Part Type", 1740 2, 1741 charset=BCSA, 1742 decoded_range=Constant("IM"), 1743 converter=StringAscii(), 1744 default="IM", 1745 ) 1746 ) 1747 self._append( 1748 Field( 1749 "IID1", 1750 "Image Identifier 1", 1751 10, 1752 charset=BCSA, 1753 converter=StringAscii(), 1754 default="", 1755 ) 1756 ) 1757 self._append( 1758 Field( 1759 "IDATIM", 1760 "Image Date and Time", 1761 14, 1762 charset=BCSN, 1763 decoded_range=DATETIME_REGEX, 1764 converter=StringAscii(), 1765 default="-" * 14, 1766 ) 1767 ) 1768 self._append( 1769 Field( 1770 "TGTID", 1771 "Target Identifier", 1772 17, 1773 charset=BCSA, 1774 converter=StringISO8859_1(), 1775 default=None, 1776 nullable=True, 1777 ) 1778 ) 1779 self._append( 1780 Field( 1781 "IID2", 1782 "Image Identifier 2", 1783 80, 1784 charset=ECSA, 1785 converter=StringISO8859_1(), 1786 default=None, 1787 nullable=True, 1788 ) 1789 ) 1790 self._extend(SecurityFields("Security Fields Image", "I").values()) 1791 self._append( 1792 Field( 1793 "ENCRYP", 1794 "Encryption", 1795 1, 1796 charset=BCSN_PI, 1797 decoded_range=Constant(0), 1798 converter=Integer(), 1799 default=0, 1800 ) 1801 ) 1802 self._append( 1803 Field( 1804 "ISORCE", 1805 "Image Source", 1806 42, 1807 charset=ECSA, 1808 converter=StringISO8859_1(), 1809 default=None, 1810 nullable=True, 1811 ) 1812 ) 1813 self._append( 1814 Field( 1815 "NROWS", 1816 "Number of Significant Rows in Image", 1817 8, 1818 charset=BCSN_PI, 1819 decoded_range=MinMax(1, None), 1820 converter=Integer(), 1821 default=1, 1822 ) 1823 ) 1824 self._append( 1825 Field( 1826 "NCOLS", 1827 "Number of Significant Columns in Image", 1828 8, 1829 charset=BCSN_PI, 1830 decoded_range=MinMax(1, None), 1831 converter=Integer(), 1832 default=1, 1833 ) 1834 ) 1835 self._append( 1836 Field( 1837 "PVTYPE", 1838 "Pixel Value Type", 1839 3, 1840 charset=BCSA, 1841 decoded_range=Enum(["INT", "B", "SI", "R", "C"]), 1842 converter=StringAscii(), 1843 default="INT", 1844 ) 1845 ) 1846 self._append( 1847 Field( 1848 "IREP", 1849 "Image Representation", 1850 8, 1851 charset=BCSA, 1852 decoded_range=Enum( 1853 [ 1854 "MONO", 1855 "RGB", 1856 "RGB/LUT", 1857 "MULTI", 1858 "NODISPLY", 1859 "NVECTOR", 1860 "POLAR", 1861 "VPH", 1862 "YCbCr601", 1863 ] 1864 ), 1865 converter=StringAscii(), 1866 default="MONO", 1867 ) 1868 ) 1869 self._append( 1870 Field( 1871 "ICAT", 1872 "Image Category", 1873 8, 1874 charset=BCSA, 1875 converter=StringAscii(), 1876 default="VIS", 1877 ) 1878 ) 1879 self._append( 1880 Field( 1881 "ABPP", 1882 "Actual Bits-Per-Pixel Per Band", 1883 2, 1884 charset=BCSN_PI, 1885 decoded_range=MinMax(1, 96), 1886 converter=Integer(), 1887 default=1, 1888 ) 1889 ) 1890 self._append( 1891 Field( 1892 "PJUST", 1893 "Pixel Justification", 1894 1, 1895 charset=BCSA, 1896 decoded_range=Enum(["L", "R"]), 1897 converter=StringAscii(), 1898 default="R", 1899 ) 1900 ) 1901 self._append( 1902 Field( 1903 "ICORDS", 1904 "Image Coordinate Representation", 1905 1, 1906 charset=BCSA, 1907 decoded_range=Enum(["U", "G", "N", "S", "D"]), 1908 converter=StringAscii(), 1909 default=None, 1910 nullable=True, 1911 setter_callback=self._icords_handler, 1912 ) 1913 ) 1914 # IGEOLO 1915 self._append( 1916 Field( 1917 "NICOM", 1918 "Number of Image Comments", 1919 1, 1920 charset=BCSN_PI, 1921 converter=Integer(), 1922 default=0, 1923 setter_callback=self._nicom_handler, 1924 ) 1925 ) 1926 # ICOMn 1927 self._append( 1928 Field( 1929 "IC", 1930 "Image Compression", 1931 2, 1932 charset=BCSA, 1933 decoded_range=Enum( 1934 [ 1935 "NC", 1936 "NM", 1937 "C1", 1938 "C3", 1939 "C4", 1940 "C5", 1941 "C6", 1942 "C7", 1943 "C8", 1944 "I1", 1945 "M1", 1946 "M3", 1947 "M4", 1948 "M5", 1949 "M6", 1950 "M7", 1951 "M8", 1952 ] 1953 ), 1954 converter=StringAscii(), 1955 setter_callback=self._ic_handler, 1956 default="NC", 1957 ) 1958 ) 1959 # COMRAT 1960 self._append( 1961 Field( 1962 "NBANDS", 1963 "Number of Bands", 1964 1, 1965 charset=BCSN_PI, 1966 converter=Integer(), 1967 setter_callback=self._nbands_handler, 1968 default=1, 1969 ) 1970 ) 1971 # XBANDS 1972 # IREPBANDn 1973 # ISUBCATn 1974 # IFCn 1975 # IMFLTn 1976 # NLUTSn 1977 # NELUTn 1978 # LUTDn 1979 self._append( 1980 Field( 1981 "ISYNC", 1982 "Image Sync Code", 1983 1, 1984 charset=BCSN_PI, 1985 decoded_range=Constant(0), 1986 converter=Integer(), 1987 default=0, 1988 ) 1989 ) 1990 self._append( 1991 Field( 1992 "IMODE", 1993 "Image Mode", 1994 1, 1995 charset=BCSA, 1996 decoded_range=Enum(["B", "P", "R", "S"]), 1997 converter=StringAscii(), 1998 default="B", 1999 ) 2000 ) 2001 self._append( 2002 Field( 2003 "NBPR", 2004 "Number of Blocks Per Row", 2005 4, 2006 charset=BCSN_PI, 2007 decoded_range=MinMax(1, None), 2008 converter=Integer(), 2009 default=1, 2010 ) 2011 ) 2012 self._append( 2013 Field( 2014 "NBPC", 2015 "Number of Blocks Per Column", 2016 4, 2017 charset=BCSN_PI, 2018 decoded_range=MinMax(1, None), 2019 converter=Integer(), 2020 default=1, 2021 ) 2022 ) 2023 self._append( 2024 Field( 2025 "NPPBH", 2026 "Number of Pixels Per Block Horizontal", 2027 4, 2028 charset=BCSN_PI, 2029 decoded_range=MinMax(0, 8192), 2030 converter=Integer(), 2031 default=0, 2032 ) 2033 ) 2034 self._append( 2035 Field( 2036 "NPPBV", 2037 "Number of Pixels Per Block Vertical", 2038 4, 2039 charset=BCSN_PI, 2040 decoded_range=MinMax(0, 8192), 2041 converter=Integer(), 2042 default=0, 2043 ) 2044 ) 2045 self._append( 2046 Field( 2047 "NBPP", 2048 "Number of Bits Per Pixel Per Band", 2049 2, 2050 charset=BCSN_PI, 2051 decoded_range=MinMax(1, 96), 2052 converter=Integer(), 2053 default=1, 2054 ) 2055 ) 2056 self._append( 2057 Field( 2058 "IDLVL", 2059 "Image Display Level", 2060 3, 2061 charset=BCSN_PI, 2062 decoded_range=MinMax(1, None), 2063 converter=Integer(), 2064 default=1, 2065 ) 2066 ) 2067 self._append( 2068 Field( 2069 "IALVL", 2070 "Attachment Level", 2071 3, 2072 charset=BCSN_PI, 2073 decoded_range=MinMax(0, 998), 2074 converter=Integer(), 2075 default=0, 2076 ) 2077 ) 2078 self._append( 2079 Field( 2080 "ILOC", 2081 "Image Location", 2082 10, 2083 charset=BCSN, 2084 converter=IntPair(), 2085 default=(0, 0), 2086 ) 2087 ) 2088 self._append( 2089 Field( 2090 "IMAG", 2091 "Image Magnification", 2092 4, 2093 charset=BCSA, 2094 decoded_range=Regex(r"(\d+\.?\d*)|(\d*\.?\d+)|(\/\d+)"), 2095 converter=StringAscii(), 2096 default="1.0 ", 2097 ) 2098 ) 2099 self._append( 2100 Field( 2101 "UDIDL", 2102 "User Defined Image Data Length", 2103 5, 2104 charset=BCSN_PI, 2105 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2106 converter=Integer(), 2107 default=0, 2108 setter_callback=self._udidl_handler, 2109 ) 2110 ) 2111 self._append( 2112 Field( 2113 "IXSHDL", 2114 "Image Extended Subheader Data Length", 2115 5, 2116 charset=BCSN_PI, 2117 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2118 converter=Integer(), 2119 default=0, 2120 setter_callback=self._ixshdl_handler, 2121 ) 2122 ) 2123 2124 def _icords_handler(self, field: Field) -> None: 2125 self._remove_all("IGEOLO") 2126 if field.value: 2127 self._insert_after( 2128 field, 2129 Field( 2130 "IGEOLO", 2131 "Image Geographic Location", 2132 60, 2133 charset=BCSA, 2134 converter=StringAscii(), 2135 default="", 2136 ), 2137 ) 2138 2139 def _nicom_handler(self, field: Field) -> None: 2140 self._remove_all("ICOM\\d+") 2141 after = self["NICOM"] 2142 for idx in range(1, field.value + 1): 2143 after = self._insert_after( 2144 after, 2145 Field( 2146 f"ICOM{idx}", 2147 "Image Comment {n}", 2148 80, 2149 charset=ECSA, 2150 converter=StringISO8859_1(), 2151 default="", 2152 ), 2153 ) 2154 2155 def _ic_handler(self, field: Field) -> None: 2156 self._remove_all("COMRAT") 2157 if field.value not in ("NC", "NM"): 2158 self._insert_after( 2159 self["IC"], 2160 Field( 2161 "COMRAT", 2162 "Compression Rate Code", 2163 4, 2164 charset=BCSA, 2165 converter=StringAscii(), 2166 default="", 2167 ), 2168 ) 2169 2170 def _nbands_handler(self, field: Field) -> None: 2171 self._remove_all("XBANDS") 2172 if field.value == 0: 2173 self._insert_after( 2174 self["NBANDS"], 2175 Field( 2176 "XBANDS", 2177 "Number of Multispectral Bands", 2178 5, 2179 charset=BCSN_PI, 2180 decoded_range=MinMax(10, None), 2181 converter=Integer(), 2182 default=10, 2183 setter_callback=self._xbands_handler, 2184 ), 2185 ) 2186 self._set_num_band_groups(field.value) 2187 2188 def _xbands_handler(self, field: Field) -> None: 2189 self._set_num_band_groups(field.value) 2190 2191 def _set_num_band_groups(self, count: int) -> None: 2192 self._remove_all("IREPBAND\\d+") 2193 self._remove_all("ISUBCAT\\d+") 2194 self._remove_all("IFC\\d+") 2195 self._remove_all("IMFLT\\d+") 2196 self._remove_all("NLUTS\\d+") 2197 self._remove_all("NELUT\\d+") 2198 self._remove_all("LUTD\\d+") 2199 2200 after = self.get("XBANDS", self["NBANDS"]) 2201 for idx in range(1, count + 1): 2202 after = self._insert_after( 2203 after, 2204 Field( 2205 f"IREPBAND{idx:05d}", 2206 "nth Band Representation", 2207 2, 2208 charset=BCSA, 2209 converter=StringAscii(), 2210 default=None, 2211 nullable=True, 2212 ), 2213 ) 2214 after = self._insert_after( 2215 after, 2216 Field( 2217 f"ISUBCAT{idx:05d}", 2218 "nth Band Subcategory", 2219 6, 2220 charset=BCSA, 2221 converter=StringAscii(), 2222 default=None, 2223 nullable=True, 2224 ), 2225 ) 2226 after = self._insert_after( 2227 after, 2228 Field( 2229 f"IFC{idx:05d}", 2230 "nth Band Image Filter Condition", 2231 1, 2232 charset=BCSA, 2233 converter=StringAscii(), 2234 default="N", 2235 ), 2236 ) 2237 after = self._insert_after( 2238 after, 2239 Field( 2240 f"IMFLT{idx:05d}", 2241 "nth Band Standard Image Filter Code", 2242 3, 2243 charset=BCSA, 2244 converter=StringAscii(), 2245 default=None, 2246 nullable=True, 2247 ), 2248 ) 2249 after = self._insert_after( 2250 after, 2251 Field( 2252 f"NLUTS{idx:05d}", 2253 "Number of LUTS for the nth Image Band", 2254 1, 2255 charset=BCSN_PI, 2256 decoded_range=MinMax(0, 4), 2257 converter=Integer(), 2258 default=0, 2259 setter_callback=self._nluts_handler, 2260 ), 2261 ) 2262 2263 def _udidl_handler(self, field: Field) -> None: 2264 self._remove_all("UDOFL") 2265 self._remove_all("UDID") 2266 if field.value > 0: 2267 after = self._insert_after( 2268 field, 2269 Field( 2270 "UDOFL", 2271 "User Defined Overflow", 2272 3, 2273 charset=BCSN_PI, 2274 converter=Integer(), 2275 default=0, 2276 ), 2277 ) 2278 if field.value > 3: 2279 after = self._insert_after(after, TreSequence("UDID", field.value - 3)) 2280 2281 def _ixshdl_handler(self, field: Field) -> None: 2282 self._remove_all("IXSOFL") 2283 self._remove_all("IXSHD") 2284 if field.value > 0: 2285 after = self._insert_after( 2286 field, 2287 Field( 2288 "IXSOFL", 2289 "Image Extended Subheader Overflow", 2290 3, 2291 charset=BCSN_PI, 2292 converter=Integer(), 2293 default=0, 2294 ), 2295 ) 2296 if field.value > 3: 2297 after = self._insert_after(after, TreSequence("IXSHD", field.value - 3)) 2298 2299 def _nluts_handler(self, field: Field) -> None: 2300 idx = int(field.name.removeprefix("NLUTS")) 2301 self._remove_all(f"NELUT{idx:05d}\\d+") 2302 self._remove_all(f"LUTD{idx:05d}\\d+") 2303 if field.value > 0: 2304 after = self._insert_after( 2305 field, 2306 Field( 2307 f"NELUT{idx:05d}", 2308 "Number of LUT Entries for the nth Image Band", 2309 5, 2310 charset=BCSN_PI, 2311 decoded_range=MinMax(1, 65536), 2312 converter=Integer(), 2313 default=1, 2314 setter_callback=self._nelut_handler, 2315 ), 2316 ) 2317 for lutidx in range(1, field.value + 1): 2318 after = self._insert_after( 2319 after, 2320 Field( 2321 f"LUTD{idx:05d}{lutidx}", 2322 "nth Image Band, mth LUT", 2323 1, 2324 converter=Bytes(), 2325 default=b"\x00", 2326 ), 2327 ) 2328 2329 def _nelut_handler(self, field: Field) -> None: 2330 idx = int(field.name.removeprefix("NELUT")) 2331 for lutd in self.find_all(f"LUTD{idx:05d}\\d+"): 2332 assert isinstance(lutd, Field) 2333 lutd.size = field.value 2334 2335 def finalize(self) -> None: 2336 super().finalize() 2337 _update_tre_lengths(self, "UDIDL", "UDOFL", "UDID") 2338 _update_tre_lengths(self, "IXSHDL", "IXSOFL", "IXSHD") 2339 2340 2341class ImageSegment(Group): 2342 def __init__(self, name: str, data_size: int = 1): 2343 super().__init__(name) 2344 self._append(ImageSubheader("subheader")) 2345 self._append(BinaryPlaceholder("Data", data_size)) 2346 2347 def print(self, *, file=None) -> None: 2348 print(f"# ImageSegment {self.name}", file=file) 2349 super().print(file=file) 2350 2351 2352class GraphicSubheader(Group): 2353 """ 2354 Graphic Subheader fields 2355 2356 Parameters 2357 ---------- 2358 name : str 2359 Name to give this component 2360 2361 Notes 2362 ----- 2363 See JBP-2025.1 Table 5.15-1 2364 """ 2365 2366 def __init__(self, name: str): 2367 super().__init__(name) 2368 2369 self._append( 2370 Field( 2371 "SY", 2372 "File Part Type", 2373 2, 2374 charset=BCSA, 2375 decoded_range=Constant("SY"), 2376 converter=StringAscii(), 2377 default="SY", 2378 ) 2379 ) 2380 self._append( 2381 Field( 2382 "SID", 2383 "Graphic Identifier", 2384 10, 2385 charset=BCSA, 2386 converter=StringAscii(), 2387 default="", 2388 ) 2389 ) 2390 self._append( 2391 Field( 2392 "SNAME", 2393 "Graphic Name", 2394 20, 2395 charset=ECSA, 2396 converter=StringISO8859_1(), 2397 default=None, 2398 nullable=True, 2399 ) 2400 ) 2401 self._extend(SecurityFields("Security Fields Graphic", "S").values()) 2402 self._append( 2403 Field( 2404 "ENCRYP", 2405 "Encryption", 2406 1, 2407 charset=BCSN_PI, 2408 decoded_range=Constant(0), 2409 converter=Integer(), 2410 default=0, 2411 ) 2412 ) 2413 self._append( 2414 Field( 2415 "SFMT", 2416 "Graphic Type", 2417 1, 2418 charset=BCSA, 2419 decoded_range=Constant("C"), 2420 converter=StringAscii(), 2421 default="C", 2422 ) 2423 ) 2424 self._append( 2425 Field( 2426 "SSTRUCT", 2427 "Reserved for Future Use", 2428 13, 2429 charset=BCSN_PI, 2430 decoded_range=Constant(0), 2431 converter=Integer(), 2432 default=0, 2433 ) 2434 ) 2435 self._append( 2436 Field( 2437 "SDLVL", 2438 "Graphic Display Level", 2439 3, 2440 charset=BCSN_PI, 2441 decoded_range=MinMax(1, None), 2442 converter=Integer(), 2443 default=1, 2444 ) 2445 ) 2446 self._append( 2447 Field( 2448 "SALVL", 2449 "Graphic Attachment Level", 2450 3, 2451 charset=BCSN_PI, 2452 decoded_range=MinMax(0, 998), 2453 converter=Integer(), 2454 default=0, 2455 ) 2456 ) 2457 self._append( 2458 Field( 2459 "SLOC", 2460 "Graphic Location", 2461 10, 2462 charset=BCSN, 2463 converter=IntPair(), 2464 default=(0, 0), 2465 ) 2466 ) 2467 self._append( 2468 Field( 2469 "SBND1", 2470 "First Graphic Bound Location", 2471 10, 2472 charset=BCSN, 2473 converter=IntPair(), 2474 default=(0, 0), 2475 ) 2476 ) 2477 self._append( 2478 Field( 2479 "SCOLOR", 2480 "Graphic Color", 2481 1, 2482 charset=BCSA, 2483 decoded_range=Enum(["C", "M"]), 2484 converter=StringAscii(), 2485 default="", # should this have a default? 2486 ) 2487 ) 2488 self._append( 2489 Field( 2490 "SBND2", 2491 "Second Graphic Bound Location", 2492 10, 2493 charset=BCSN, 2494 converter=IntPair(), 2495 default=(0, 0), 2496 ) 2497 ) 2498 self._append( 2499 Field( 2500 "SRES2", 2501 "Reserved for Future Use", 2502 2, 2503 charset=BCSN_PI, 2504 decoded_range=Constant(0), 2505 converter=Integer(), 2506 default=0, 2507 ) 2508 ) 2509 self._append( 2510 Field( 2511 "SXSHDL", 2512 "Graphic Extended Subheader Data Length", 2513 5, 2514 charset=BCSN_PI, 2515 decoded_range=AnyOf( 2516 Constant(0), 2517 MinMax(3, 9741), 2518 ), 2519 converter=Integer(), 2520 default=0, 2521 setter_callback=self._sxshdl_handler, 2522 ) 2523 ) 2524 2525 def _sxshdl_handler(self, field: Field) -> None: 2526 self._remove_all("SXSOFL") 2527 self._remove_all("SXSHD") 2528 if field.value > 0: 2529 after = self._insert_after( 2530 field, 2531 Field( 2532 "SXSOFL", 2533 "Graphic Extended Subheader Overflow", 2534 3, 2535 charset=BCSN_PI, 2536 converter=Integer(), 2537 default=0, 2538 ), 2539 ) 2540 if field.value > 3: 2541 after = self._insert_after(after, TreSequence("SXSHD", field.value - 3)) 2542 2543 def finalize(self) -> None: 2544 super().finalize() 2545 _update_tre_lengths(self, "SXSHDL", "SXSOFL", "SXSHD") 2546 2547 2548class GraphicSegment(Group): 2549 def __init__(self, name: str, data_size: int = 1): 2550 super().__init__(name) 2551 self._append(GraphicSubheader("subheader")) 2552 self._append(BinaryPlaceholder("Data", data_size)) 2553 2554 def print(self, *, file=None) -> None: 2555 print(f"# GraphicSegment {self.name}", file=file) 2556 super().print(file=file) 2557 2558 2559class TextSubheader(Group): 2560 """ 2561 Text Subheader fields 2562 2563 Parameters 2564 ---------- 2565 name : str 2566 Name to give this component 2567 2568 Notes 2569 ----- 2570 See JBP-2025.1 Table 5.17-1 2571 """ 2572 2573 def __init__(self, name: str): 2574 super().__init__(name) 2575 2576 self._append( 2577 Field( 2578 "TE", 2579 "File Part Type", 2580 2, 2581 charset=BCSA, 2582 decoded_range=Constant("TE"), 2583 converter=StringAscii(), 2584 default="TE", 2585 ) 2586 ) 2587 self._append( 2588 Field( 2589 "TEXTID", 2590 "Text Identifier", 2591 7, 2592 charset=BCSA, 2593 converter=StringAscii(), 2594 default="", 2595 ) 2596 ) 2597 self._append( 2598 Field( 2599 "TXTALVL", 2600 "Text Attachment Level", 2601 3, 2602 charset=BCSN_PI, 2603 decoded_range=MinMax(0, 998), 2604 converter=Integer(), 2605 default=0, 2606 ) 2607 ) 2608 self._append( 2609 Field( 2610 "TXTDT", 2611 "Text Date and Time", 2612 14, 2613 charset=BCSN, 2614 decoded_range=DATETIME_REGEX, 2615 converter=StringAscii(), 2616 default="-" * 14, 2617 ) 2618 ) 2619 self._append( 2620 Field( 2621 "TXTITL", 2622 "Text Title", 2623 80, 2624 charset=ECSA, 2625 converter=StringISO8859_1(), 2626 default=None, 2627 nullable=True, 2628 ) 2629 ) 2630 self._extend(SecurityFields("Security Fields Text", "T").values()) 2631 self._append( 2632 Field( 2633 "ENCRYP", 2634 "Encryption", 2635 1, 2636 charset=BCSN_PI, 2637 decoded_range=Constant(0), 2638 converter=Integer(), 2639 default=0, 2640 ) 2641 ) 2642 self._append( 2643 Field( 2644 "TXTFMT", 2645 "Text Format", 2646 3, 2647 charset=BCSA, 2648 decoded_range=Enum(["MTF", "STA", "UT1", "U8S"]), 2649 converter=StringAscii(), 2650 default="", 2651 ) 2652 ) 2653 self._append( 2654 Field( 2655 "TXSHDL", 2656 "Text Extended Subheader Data Length", 2657 5, 2658 charset=BCSN_PI, 2659 decoded_range=AnyOf( 2660 Constant(0), 2661 MinMax(3, 9717), 2662 ), 2663 converter=Integer(), 2664 default=0, 2665 setter_callback=self._txshdl_handler, 2666 ) 2667 ) 2668 2669 def _txshdl_handler(self, field: Field) -> None: 2670 self._remove_all("TXSOFL") 2671 self._remove_all("TXSHD") 2672 if field.value > 0: 2673 after = self._insert_after( 2674 field, 2675 Field( 2676 "TXSOFL", 2677 "Text Extended Subheader Overflow", 2678 3, 2679 charset=BCSN_PI, 2680 converter=Integer(), 2681 default=0, 2682 ), 2683 ) 2684 if field.value > 3: 2685 after = self._insert_after(after, TreSequence("TXSHD", field.value - 3)) 2686 2687 def finalize(self) -> None: 2688 super().finalize() 2689 _update_tre_lengths(self, "TXSHDL", "TXSOFL", "TXSHD") 2690 2691 2692class TextSegment(Group): 2693 def __init__(self, name: str, data_size: int = 1): 2694 super().__init__(name) 2695 self._append(TextSubheader("subheader")) 2696 self._append(BinaryPlaceholder("Data", data_size)) 2697 2698 def print(self, *, file=None) -> None: 2699 print(f"# TextSegment {self.name}", file=file) 2700 super().print(file=file) 2701 2702 2703class ReservedExtensionSegment(Group): 2704 def __init__(self, name: str, subheader_size: int = LRESH_MIN, data_size: int = 1): 2705 super().__init__(name) 2706 self._append( 2707 Field( 2708 "subheader", 2709 "Placeholder", 2710 subheader_size, 2711 converter=Bytes(), 2712 default=b"\x00" * subheader_size, 2713 ) 2714 ) 2715 self._append(BinaryPlaceholder("RESDATA", data_size)) 2716 2717 def print(self, *, file=None) -> None: 2718 print(f"# ReservedExtensionSegment {self.name}", file=file) 2719 super().print(file=file) 2720 2721 2722class DataExtensionSubheader(Group): 2723 """ 2724 Data Extension Segment (DES) Subheader with unrecognized user-defined subheader fields 2725 2726 Parameters 2727 ---------- 2728 name : str 2729 Name to give this component 2730 desid_constraint : RangeCheck or None, optional 2731 Decoded range check for 'DESID' 2732 desver_constraint : RangeCheck or None, optional 2733 Decoded range check for 'DESVER' 2734 desshl_constraint : RangeCheck or None, optional 2735 Decoded range check for 'DESSHL' 2736 2737 Notes 2738 ----- 2739 See JBP-2025.1 Table 5.18-1 2740 """ 2741 2742 def __init__( 2743 self, 2744 name: str, 2745 *, 2746 desid_constraint: RangeCheck | None = None, 2747 desver_constraint: RangeCheck | None = None, 2748 desshl_constraint: RangeCheck | None = None, 2749 ): 2750 super().__init__(name) 2751 self._append( 2752 Field( 2753 "DE", 2754 "File Part Type", 2755 2, 2756 charset=BCSA, 2757 decoded_range=Constant("DE"), 2758 converter=StringAscii(), 2759 default="DE", 2760 ) 2761 ) 2762 self._append( 2763 Field( 2764 "DESID", 2765 "Unique DES Type Identifier", 2766 25, 2767 charset=BCSA, 2768 decoded_range=desid_constraint, 2769 converter=StringAscii(), 2770 default="", 2771 ) 2772 ) 2773 self._append( 2774 Field( 2775 "DESVER", 2776 "Version of the Data Definition", 2777 2, 2778 charset=BCSN_PI, 2779 decoded_range=desver_constraint or MinMax(1, None), 2780 converter=Integer(), 2781 default=1, 2782 ) 2783 ) 2784 self._extend(SecurityFields("Security Fields DES", "DE").values()) 2785 # DESOFLW/DESITEM only in TRE_OVERFLOW DES 2786 self._append( 2787 Field( 2788 "DESSHL", 2789 "DES User-defined Subheader Length", 2790 4, 2791 charset=BCSN_PI, 2792 converter=Integer(), 2793 decoded_range=desshl_constraint, 2794 default=0, 2795 setter_callback=self._populate_user_defined_subheader, 2796 ) 2797 ) 2798 # DESSHF handled by DESSHL callback 2799 2800 def _populate_user_defined_subheader(self, desshl_field: Field): 2801 """Populate user-defined subheader fields 2802 2803 Subclasses should override this method with their own definition. 2804 """ 2805 self._remove_all("DESSHF") 2806 if desshl_field.value > 0: 2807 # JBP claims DESSHF C-set is BCS-A, but there are some violations in STDI-0002 so we'll treat as bytes 2808 self._insert_after( 2809 desshl_field, 2810 Field( 2811 "DESSHF", 2812 "DES User-defined Subheader Fields", 2813 desshl_field.value, 2814 converter=Bytes(), 2815 default=b"\x00" * desshl_field.value, 2816 ), 2817 ) 2818 2819 2820class TreOverflowDesSubheader(DataExtensionSubheader): 2821 """Tagged Record Extension Overflow (TRE-OVERFLOW) DES 2822 2823 See JBP-2025.1 Table 5.18.2 2824 """ 2825 2826 def __init__(self, name): 2827 super().__init__( 2828 name, 2829 desid_constraint=Constant("TRE_OVERFLOW"), 2830 desver_constraint=Constant(1), 2831 desshl_constraint=Constant(0), 2832 ) 2833 2834 # For some reason, the TRE_OVERFLOW fields are not in the user-defined subheader area 2835 self._insert_after( 2836 self["DESCTLN"], 2837 Field( 2838 "DESOFLW", 2839 "DES Overflowed Header Type", 2840 6, 2841 charset=BCSA, 2842 decoded_range=Enum(["XHD", "IXSHD", "SXSHD", "TXSHD", "UDHD", "UDID"]), 2843 converter=StringAscii(), 2844 default="", 2845 ), 2846 Field( 2847 "DESITEM", 2848 "DES Data Item Overflowed", 2849 3, 2850 charset=BCSN_PI, 2851 converter=Integer(), 2852 default=0, 2853 ), 2854 ) 2855 2856 def _populate_user_defined_subheader(self, desshl_field): 2857 """TRE-OVERFLOW doesn't have used-defined subheader fields""" 2858 2859 2860DesSubheaderDefs = dict[tuple[str, int], Callable[[str], DataExtensionSubheader]] 2861 2862 2863def available_des_subheaders() -> DesSubheaderDefs: 2864 """All discovered and available Data Extension Segment (DES) subheaders 2865 2866 Returns 2867 ------- 2868 dict of {(str, int) : callable} 2869 Mapping of (desid, desver) pairs to a function that accepts a string-valued name and 2870 instantiates the appropriate DES subheader 2871 """ 2872 d: DesSubheaderDefs = {} 2873 for plugin in importlib.metadata.entry_points( 2874 group="jbpy.extensions.des_subheader" 2875 ): 2876 try: 2877 assert len(plugin.name) == 27 2878 desid = plugin.name[:25].rstrip() 2879 desver = int(plugin.name[-2:]) 2880 d[(desid, desver)] = plugin.load() 2881 except (AssertionError, ValueError): 2882 logger.warning(f"Skipping {plugin=}; unable to parse") 2883 return d 2884 2885 2886def des_subheader_factory( 2887 desid: str, desver: int, name: str = "subheader" 2888) -> DataExtensionSubheader: 2889 """Create a Data Extension Segment (DES) subheader 2890 2891 Parameters 2892 ---------- 2893 desid : str 2894 Unique DES type identifier 2895 desver : int 2896 Version of the data definition 2897 name : str, optional 2898 Name to give component 2899 2900 Returns 2901 ------- 2902 DataExtensionSubheader 2903 If the DES data definition is available, an object of the appropriate DataExtensionSubheader subclass. 2904 Otherwise, a DataExtensionSubheader object with generic DES subheader. 2905 """ 2906 des_subheaders = available_des_subheaders() 2907 subheader = des_subheaders.get((desid, desver), DataExtensionSubheader)(name) 2908 subheader["DESID"].value = desid 2909 subheader["DESVER"].value = desver 2910 return subheader 2911 2912 2913class DataExtensionSegment(Group): 2914 def __init__(self, name: str, data_size: int = 1): 2915 super().__init__(name) 2916 self._append(DataExtensionSubheader("subheader")) 2917 self._append(BinaryPlaceholder("DESDATA", data_size)) 2918 2919 def set_subheader(self, subhdr: DataExtensionSubheader) -> None: 2920 """Set this segment's subheader to ``subhdr``""" 2921 if not isinstance(subhdr, DataExtensionSubheader): 2922 raise TypeError(f"unexpected {type(subhdr)=}") 2923 if subhdr._parent is not None: 2924 subhdr = copy.deepcopy(subhdr) 2925 subhdr._parent = None 2926 subhdr.name = "subheader" 2927 self._replace( 2928 self["subheader"], 2929 subhdr, 2930 ) 2931 if isinstance(self["subheader"], TreOverflowDesSubheader): 2932 self._replace( 2933 self["DESDATA"], TreSequence("DESDATA", self["DESDATA"].get_size()) 2934 ) 2935 2936 def _load_impl(self, fd): 2937 for fld in ("DE", "DESID", "DESVER"): 2938 self["subheader"][fld].load(fd) 2939 assert self["subheader"]["DE"].value == "DE" 2940 self.set_subheader( 2941 des_subheader_factory( 2942 self["subheader"]["DESID"].value, self["subheader"]["DESVER"].value 2943 ) 2944 ) 2945 fd.seek(self.get_offset()) 2946 super()._load_impl(fd) 2947 2948 def print(self, *, file=None) -> None: 2949 print(f"# DESegment {self.name}", file=file) 2950 super().print(file=file) 2951 2952 2953def _update_tre_lengths(header, hdl, ofl, hd): 2954 length = 0 2955 if ofl in header: 2956 length += 3 2957 if hd in header: 2958 length += header[hd].get_size() 2959 header[hdl]._set_value(length) 2960 2961 2962class Jbp(Group): 2963 """Class representing an entire NITF/NSIF 2964 2965 Contains the following keys: 2966 * FileHeader 2967 * ImageSegments 2968 * GraphicSegments 2969 * TextSegments 2970 * DataExtensionSegments 2971 * ReservedExtensionSegments 2972 """ 2973 2974 def __init__(self): 2975 super().__init__("Root") 2976 self._append( 2977 FileHeader( 2978 "FileHeader", 2979 numi_callback=self._numi_handler, 2980 lin_callback=self._lin_handler, 2981 nums_callback=self._nums_handler, 2982 lsn_callback=self._lsn_handler, 2983 numt_callback=self._numt_handler, 2984 ltn_callback=self._ltn_handler, 2985 numdes_callback=self._numdes_handler, 2986 ldn_callback=self._ldn_handler, 2987 numres_callback=self._numres_handler, 2988 lreshn_callback=self._lreshn_handler, 2989 lren_callback=self._lren_handler, 2990 ) 2991 ) 2992 self._append( 2993 SegmentList( 2994 "ImageSegments", 2995 ImageSegment, 2996 maximum=999, 2997 ) 2998 ) 2999 self._append( 3000 SegmentList( 3001 "GraphicSegments", 3002 GraphicSegment, 3003 maximum=999, 3004 ) 3005 ) 3006 self._append( 3007 SegmentList( 3008 "TextSegments", 3009 TextSegment, 3010 maximum=999, 3011 ) 3012 ) 3013 self._append( 3014 SegmentList( 3015 "DataExtensionSegments", 3016 DataExtensionSegment, 3017 maximum=999, 3018 ) 3019 ) 3020 self._append( 3021 SegmentList( 3022 "ReservedExtensionSegments", 3023 ReservedExtensionSegment, 3024 maximum=999, 3025 ) 3026 ) 3027 3028 def _numi_handler(self, field: Field) -> None: 3029 self["ImageSegments"].set_count(field.value) 3030 3031 def _lin_handler(self, field: Field) -> None: 3032 idx = int(field.name.removeprefix("LI")) - 1 3033 self["ImageSegments"][idx]["Data"].size = field.value 3034 3035 def _nums_handler(self, field: Field) -> None: 3036 self["GraphicSegments"].set_count(field.value) 3037 3038 def _lsn_handler(self, field: Field) -> None: 3039 idx = int(field.name.removeprefix("LS")) - 1 3040 self["GraphicSegments"][idx]["Data"].size = field.value 3041 3042 def _numt_handler(self, field: Field) -> None: 3043 self["TextSegments"].set_count(field.value) 3044 3045 def _ltn_handler(self, field: Field) -> None: 3046 idx = int(field.name.removeprefix("LT")) - 1 3047 self["TextSegments"][idx]["Data"].size = field.value 3048 3049 def _numdes_handler(self, field: Field) -> None: 3050 self["DataExtensionSegments"].set_count(field.value) 3051 3052 def _ldn_handler(self, field: Field) -> None: 3053 idx = int(field.name.removeprefix("LD")) - 1 3054 self["DataExtensionSegments"][idx]["DESDATA"].size = field.value 3055 3056 def _numres_handler(self, field: Field) -> None: 3057 self["ReservedExtensionSegments"].set_count(field.value) 3058 3059 def _lreshn_handler(self, field: Field) -> None: 3060 # this callback should be removed if the Reserved Subheader is implemented 3061 idx = int(field.name.removeprefix("LRESH")) - 1 3062 self["ReservedExtensionSegments"][idx]["subheader"].size = field.value 3063 3064 def _lren_handler(self, field: Field) -> None: 3065 idx = int(field.name.removeprefix("LRE")) - 1 3066 self["ReservedExtensionSegments"][idx]["RESDATA"].size = field.value 3067 3068 def update_lengths(self) -> None: 3069 """Compute and set the segment lengths""" 3070 self["FileHeader"]["FL"]._set_value(self.get_size()) 3071 self["FileHeader"]["HL"]._set_value(self["FileHeader"].get_size()) 3072 3073 for idx, seg in enumerate(self["ImageSegments"]): 3074 self["FileHeader"][f"LISH{idx + 1:03d}"]._set_value( 3075 seg["subheader"].get_size() 3076 ) 3077 self["FileHeader"][f"LI{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3078 3079 for idx, seg in enumerate(self["GraphicSegments"]): 3080 self["FileHeader"][f"LSSH{idx + 1:03d}"]._set_value( 3081 seg["subheader"].get_size() 3082 ) 3083 self["FileHeader"][f"LS{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3084 3085 for idx, seg in enumerate(self["TextSegments"]): 3086 self["FileHeader"][f"LTSH{idx + 1:03d}"]._set_value( 3087 seg["subheader"].get_size() 3088 ) 3089 self["FileHeader"][f"LT{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3090 3091 for idx, seg in enumerate(self["DataExtensionSegments"]): 3092 self["FileHeader"][f"LDSH{idx + 1:03d}"]._set_value( 3093 seg["subheader"].get_size() 3094 ) 3095 self["FileHeader"][f"LD{idx + 1:03d}"]._set_value(seg["DESDATA"].get_size()) 3096 3097 for idx, seg in enumerate(self["ReservedExtensionSegments"]): 3098 self["FileHeader"][f"LRESH{idx + 1:03d}"]._set_value( 3099 seg["subheader"].get_size() 3100 ) 3101 self["FileHeader"][f"LRE{idx + 1:03d}"]._set_value( 3102 seg["RESDATA"].get_size() 3103 ) 3104 3105 def update_fdt(self) -> None: 3106 """Set the FDT field to the current time""" 3107 now = datetime.datetime.now(datetime.timezone.utc) 3108 self["FileHeader"]["FDT"].value = now.strftime("%Y%m%d%H%M%S") 3109 3110 def finalize(self) -> None: 3111 """Compute derived values such as lengths, and CLEVEL""" 3112 super().finalize() 3113 self.update_lengths() 3114 self.update_fdt() 3115 self.update_clevel() # must be after lengths 3116 3117 def _clevel_ccs_extent(self) -> int: 3118 min_ccs_row = min_ccs_col = 0 3119 max_ccs_row = max_ccs_col = 0 3120 3121 level_origin = {0: {"row": 0, "col": 0}} 3122 for imseg in self["ImageSegments"]: 3123 alvl = imseg["subheader"]["IALVL"].value 3124 dlvl = imseg["subheader"]["IDLVL"].value 3125 iloc_row, iloc_col = imseg["subheader"]["ILOC"].value 3126 nrows = imseg["subheader"]["NROWS"].value 3127 ncols = imseg["subheader"]["NCOLS"].value 3128 level_origin[dlvl] = { 3129 "row": level_origin[alvl]["row"] + iloc_row, 3130 "col": level_origin[alvl]["col"] + iloc_col, 3131 } 3132 3133 min_ccs_row = min(min_ccs_row, level_origin[dlvl]["row"]) 3134 min_ccs_col = min(min_ccs_col, level_origin[dlvl]["col"]) 3135 3136 max_ccs_row = max(max_ccs_row, level_origin[dlvl]["row"] + nrows) 3137 max_ccs_col = max(max_ccs_col, level_origin[dlvl]["col"] + ncols) 3138 3139 if len(self["GraphicSegments"]): 3140 logger.warning("CLEVEL of JBPs with Graphic Segments is not supported") 3141 3142 max_extent = max(max_ccs_row - min_ccs_row, max_ccs_col - min_ccs_col) 3143 if max_extent <= 2047: 3144 return 3 3145 if max_extent <= 8191: 3146 return 5 3147 if max_extent <= 65535: 3148 return 6 3149 if max_extent <= 99_999_999: 3150 return 7 3151 return 9 3152 3153 def _clevel_file_size(self) -> int: 3154 if self["FileHeader"]["FL"].value < 50 * (1 << 20): 3155 return 3 3156 if self["FileHeader"]["FL"].value < 1 * (1 << 30): 3157 return 5 3158 if self["FileHeader"]["FL"].value < 2 * (1 << 30): 3159 return 6 3160 if self["FileHeader"]["FL"].value < 10 * (1 << 30): 3161 return 7 3162 return 9 3163 3164 def _clevel_image_size(self) -> int: 3165 clevel = 3 3166 for imseg in self["ImageSegments"]: 3167 nrows = imseg["subheader"]["NROWS"].value 3168 ncols = imseg["subheader"]["NCOLS"].value 3169 3170 if nrows <= 2048 and ncols <= 2048: 3171 clevel = max(clevel, 3) 3172 elif nrows <= 8192 and ncols <= 8192: 3173 clevel = max(clevel, 5) 3174 elif nrows <= 65536 and ncols <= 65536: 3175 clevel = max(clevel, 6) 3176 elif nrows <= 99_999_999 and ncols <= 99_999_999: 3177 clevel = max(clevel, 7) 3178 return clevel 3179 3180 def _clevel_image_blocking(self) -> int: 3181 clevel = 3 3182 for imseg in self["ImageSegments"]: 3183 horiz = imseg["subheader"]["NPPBH"].value 3184 vert = imseg["subheader"]["NPPBV"].value 3185 3186 if horiz <= 2048 and vert <= 2048: 3187 clevel = max(clevel, 3) 3188 elif horiz <= 8192 and vert <= 8192: 3189 clevel = max(clevel, 5) 3190 return clevel 3191 3192 def _clevel_irep(self) -> int: 3193 clevel = 0 3194 for imseg in self["ImageSegments"]: 3195 has_lut = bool(imseg["subheader"].find_all("NLUT.*")) 3196 num_bands = ( 3197 imseg["subheader"].get("XBANDS", imseg["subheader"]["NBANDS"]).value 3198 ) 3199 # Color (RGB) No Compression 3200 if ( 3201 imseg["subheader"]["IREP"].value == "RGB" 3202 and num_bands == 3 3203 and not has_lut 3204 and imseg["subheader"]["IC"].value in ("NC", "NM") 3205 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3206 ): 3207 if imseg["subheader"]["NBPP"].value == 8: 3208 clevel = max(clevel, 3) 3209 3210 if imseg["subheader"]["NBPP"].value in (8, 16, 32): 3211 clevel = max(clevel, 6) 3212 3213 # Multiband (MULTI) No Compression 3214 if ( 3215 imseg["subheader"]["IREP"].value == "MULTI" 3216 and imseg["subheader"]["NBPP"].value in (1, 8, 16, 32, 64) 3217 and imseg["subheader"]["IC"].value in ("NC", "NM") 3218 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3219 ): 3220 if 2 <= num_bands <= 9: 3221 clevel = max(clevel, 3) 3222 3223 if 10 <= num_bands <= 255: 3224 clevel = max(clevel, 5) 3225 3226 if 255 <= num_bands <= 999: 3227 clevel = max(clevel, 7) 3228 3229 # JPEG2000 Compression Multiband (MULTI) 3230 if ( 3231 imseg["subheader"]["IREP"].value == "MULTI" 3232 and imseg["subheader"]["NBPP"].value <= 32 3233 and imseg["subheader"]["IC"].value in ("C8", "M8") 3234 and imseg["subheader"]["IMODE"].value == "B" 3235 ): 3236 if 1 <= num_bands <= 9: 3237 clevel = max(clevel, 3) 3238 3239 if 10 <= num_bands <= 255: 3240 clevel = max(clevel, 5) 3241 3242 if 256 <= num_bands <= 999: 3243 clevel = max(clevel, 7) 3244 3245 # Multiband (MULTI) Individual Band JPEG Compression 3246 if ( 3247 imseg["subheader"]["IREP"].value == "MULTI" 3248 and imseg["subheader"]["NBPP"].value in (8, 12) 3249 and not has_lut 3250 and imseg["subheader"]["IC"].value in ("C3", "M3") 3251 and imseg["subheader"]["IMODE"].value in ("B", "S") 3252 ): 3253 if 2 <= num_bands <= 9: 3254 clevel = max(clevel, 3) 3255 3256 if 10 <= num_bands <= 255: 3257 clevel = max(clevel, 5) 3258 3259 if 256 <= num_bands <= 999: 3260 clevel = max(clevel, 7) 3261 3262 # Multiband (MULTI) Multi-Component Compression 3263 if ( 3264 imseg["subheader"]["IREP"].value == "MULTI" 3265 and imseg["subheader"]["NBPP"].value in (8, 12) 3266 and not has_lut 3267 and imseg["subheader"]["IC"].value in ("C6", "M6") 3268 and imseg["subheader"]["IMODE"].value in ("B", "P", "S") 3269 ): 3270 if 2 <= num_bands <= 9: 3271 clevel = max(clevel, 3) 3272 3273 if 10 <= num_bands <= 255: 3274 clevel = max(clevel, 5) 3275 3276 if 256 <= num_bands <= 999: 3277 clevel = max(clevel, 7) 3278 3279 # Matrix Data (NODISPLY) 3280 if ( 3281 imseg["subheader"]["IREP"].value == "NODISPLY" 3282 and imseg["subheader"]["NBPP"].value in (8, 16, 32, 64) 3283 and not has_lut 3284 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3285 ): 3286 if 2 <= num_bands <= 9: 3287 clevel = max(clevel, 3) 3288 3289 if 10 <= num_bands <= 255: 3290 clevel = max(clevel, 5) 3291 3292 if 256 <= num_bands <= 999: 3293 clevel = max(clevel, 7) 3294 3295 return clevel 3296 3297 def _clevel_num_imseg(self) -> int: 3298 if len(self["ImageSegments"]) <= 20: 3299 return 3 3300 if 20 < len(self["ImageSegments"]) <= 100: 3301 return 5 3302 return 9 3303 3304 def _clevel_aggregate_size_of_graphic_segments(self) -> int: 3305 size = 0 3306 for field in self["FileHeader"].find_all("LS\\d+"): 3307 size += field.value 3308 3309 if size <= 1 * (1 << 20): 3310 return 3 3311 if size <= 2 * (1 << 20): 3312 return 5 3313 return 9 3314 3315 def _clevel_cl9(self) -> int: 3316 """Explicit CLEVEL 9 checks""" 3317 # 1 3318 if self["FileHeader"]["FL"].value >= 10 * (1 << 30): 3319 return 9 3320 3321 total_num_bands = 0 3322 for imseg in self["ImageSegments"]: 3323 # 2 3324 if ( 3325 imseg["subheader"]["NPPBH"].value == 0 3326 or imseg["subheader"]["NPPBV"].value == 0 3327 ): 3328 return 9 3329 total_num_bands += imseg.get("XBANDS", imseg["subheader"]["NBANDS"]).value 3330 3331 # 3 3332 if total_num_bands > 999: 3333 return 9 3334 3335 # 4 3336 if len(self["ImageSegments"]) > 100: 3337 return 9 3338 3339 # 5 3340 if len(self["GraphicSegments"]) > 100: 3341 return 9 3342 3343 # 6 3344 size = 0 3345 for field in self["FileHeader"].find_all("LS\\d+"): 3346 size += field.value 3347 if size > 2 * (1 << 20): 3348 return 9 3349 3350 # 7 3351 if len(self["TextSegments"]) > 32: 3352 return 9 3353 3354 # 8 3355 if len(self["DataExtensionSegments"]) > 100: 3356 return 9 3357 3358 return 0 3359 3360 def update_clevel(self) -> None: 3361 """Compute and update the CLEVEL field. See JBP-2025.1 Table G-1""" 3362 clevel = 3 3363 helpers = [attrib for attrib in dir(self) if attrib.startswith("_clevel_")] 3364 for helper in helpers: 3365 clevel = max(clevel, getattr(self, helper)()) 3366 3367 self["FileHeader"]["CLEVEL"].value = clevel 3368 3369 3370class TreSequence(ComponentCollection, collections.abc.MutableSequence): 3371 """ 3372 TREs which appear one after the other with no intervening bytes 3373 3374 Intended for use as the user defined and/or extended data fields. See Section 5.9.3. 3375 3376 Parameters 3377 ---------- 3378 name : str 3379 Name to give the field 3380 length : int 3381 Initial length in bytes 3382 """ 3383 3384 def __init__(self, name, length): 3385 super().__init__(name) 3386 self._length = length 3387 3388 def _load_impl(self, fd): 3389 if self._children: 3390 return super()._load_impl(fd) 3391 3392 # else need to discover which TREs are in the file 3393 bytes_read = 0 3394 while bytes_read < self._length: 3395 tretag = fd.read(6).decode() 3396 fd.seek(-6, os.SEEK_CUR) 3397 tre = tre_factory(tretag) 3398 self._append(tre) 3399 tre.load(fd) 3400 bytes_read += tre.get_size() 3401 3402 if bytes_read != self._length: 3403 logger.warning( 3404 f"Length of TREs ({bytes_read}) in {self.name} does not match expected length ({self._length})" 3405 ) 3406 3407 def __getitem__(self, key): 3408 return self._children[key] 3409 3410 def __setitem__(self, key, value): 3411 value._parent = self 3412 self._children[key] = value 3413 3414 def __delitem__(self, key): 3415 del self._children[key] 3416 3417 def __len__(self): 3418 return len(self._children) 3419 3420 def insert(self, index, element): 3421 element._parent = self 3422 self._children.insert(index, element) 3423 3424 3425class Tre(Group): 3426 """Base class for TREs 3427 3428 Includes the TRETAG and TREL tags. 3429 3430 Parameters 3431 ---------- 3432 identifier : str 3433 identifier of the TRE. Must be 1-6 characters. 3434 tretag_rename : str 3435 Alternative to give the 'TRETAG' field 3436 trel_rename : str 3437 Alternative to give the 'TREL' field 3438 length_constraint : RangeCheck or None 3439 Decoded range check for 'TREL' field. Defaults to MinMax(1, 99985) 3440 3441 Notes 3442 ----- 3443 BIIF and JBP define TREs as having 3 fields, TRETAG, TREL, and. TREDATA. 3444 However, TREs commonly rename TRETAG and TREL and define their own fields as replacing TREDATA. 3445 """ 3446 3447 def __init__( 3448 self, 3449 identifier: str, 3450 tretag_rename: str = "TRETAG", 3451 trel_rename: str = "TREL", 3452 length_constraint: RangeCheck | None = None, 3453 ): 3454 if not (1 <= len(identifier) <= 6): 3455 raise ValueError(f"TRE identifier '{identifier}' must be 1-6 characters") 3456 3457 ident_rstrip = identifier.rstrip(" ") 3458 super().__init__(ident_rstrip) 3459 self.tretag_rename = tretag_rename 3460 self.trel_rename = trel_rename 3461 3462 if length_constraint is None: 3463 length_constraint = MinMax(1, 99985) 3464 3465 self._append( 3466 Field( 3467 tretag_rename, 3468 "Unique Extension Type Identifier", 3469 6, 3470 charset=BCSA, 3471 decoded_range=Constant(ident_rstrip), 3472 converter=StringAscii(), 3473 default=ident_rstrip, 3474 ) 3475 ) 3476 3477 self._append( 3478 Field( 3479 trel_rename, 3480 "Length of the TREDATA", 3481 5, 3482 charset=BCSN_PI, 3483 decoded_range=length_constraint, 3484 converter=Integer(), 3485 default=0, 3486 ) 3487 ) 3488 3489 def finalize(self) -> None: 3490 """Set the TREL field""" 3491 length = 0 3492 for child in self._children: 3493 if child.name in (self.tretag_rename, self.trel_rename): 3494 continue 3495 length += child.get_size() 3496 self[self.trel_rename]._set_value(length) 3497 3498 3499class UnknownTre(Tre): 3500 """TRE without known TREDATA definition. 3501 see: Table 5.9-1. Registered and Controlled Tagged Record Extension Format 3502 """ 3503 3504 def __init__(self, name): 3505 super().__init__(name) 3506 self["TREL"]._setter_callback = self._trel_handler 3507 3508 self._append( 3509 Field( 3510 "TREDATA", 3511 "User-Defined Data", 3512 0, 3513 converter=Bytes(), 3514 default=b"", 3515 ) 3516 ) 3517 3518 def _trel_handler(self, field): 3519 self["TREDATA"].size = field.value 3520 3521 3522def available_tres() -> dict[str, Callable[[], Tre]]: 3523 """All discovered and available Tagged Record Extensions (TREs) 3524 3525 Returns 3526 ------- 3527 dict of {str : callable} 3528 Mapping of TRETAG name to a function with no required arguments that 3529 instantiates the appropriate TRE 3530 """ 3531 d = {} 3532 for plugin in importlib.metadata.entry_points(group="jbpy.extensions.tre"): 3533 try: 3534 assert len(plugin.name) == 6 3535 tretag = plugin.name.rstrip() 3536 d[tretag] = plugin.load() 3537 except AssertionError: 3538 logger.warning(f"Skipping {plugin=}; unable to parse") 3539 return d 3540 3541 3542def tre_factory(tretag: str) -> Tre: 3543 """Create a TRE instance 3544 3545 Parameters 3546 ---------- 3547 tretag : str 3548 The 1-6 character name of the TRE 3549 3550 Returns 3551 ------- 3552 Tre 3553 TRE object 3554 """ 3555 tres = available_tres() 3556 if tretag in tres: 3557 return tres[tretag]() 3558 3559 return UnknownTre(tretag) 3560 3561 3562class _JsonEncoder(json.JSONEncoder): 3563 def __init__(self, *args, full_details=False, **kwargs): 3564 super().__init__(*args, **kwargs) 3565 self.full_details = full_details 3566 3567 def default(self, obj): 3568 if isinstance(obj, collections.abc.Mapping): 3569 return dict(obj) 3570 if isinstance(obj, bytes): 3571 return list(obj) 3572 if isinstance(obj, Field): 3573 if self.full_details: 3574 return { 3575 "size": obj.size, 3576 "offset": obj.get_offset(), 3577 "value": obj.value, 3578 } 3579 return obj.value 3580 if isinstance(obj, BinaryPlaceholder): 3581 if self.full_details: 3582 return { 3583 "size": obj.size, 3584 "offset": obj.get_offset(), 3585 "value": "__binary__", 3586 } 3587 return f"__binary__ ({obj.get_size()} bytes)" 3588 if isinstance(obj, SegmentList): 3589 return list(obj) 3590 if isinstance(obj, TreSequence): 3591 return list(obj) 3592 return super().default(obj)
34class BinaryFile_R: 35 """Binary file-like object supporting reading""" 36 37 @abc.abstractmethod 38 def seek(self, __offset: int, __whence: int = ...) -> int: ... 39 @abc.abstractmethod 40 def read(self, __length: int = ...) -> bytes: ...
Binary file-like object supporting reading
43class BinaryFile_RW(BinaryFile_R): 44 """Binary file-like object supporting reading and writing""" 45 46 @abc.abstractmethod 47 def write(self, __data: bytes) -> int: ...
Binary file-like object supporting reading and writing
Inherited Members
50class SubFile: 51 """File-like object mapping to a contiguous subset of another file-like object 52 53 Parameters 54 ---------- 55 file : file-like 56 An open file object. Must be binary. 57 start : int 58 Start byte offset of the subfile 59 length : int 60 Number of bytes to expose from the start 61 """ 62 63 def __init__(self, file: Any, start: int, length: int): 64 self._file = file 65 self._start = start 66 self._length = length 67 self._pos = 0 # position within the subfile 68 69 def seek(self, offset: int, whence: int = 0) -> int: 70 """ 71 Seek to a position within the subfile. 72 73 Parameters 74 ---------- 75 offset : int 76 Offset to seek 77 whence : int 78 0 (start), 1 (current), or 2 (end of subfile) 79 80 Returns 81 ------- 82 int 83 Current offset in the SubFile 84 """ 85 if whence == 0: 86 new_pos = offset 87 elif whence == 1: 88 new_pos = self._pos + offset 89 elif whence == 2: 90 new_pos = self._length + offset 91 else: 92 raise ValueError(f"whence value {whence} unsupported") 93 94 if new_pos < 0: 95 raise OSError("Seek before start of subfile.") 96 97 self._pos = new_pos 98 return self._pos 99 100 def tell(self) -> int: 101 """Return the current position within the subfile.""" 102 return self._pos 103 104 def read(self, size: int = -1) -> bytes: 105 """ 106 Read data from the subfile. 107 108 Parameters 109 ---------- 110 size : int 111 Number of bytes to read, or -1 for all remaining 112 """ 113 if self._pos >= self._length: 114 return b"" 115 116 read_len = ( 117 self._length - self._pos 118 if size < 0 119 else min(size, self._length - self._pos) 120 ) 121 self._file.seek(self._start + self._pos) 122 data = self._file.read(read_len) 123 self._pos += len(data) 124 return data 125 126 def readinto(self, b) -> int | None: 127 if self._pos >= self._length: 128 return 0 129 self._file.seek(self._start + self._pos) 130 bytes_remaining = self._length - self._pos 131 v = memoryview(b) 132 num_read = self._file.readinto(v[:bytes_remaining]) 133 if num_read is not None: 134 self._pos += num_read 135 return num_read 136 137 def readline(self, size=-1) -> bytes: 138 if self._pos >= self._length: 139 return b"" 140 self._file.seek(self._start + self._pos) 141 bytes_remaining = self._length - self._pos 142 _sz = bytes_remaining if size == -1 else min(bytes_remaining, size) 143 data = self._file.readline(_sz) 144 self._pos += len(data) 145 return data 146 147 def readlines(self, hint=-1) -> list[bytes]: 148 if self._pos >= self._length: 149 return [] 150 self._file.seek(self._start + self._pos) 151 line = self.readline() 152 n = len(line) 153 lines = [line] 154 155 while line and (n < hint or (hint <= 0 or hint is None)): 156 line = self.readline() 157 if line: 158 lines.append(line) 159 n += len(line) 160 return lines 161 162 def readable(self) -> bool: 163 return self._file.readable()
File-like object mapping to a contiguous subset of another file-like object
Parameters
- file (file-like): An open file object. Must be binary.
- start (int): Start byte offset of the subfile
- length (int): Number of bytes to expose from the start
69 def seek(self, offset: int, whence: int = 0) -> int: 70 """ 71 Seek to a position within the subfile. 72 73 Parameters 74 ---------- 75 offset : int 76 Offset to seek 77 whence : int 78 0 (start), 1 (current), or 2 (end of subfile) 79 80 Returns 81 ------- 82 int 83 Current offset in the SubFile 84 """ 85 if whence == 0: 86 new_pos = offset 87 elif whence == 1: 88 new_pos = self._pos + offset 89 elif whence == 2: 90 new_pos = self._length + offset 91 else: 92 raise ValueError(f"whence value {whence} unsupported") 93 94 if new_pos < 0: 95 raise OSError("Seek before start of subfile.") 96 97 self._pos = new_pos 98 return self._pos
Seek to a position within the subfile.
Parameters
- offset (int): Offset to seek
- whence (int): 0 (start), 1 (current), or 2 (end of subfile)
Returns
- int: Current offset in the SubFile
100 def tell(self) -> int: 101 """Return the current position within the subfile.""" 102 return self._pos
Return the current position within the subfile.
104 def read(self, size: int = -1) -> bytes: 105 """ 106 Read data from the subfile. 107 108 Parameters 109 ---------- 110 size : int 111 Number of bytes to read, or -1 for all remaining 112 """ 113 if self._pos >= self._length: 114 return b"" 115 116 read_len = ( 117 self._length - self._pos 118 if size < 0 119 else min(size, self._length - self._pos) 120 ) 121 self._file.seek(self._start + self._pos) 122 data = self._file.read(read_len) 123 self._pos += len(data) 124 return data
Read data from the subfile.
Parameters
- size (int): Number of bytes to read, or -1 for all remaining
126 def readinto(self, b) -> int | None: 127 if self._pos >= self._length: 128 return 0 129 self._file.seek(self._start + self._pos) 130 bytes_remaining = self._length - self._pos 131 v = memoryview(b) 132 num_read = self._file.readinto(v[:bytes_remaining]) 133 if num_read is not None: 134 self._pos += num_read 135 return num_read
137 def readline(self, size=-1) -> bytes: 138 if self._pos >= self._length: 139 return b"" 140 self._file.seek(self._start + self._pos) 141 bytes_remaining = self._length - self._pos 142 _sz = bytes_remaining if size == -1 else min(bytes_remaining, size) 143 data = self._file.readline(_sz) 144 self._pos += len(data) 145 return data
147 def readlines(self, hint=-1) -> list[bytes]: 148 if self._pos >= self._length: 149 return [] 150 self._file.seek(self._start + self._pos) 151 line = self.readline() 152 n = len(line) 153 lines = [line] 154 155 while line and (n < hint or (hint <= 0 or hint is None)): 156 line = self.readline() 157 if line: 158 lines.append(line) 159 n += len(line) 160 return lines
166class PythonConverter(abc.ABC): 167 """Abstract base class for converting between JBP field bytes and python types""" 168 169 def to_bytes(self, decoded_value: Any, size: int) -> bytes: 170 """Convert python type to bytes 171 172 Parameters 173 ---------- 174 decoded_value 175 Value to convert 176 size : int 177 Minimum field width in bytes 178 179 Returns 180 ------- 181 bytes 182 Encoded value 183 """ 184 return self.to_bytes_impl(decoded_value, size) 185 186 @abc.abstractmethod 187 def to_bytes_impl(self, decoded_value: Any, size: int) -> bytes: 188 """Convert python type to bytes""" 189 190 def from_bytes(self, encoded_value: bytes) -> Any: 191 """Convert bytes to python type""" 192 return self.from_bytes_impl(encoded_value) 193 194 @abc.abstractmethod 195 def from_bytes_impl(self, encoded_value) -> Any: 196 """Convert bytes to python type"""
Abstract base class for converting between JBP field bytes and python types
169 def to_bytes(self, decoded_value: Any, size: int) -> bytes: 170 """Convert python type to bytes 171 172 Parameters 173 ---------- 174 decoded_value 175 Value to convert 176 size : int 177 Minimum field width in bytes 178 179 Returns 180 ------- 181 bytes 182 Encoded value 183 """ 184 return self.to_bytes_impl(decoded_value, size)
Convert python type to bytes
Parameters
- decoded_value: Value to convert
- size (int): Minimum field width in bytes
Returns
- bytes: Encoded value
186 @abc.abstractmethod 187 def to_bytes_impl(self, decoded_value: Any, size: int) -> bytes: 188 """Convert python type to bytes"""
Convert python type to bytes
199class StringUtf8(PythonConverter): 200 """Convert to/from UTF-8 str""" 201 202 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 203 return decoded_value.encode().ljust(size) 204 205 def from_bytes_impl(self, encoded_value: bytes) -> str: 206 return encoded_value.decode().rstrip(" ")
Convert to/from UTF-8 str
202 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 203 return decoded_value.encode().ljust(size)
Convert python type to bytes
205 def from_bytes_impl(self, encoded_value: bytes) -> str: 206 return encoded_value.decode().rstrip(" ")
Convert bytes to python type
Inherited Members
209class StringAscii(PythonConverter): 210 """Convert to/from ASCII str""" 211 212 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 213 return decoded_value.encode("ascii").ljust(size) 214 215 def from_bytes_impl(self, encoded_value: bytes) -> str: 216 return encoded_value.decode("ascii").rstrip(" ")
Convert to/from ASCII str
212 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 213 return decoded_value.encode("ascii").ljust(size)
Convert python type to bytes
215 def from_bytes_impl(self, encoded_value: bytes) -> str: 216 return encoded_value.decode("ascii").rstrip(" ")
Convert bytes to python type
Inherited Members
219class StringISO8859_1(PythonConverter): # noqa: N801 220 """Convert to/from an ISO 8859-1 str 221 222 Notes 223 ----- 224 JBP-2025.1 Table D-1 specifies the full ECS-A character set, which 225 happens to match ISO 8859 part 1. 226 """ 227 228 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 229 return decoded_value.encode("iso8859_1").ljust(size) 230 231 def from_bytes_impl(self, encoded_value: bytes) -> str: 232 return encoded_value.decode("iso8859_1").rstrip(" ")
Convert to/from an ISO 8859-1 str
Notes
JBP-2025.1 Table D-1 specifies the full ECS-A character set, which happens to match ISO 8859 part 1.
228 def to_bytes_impl(self, decoded_value: str, size: int) -> bytes: 229 return decoded_value.encode("iso8859_1").ljust(size)
Convert python type to bytes
231 def from_bytes_impl(self, encoded_value: bytes) -> str: 232 return encoded_value.decode("iso8859_1").rstrip(" ")
Convert bytes to python type
Inherited Members
235class IntPair(PythonConverter): 236 """convert to/from two int tuple""" 237 238 def to_bytes_impl(self, decoded_value: tuple[int, int], size: int) -> bytes: 239 if (size < 2) or (size % 2): 240 raise ValueError(f"invalid {size=}; must be positive and even") 241 length = size // 2 242 return f"{decoded_value[0]:0{length}d}{decoded_value[1]:0{length}d}".encode() 243 244 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int]: 245 length = len(encoded_value) // 2 246 return (int(encoded_value[0:length]), int(encoded_value[length:]))
convert to/from two int tuple
238 def to_bytes_impl(self, decoded_value: tuple[int, int], size: int) -> bytes: 239 if (size < 2) or (size % 2): 240 raise ValueError(f"invalid {size=}; must be positive and even") 241 length = size // 2 242 return f"{decoded_value[0]:0{length}d}{decoded_value[1]:0{length}d}".encode()
Convert python type to bytes
244 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int]: 245 length = len(encoded_value) // 2 246 return (int(encoded_value[0:length]), int(encoded_value[length:]))
Convert bytes to python type
Inherited Members
249class Bytes(PythonConverter): 250 """Convert to/from bytes""" 251 252 def to_bytes_impl(self, decoded_value: bytes, size: int) -> bytes: 253 if len(decoded_value) < size: 254 raise ValueError(f"{len(decoded_value)=} must be at least {size=}") 255 return decoded_value 256 257 def from_bytes_impl(self, encoded_value: bytes) -> bytes: 258 return encoded_value
Convert to/from bytes
252 def to_bytes_impl(self, decoded_value: bytes, size: int) -> bytes: 253 if len(decoded_value) < size: 254 raise ValueError(f"{len(decoded_value)=} must be at least {size=}") 255 return decoded_value
Convert python type to bytes
Inherited Members
261class Integer(PythonConverter): 262 """Convert to/from int 263 264 Parameters 265 ---------- 266 sign : {'+', '-', space}, optional 267 When to encode with a sign. The meaning of ``sign`` is the same as the meaning of the sign option 268 in python's string format specification mini-language: 269 270 * '+': a sign should be used for positive and negative numbers 271 * '-': a sign should be used for negative numbers only 272 * space: a leading space should be used for positive and a minus sign on negative numbers 273 """ 274 275 def __init__(self, sign: Literal["+", "-", " "] = "-"): 276 self.sign = sign 277 278 def to_bytes_impl(self, decoded_value: int, size: int) -> bytes: 279 decoded_value = int(decoded_value) 280 return f"{decoded_value:{self.sign}0{size}}".encode() 281 282 def from_bytes_impl(self, encoded_value: bytes) -> int: 283 return int(encoded_value)
Convert to/from int
Parameters
sign ({'+', '-', space}, optional): When to encode with a sign. The meaning of
signis the same as the meaning of the sign option in python's string format specification mini-language:- '+': a sign should be used for positive and negative numbers
- '-': a sign should be used for negative numbers only
- space: a leading space should be used for positive and a minus sign on negative numbers
278 def to_bytes_impl(self, decoded_value: int, size: int) -> bytes: 279 decoded_value = int(decoded_value) 280 return f"{decoded_value:{self.sign}0{size}}".encode()
Convert python type to bytes
Inherited Members
286class RGB(PythonConverter): 287 """convert to/from three int tuple""" 288 289 def to_bytes_impl(self, decoded_value: tuple[int, int, int], size: int) -> bytes: 290 assert size == 3 291 return ( 292 decoded_value[0].to_bytes(1, "big") 293 + decoded_value[1].to_bytes(1, "big") 294 + decoded_value[2].to_bytes(1, "big") 295 ) 296 297 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int, int]: 298 return (encoded_value[0], encoded_value[1], encoded_value[2])
convert to/from three int tuple
289 def to_bytes_impl(self, decoded_value: tuple[int, int, int], size: int) -> bytes: 290 assert size == 3 291 return ( 292 decoded_value[0].to_bytes(1, "big") 293 + decoded_value[1].to_bytes(1, "big") 294 + decoded_value[2].to_bytes(1, "big") 295 )
Convert python type to bytes
297 def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int, int]: 298 return (encoded_value[0], encoded_value[1], encoded_value[2])
Convert bytes to python type
Inherited Members
323class RangeCheck(abc.ABC): 324 """Base Class for checking the range of a JBP field""" 325 326 @abc.abstractmethod 327 def isvalid(self, value: Any) -> bool: 328 """Returns ``True`` if field satisfies range check."""
Base Class for checking the range of a JBP field
331class AnyRange(RangeCheck): 332 """Field has no range restrictions""" 333 334 def isvalid(self, value: Any) -> bool: 335 return True
Field has no range restrictions
338class MinMax(RangeCheck): 339 """Field has a minimum and/or maximum value 340 341 Parameters 342 ---------- 343 minimum 344 Minimum value. A value of 'None' indicates no minimum. 345 maximum 346 Maximum value. A value of 'None' indicates no maximum. 347 """ 348 349 def __init__(self, minimum: int | float | None, maximum: int | float | None): 350 self.minimum = minimum 351 self.maximum = maximum 352 353 def isvalid(self, value: int | float) -> bool: 354 valid = True 355 if self.minimum is not None: 356 valid &= value >= self.minimum 357 if self.maximum is not None: 358 valid &= value <= self.maximum 359 return valid
Field has a minimum and/or maximum value
Parameters
- minimum: Minimum value. A value of 'None' indicates no minimum.
- maximum: Maximum value. A value of 'None' indicates no maximum.
353 def isvalid(self, value: int | float) -> bool: 354 valid = True 355 if self.minimum is not None: 356 valid &= value >= self.minimum 357 if self.maximum is not None: 358 valid &= value <= self.maximum 359 return valid
Returns True if field satisfies range check.
362class Regex(RangeCheck): 363 """Field value is restricted by a regex""" 364 365 def __init__(self, pattern: str): 366 self.pattern = pattern 367 368 def isvalid(self, value: str) -> bool: 369 return bool(re.fullmatch(self.pattern, value))
Field value is restricted by a regex
372class Constant(RangeCheck): 373 """Field value must be a constant""" 374 375 def __init__(self, const: Any): 376 self.const = const 377 378 def isvalid(self, value: Any) -> bool: 379 return value == self.const
Field value must be a constant
382class Enum(RangeCheck): 383 """Field value must match one value of an Enumeration""" 384 385 def __init__(self, enumeration: Iterable): 386 self.enumeration = set(enumeration) 387 388 def isvalid(self, value: Any) -> bool: 389 return value in self.enumeration
Field value must match one value of an Enumeration
392class AnyOf(RangeCheck): 393 """Field value must match at least one of many different RangeChecks 394 395 Parameters 396 ---------- 397 *ranges: RangeCheck 398 RangeCheck objects to check against 399 """ 400 401 def __init__(self, *ranges: RangeCheck): 402 self.ranges = ranges 403 404 def isvalid(self, value: Any) -> bool: 405 # Use any(generator) to ensure short circuit logic 406 return any(check.isvalid(value) for check in self.ranges)
Field value must match at least one of many different RangeChecks
Parameters
- *ranges (RangeCheck): RangeCheck objects to check against
409class AllOf(RangeCheck): 410 """Field value must match all of many different RangeChecks 411 412 Parameters 413 ---------- 414 *ranges: RangeCheck 415 RangeCheck objects to check against 416 """ 417 418 def __init__(self, *ranges: RangeCheck): 419 self.ranges = ranges 420 421 def isvalid(self, value: Any) -> bool: 422 # Use all(generator) to ensure short circuit logic 423 return all(check.isvalid(value) for check in self.ranges)
Field value must match all of many different RangeChecks
Parameters
- *ranges (RangeCheck): RangeCheck objects to check against
426class Not(RangeCheck): 427 """Negate a range check""" 428 429 def __init__(self, range_check: RangeCheck): 430 self.range_check = range_check 431 432 def isvalid(self, value: Any) -> bool: 433 return not self.range_check.isvalid(value)
Negate a range check
456class JbpIOComponent: 457 """Base Class for read/writable JBP components""" 458 459 def __init__(self, name: str): 460 self.name = name 461 self._parent: ComponentCollection | None = None 462 463 def load(self, fd: BinaryFile_R) -> Self: 464 """Read from a file descriptor 465 466 Parameters 467 ---------- 468 fd : file-like 469 Binary file-like object to read from 470 471 Returns 472 ------- 473 A reference to self 474 """ 475 try: 476 self._load_impl(fd) 477 return self 478 except Exception: 479 logger.error(f"Failed to read {self.name}") 480 raise 481 482 def dump(self, fd: BinaryFile_RW, seek_first: bool = False) -> int: 483 """Write to a file descriptor 484 485 Parameters 486 ---------- 487 fd : file-like 488 Binary file-like object to write to 489 seek_first : bool 490 Seek to the components offset before writing 491 492 Returns 493 ------- 494 int 495 Number of bytes written 496 """ 497 if seek_first: 498 fd.seek(self.get_offset(), os.SEEK_SET) 499 500 try: 501 return self._dump_impl(fd) 502 except Exception: 503 logger.error(f"Failed to write {self.name}") 504 raise 505 506 def _load_impl(self, fd: BinaryFile_R) -> None: 507 raise NotImplementedError() 508 509 def _dump_impl(self, fd: BinaryFile_RW) -> int: 510 raise NotImplementedError() 511 512 def get_offset(self) -> int: 513 """Return the offset from the start of the file to this component""" 514 offset = 0 515 if self._parent is not None: 516 offset = self._parent.get_offset_of(self) 517 return offset 518 519 def get_size(self) -> int: 520 """Size of this component in bytes""" 521 raise NotImplementedError() 522 523 def as_json(self, full: bool = False) -> str: 524 """Return a JSON representation of the component 525 526 Parameters 527 ---------- 528 full : bool 529 Include additional details such as offset and length 530 """ 531 return json.dumps(self, indent=2, cls=_JsonEncoder, full_details=full) 532 533 def as_text(self) -> str: 534 """Return a text representation of the component""" 535 buf = io.StringIO() 536 self.print(file=buf) 537 return buf.getvalue() 538 539 def print(self, *, file=None) -> None: 540 """Print information about the component to stdout""" 541 raise NotImplementedError() 542 543 def finalize(self): 544 """Perform any necessary final updates""" 545 546 def as_filelike(self, file: Any) -> SubFile: 547 """Create file object containing just this component 548 549 Parameters 550 ---------- 551 file : file-like 552 File object for entire file 553 554 Returns 555 ------- 556 SubFile 557 File like object for this component 558 """ 559 return SubFile(file, self.get_offset(), self.get_size())
Base Class for read/writable JBP components
463 def load(self, fd: BinaryFile_R) -> Self: 464 """Read from a file descriptor 465 466 Parameters 467 ---------- 468 fd : file-like 469 Binary file-like object to read from 470 471 Returns 472 ------- 473 A reference to self 474 """ 475 try: 476 self._load_impl(fd) 477 return self 478 except Exception: 479 logger.error(f"Failed to read {self.name}") 480 raise
Read from a file descriptor
Parameters
- fd (file-like): Binary file-like object to read from
Returns
- A reference to self
482 def dump(self, fd: BinaryFile_RW, seek_first: bool = False) -> int: 483 """Write to a file descriptor 484 485 Parameters 486 ---------- 487 fd : file-like 488 Binary file-like object to write to 489 seek_first : bool 490 Seek to the components offset before writing 491 492 Returns 493 ------- 494 int 495 Number of bytes written 496 """ 497 if seek_first: 498 fd.seek(self.get_offset(), os.SEEK_SET) 499 500 try: 501 return self._dump_impl(fd) 502 except Exception: 503 logger.error(f"Failed to write {self.name}") 504 raise
Write to a file descriptor
Parameters
- fd (file-like): Binary file-like object to write to
- seek_first (bool): Seek to the components offset before writing
Returns
- int: Number of bytes written
512 def get_offset(self) -> int: 513 """Return the offset from the start of the file to this component""" 514 offset = 0 515 if self._parent is not None: 516 offset = self._parent.get_offset_of(self) 517 return offset
Return the offset from the start of the file to this component
519 def get_size(self) -> int: 520 """Size of this component in bytes""" 521 raise NotImplementedError()
Size of this component in bytes
523 def as_json(self, full: bool = False) -> str: 524 """Return a JSON representation of the component 525 526 Parameters 527 ---------- 528 full : bool 529 Include additional details such as offset and length 530 """ 531 return json.dumps(self, indent=2, cls=_JsonEncoder, full_details=full)
Return a JSON representation of the component
Parameters
- full (bool): Include additional details such as offset and length
533 def as_text(self) -> str: 534 """Return a text representation of the component""" 535 buf = io.StringIO() 536 self.print(file=buf) 537 return buf.getvalue()
Return a text representation of the component
539 def print(self, *, file=None) -> None: 540 """Print information about the component to stdout""" 541 raise NotImplementedError()
Print information about the component to stdout
546 def as_filelike(self, file: Any) -> SubFile: 547 """Create file object containing just this component 548 549 Parameters 550 ---------- 551 file : file-like 552 File object for entire file 553 554 Returns 555 ------- 556 SubFile 557 File like object for this component 558 """ 559 return SubFile(file, self.get_offset(), self.get_size())
Create file object containing just this component
Parameters
- file (file-like): File object for entire file
Returns
- SubFile: File like object for this component
562class Field(JbpIOComponent): 563 """JBP Field containing a single value. 564 Intended to have 1:1 mapping to rows in JBP-2025.1 header tables. 565 566 Parameters 567 ---------- 568 name : str 569 Name of this field 570 description : str 571 Text description of the field 572 size : int 573 Size in bytes of the field 574 charset : str or None, optional 575 regex expression matching a single character. If ``None``, character set check is skipped. 576 encoded_range : RangeCheck or None, optional 577 Checker for the encoded value. If ``None``, encoded validation is skipped. 578 decoded_range : RangeCheck or None, optional 579 Checker for the decoded value. If ``None``, decoded validation is skipped. 580 converter : PythonConverter 581 Object to use for converting to/from python data types 582 default : any 583 Initial python value of the field 584 setter_callback : callable or None, optional 585 function to call if the field's value changes 586 nullable : bool, optional 587 ``True`` if BCS-A spaces are allowed for entire field (often denoted with "<>" in JBP Field Type). 588 When ``True``, charset, range checks, conversion, etc. are bypassed when the python-typed value is ``None``. 589 590 Attributes 591 ---------- 592 description: str 593 Text description of the field. For informational purposes only. 594 size: int 595 Field size in bytes 596 nullable: bool 597 ``True`` if BCS-A spaces are allowed for entire field 598 encoded_value: bytes 599 Field value as bytes 600 value 601 Field value as python type 602 """ 603 604 def __init__( 605 self, 606 name: str, 607 description: str, 608 size: int, 609 *, 610 charset: str | None = None, 611 encoded_range: RangeCheck | None = None, 612 decoded_range: RangeCheck | None = None, 613 converter: PythonConverter, 614 default: Any, 615 setter_callback: Callable | None = None, 616 nullable: bool = False, 617 ): 618 super().__init__(name) 619 self.description = description 620 self.nullable = nullable 621 self._size = size 622 self._charset = charset 623 self._encoded_range_check = encoded_range 624 self._decoded_range_check = decoded_range 625 self._converter = converter 626 self._setter_callback = setter_callback 627 628 encoded_default = self._encode(default) 629 if len(encoded_default) != size: 630 raise ValueError( 631 f"Field {name} {default=} does not encode to the proper {size=}" 632 ) 633 self._encoded_value = encoded_default 634 635 def __eq__(self, other): 636 if not isinstance(other, type(self)): 637 return NotImplemented 638 639 return ( 640 self.name == other.name 641 and self.description == other.description 642 and self._charset == other._charset 643 and self.encoded_value == other.encoded_value 644 ) 645 646 def _encode(self, val: Any) -> bytes: 647 if self.nullable and val is None: 648 return BCSA_SPACE.encode() * self.size 649 return self._converter.to_bytes(val, self.size) 650 651 def isnull(self) -> bool: 652 """Return True if Field is nullable and all bytes are BCS spaces""" 653 return self.nullable and self.encoded_value == BCSA_SPACE.encode() * len( 654 self.encoded_value 655 ) 656 657 def isvalid(self) -> bool: 658 """Check if the field value matches the required character set and range restrictions""" 659 if self.isnull(): 660 return True 661 662 if self._charset is not None: 663 valid_charset = bool( 664 re.fullmatch(f"[{self._charset}]*", self.encoded_value.decode()) 665 ) 666 if not valid_charset: 667 return False 668 669 if self._encoded_range_check is not None: 670 valid_encoding = self._encoded_range_check.isvalid(self.encoded_value) 671 if not valid_encoding: 672 return False 673 674 if self._decoded_range_check is not None: 675 valid_decoding = self._decoded_range_check.isvalid(self.value) 676 if not valid_decoding: 677 return False 678 679 return True 680 681 @property 682 def encoded_value(self) -> bytes: 683 return self._encoded_value 684 685 @encoded_value.setter 686 def encoded_value(self, value: bytes): 687 truncated = value[: self.size] 688 if len(truncated) < len(value): 689 logger.warning( 690 f"JBP header field {self.name} truncated to {self.size} characters.\n" 691 f" old: {value!r}" 692 f" new: {truncated!r}" 693 ) 694 self._encoded_value = truncated 695 696 try: 697 if not self.isvalid(): 698 logger.warning( 699 f"{self.name}: Invalid field value: {self.encoded_value!r}" 700 ) 701 except Exception: 702 logger.exception( 703 f"An exception occurred when trying to validate {self.name}:" 704 ) 705 706 @property 707 def size(self) -> int: 708 return self._size 709 710 @size.setter 711 def size(self, value: int): 712 old_value = self._size 713 self._size = value 714 715 if (old_value != self._size) and self._setter_callback: 716 self._setter_callback(self) 717 718 @property 719 def value(self) -> Any: 720 if self.isnull(): 721 return None 722 return self._converter.from_bytes(self.encoded_value) 723 724 @value.setter 725 def value(self, val: Any): 726 self._set_value(val, callback=self._setter_callback) 727 728 def _set_value(self, val, callback=None): 729 self.encoded_value = self._encode(val) 730 731 if callback: 732 callback(self) 733 734 def _load_impl(self, fd: BinaryFile_R) -> None: 735 self.encoded_value = fd.read(self.size) 736 737 if self._setter_callback: 738 self._setter_callback(self) 739 740 def _dump_impl(self, fd: BinaryFile_RW) -> int: 741 return fd.write(self.encoded_value) 742 743 def get_size(self) -> int: 744 return self.size 745 746 def print(self, *, file=None) -> None: 747 print( 748 f"{self.name:15}{self.size:11} @ {self.get_offset():11} {self.encoded_value!r}", 749 file=file, 750 )
JBP Field containing a single value. Intended to have 1:1 mapping to rows in JBP-2025.1 header tables.
Parameters
- name (str): Name of this field
- description (str): Text description of the field
- size (int): Size in bytes of the field
- charset (str or None, optional):
regex expression matching a single character. If
None, character set check is skipped. - encoded_range (RangeCheck or None, optional):
Checker for the encoded value. If
None, encoded validation is skipped. - decoded_range (RangeCheck or None, optional):
Checker for the decoded value. If
None, decoded validation is skipped. - converter (PythonConverter): Object to use for converting to/from python data types
- default (any): Initial python value of the field
- setter_callback (callable or None, optional): function to call if the field's value changes
- nullable (bool, optional):
Trueif BCS-A spaces are allowed for entire field (often denoted with "<>" in JBP Field Type). WhenTrue, charset, range checks, conversion, etc. are bypassed when the python-typed value isNone.
Attributes
- description (str): Text description of the field. For informational purposes only.
- size (int): Field size in bytes
- nullable (bool):
Trueif BCS-A spaces are allowed for entire field - encoded_value (bytes): Field value as bytes
- value: Field value as python type
604 def __init__( 605 self, 606 name: str, 607 description: str, 608 size: int, 609 *, 610 charset: str | None = None, 611 encoded_range: RangeCheck | None = None, 612 decoded_range: RangeCheck | None = None, 613 converter: PythonConverter, 614 default: Any, 615 setter_callback: Callable | None = None, 616 nullable: bool = False, 617 ): 618 super().__init__(name) 619 self.description = description 620 self.nullable = nullable 621 self._size = size 622 self._charset = charset 623 self._encoded_range_check = encoded_range 624 self._decoded_range_check = decoded_range 625 self._converter = converter 626 self._setter_callback = setter_callback 627 628 encoded_default = self._encode(default) 629 if len(encoded_default) != size: 630 raise ValueError( 631 f"Field {name} {default=} does not encode to the proper {size=}" 632 ) 633 self._encoded_value = encoded_default
651 def isnull(self) -> bool: 652 """Return True if Field is nullable and all bytes are BCS spaces""" 653 return self.nullable and self.encoded_value == BCSA_SPACE.encode() * len( 654 self.encoded_value 655 )
Return True if Field is nullable and all bytes are BCS spaces
657 def isvalid(self) -> bool: 658 """Check if the field value matches the required character set and range restrictions""" 659 if self.isnull(): 660 return True 661 662 if self._charset is not None: 663 valid_charset = bool( 664 re.fullmatch(f"[{self._charset}]*", self.encoded_value.decode()) 665 ) 666 if not valid_charset: 667 return False 668 669 if self._encoded_range_check is not None: 670 valid_encoding = self._encoded_range_check.isvalid(self.encoded_value) 671 if not valid_encoding: 672 return False 673 674 if self._decoded_range_check is not None: 675 valid_decoding = self._decoded_range_check.isvalid(self.value) 676 if not valid_decoding: 677 return False 678 679 return True
Check if the field value matches the required character set and range restrictions
746 def print(self, *, file=None) -> None: 747 print( 748 f"{self.name:15}{self.size:11} @ {self.get_offset():11} {self.encoded_value!r}", 749 file=file, 750 )
Print information about the component to stdout
Inherited Members
753class BinaryPlaceholder(JbpIOComponent): 754 """Represents a block of large binary data. 755 756 This class does not actually read, write or store data, only seek past it. 757 """ 758 759 def __init__(self, name: str, size: int): 760 super().__init__(name) 761 self._size = size 762 763 def __eq__(self, other): 764 if not isinstance(other, type(self)): 765 return NotImplemented 766 767 return self.name == other.name and self._size == other._size 768 769 @property 770 def size(self) -> int: 771 return self._size 772 773 @size.setter 774 def size(self, value: int): 775 self._size = value 776 777 def _load_impl(self, fd: BinaryFile_R): 778 fd.seek(self.size, os.SEEK_CUR) 779 780 def _dump_impl(self, fd: BinaryFile_RW) -> int: 781 if self.size: 782 fd.seek(self.size, os.SEEK_CUR) 783 return self.size 784 785 def get_size(self) -> int: 786 return self.size 787 788 def print(self, *, file=None) -> None: 789 print( 790 f"{self.name:15}{self.size:11} @ {self.get_offset():11} <Binary>", file=file 791 )
Represents a block of large binary data.
This class does not actually read, write or store data, only seek past it.
788 def print(self, *, file=None) -> None: 789 print( 790 f"{self.name:15}{self.size:11} @ {self.get_offset():11} <Binary>", file=file 791 )
Print information about the component to stdout
Inherited Members
794class ComponentCollection(JbpIOComponent): 795 """Base class for components with child sub-components""" 796 797 def __init__(self, name: str): 798 super().__init__(name) 799 self._children: Final[list[JbpIOComponent]] = [] 800 801 def __eq__(self, other): 802 if not isinstance(other, type(self)): 803 return NotImplemented 804 805 return len(self._children) == len(other._children) and all( 806 [left == right for left, right in zip(self._children, other._children)] 807 ) 808 809 def __len__(self) -> int: 810 return len(self._children) 811 812 def _contains(self, item): 813 in_children = item in self._children 814 is_parent_set = item._parent == self 815 assert in_children == is_parent_set 816 return in_children 817 818 def get_size(self) -> int: 819 size = 0 820 for child in self._children: 821 size += child.get_size() 822 return size 823 824 def _load_impl(self, fd: BinaryFile_R) -> None: 825 for child in self._children: 826 child.load(fd) 827 828 def _dump_impl(self, fd: BinaryFile_RW) -> int: 829 written = 0 830 for child in self._children: 831 written += child.dump(fd) 832 return written 833 834 def _append(self, field: JbpIOComponent) -> None: 835 field._parent = self 836 self._children.append(field) 837 838 def _extend(self, fields: Iterable[JbpIOComponent]) -> None: 839 for field in fields: 840 self._append(field) 841 842 def _replace(self, old_field: JbpIOComponent, new_field: JbpIOComponent) -> None: 843 if not self._contains(old_field): 844 raise ValueError("old_field must be in collection") 845 if new_field._parent is not None: 846 raise ValueError("new_field already has a parent") 847 self._children[self._children.index(old_field)] = new_field 848 new_field._parent = self 849 850 def get_offset_of(self, child_obj: JbpIOComponent) -> int: 851 offset = self.get_offset() 852 853 for child in self._children: 854 if child is child_obj: 855 return offset 856 else: 857 offset += child.get_size() 858 else: 859 raise ValueError(f"Could not find {child_obj.name}") 860 861 def print(self, *, file=None) -> None: 862 for child in self._children: 863 child.print(file=file) 864 865 def finalize(self): 866 for child in self._children: 867 child.finalize()
Base class for components with child sub-components
818 def get_size(self) -> int: 819 size = 0 820 for child in self._children: 821 size += child.get_size() 822 return size
Size of this component in bytes
861 def print(self, *, file=None) -> None: 862 for child in self._children: 863 child.print(file=file)
Print information about the component to stdout
Inherited Members
870class Group(ComponentCollection, collections.abc.Mapping): 871 """ 872 A Collection of JBP fields. Indexed by JBP short names. 873 874 Parameters 875 ---------- 876 name : str 877 Name to give the group of fields 878 """ 879 880 def __init__(self, name): 881 super().__init__(name) 882 883 def _child_names(self) -> list[str]: 884 return [child.name for child in self._children] 885 886 def __iter__(self): 887 return iter(self._child_names()) 888 889 def __getitem__(self, key: str): 890 try: 891 index = self._index(key) 892 except ValueError: 893 raise KeyError(key) 894 895 return self._children[index] 896 897 def _insert_after( 898 self, existing: JbpIOComponent, *field: JbpIOComponent 899 ) -> JbpIOComponent: 900 insert_pos = self._children.index(existing) + 1 901 self._children[insert_pos:insert_pos] = field 902 for f in field: 903 f._parent = self 904 return f 905 906 def find_all(self, pattern: str) -> Iterator[JbpIOComponent]: 907 """Find child components with names matching a regex pattern 908 909 Parameters 910 ---------- 911 pattern : str 912 Regex pattern 913 914 Yields 915 ------ 916 child with name matching `pattern` 917 """ 918 for child in self._children[:]: 919 if re.fullmatch(pattern, child.name): 920 yield child 921 922 def _remove_all(self, pattern: str) -> None: 923 for child in self.find_all(pattern): 924 self._children.remove(child) 925 926 def _index(self, name: str) -> int: 927 return self._child_names().index(name)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
906 def find_all(self, pattern: str) -> Iterator[JbpIOComponent]: 907 """Find child components with names matching a regex pattern 908 909 Parameters 910 ---------- 911 pattern : str 912 Regex pattern 913 914 Yields 915 ------ 916 child with name matching `pattern` 917 """ 918 for child in self._children[:]: 919 if re.fullmatch(pattern, child.name): 920 yield child
Find child components with names matching a regex pattern
Parameters
- pattern (str): Regex pattern
Yields
- child with name matching
pattern
Inherited Members
930class SegmentList(ComponentCollection, collections.abc.Sequence): 931 """A sequence of JBP segments""" 932 933 def __init__( 934 self, 935 name: str, 936 field_creator: Callable[[str], Group], 937 minimum: int = 0, 938 maximum: int = 1, 939 ): 940 super().__init__(name) 941 self.field_creator = field_creator 942 self.minimum = minimum 943 self.maximum = maximum 944 self.set_count(self.minimum) 945 946 def __getitem__(self, idx): 947 return self._children[idx] 948 949 def set_count(self, size: int) -> None: 950 if not self.minimum <= size <= self.maximum: 951 raise ValueError(f"Invalid {size=}") 952 for idx in range(len(self._children), size): 953 new_field = self.field_creator(str(idx + 1)) 954 self._append(new_field) 955 for _ in range(size, len(self._children)): 956 self._children.pop()
A sequence of JBP segments
949 def set_count(self, size: int) -> None: 950 if not self.minimum <= size <= self.maximum: 951 raise ValueError(f"Invalid {size=}") 952 for idx in range(len(self._children), size): 953 new_field = self.field_creator(str(idx + 1)) 954 self._append(new_field) 955 for _ in range(size, len(self._children)): 956 self._children.pop()
Inherited Members
959class SecurityFields(Group): 960 """ 961 JBP security header/subheader fields 962 963 Parameters 964 ---------- 965 name : str 966 Name to give this component 967 x : str 968 Value to replace leading "x" of Short Name in fields 969 970 Notes 971 ----- 972 See JBP-2025.1 Table 5.10-1 and Table 5.10-2 973 """ 974 975 def __init__(self, name: str, x: str): 976 super().__init__(name) 977 self._append( 978 Field( 979 f"{x}SCLAS", 980 "Security Classification", 981 1, 982 charset=ECSA, 983 decoded_range=Enum(["T", "S", "C", "R", "U"]), 984 converter=StringISO8859_1(), 985 default="U", 986 ) 987 ) 988 self._append( 989 Field( 990 f"{x}SCLSY", 991 "Security Classification System", 992 2, 993 charset=ECSA, 994 converter=StringISO8859_1(), 995 default=None, 996 nullable=True, 997 ) 998 ) 999 self._append( 1000 Field( 1001 f"{x}SCODE", 1002 "Codewords", 1003 11, 1004 charset=ECSA, 1005 converter=StringISO8859_1(), 1006 default=None, 1007 nullable=True, 1008 ) 1009 ) 1010 self._append( 1011 Field( 1012 f"{x}SCTLH", 1013 "Control and Handling", 1014 2, 1015 charset=ECSA, 1016 converter=StringISO8859_1(), 1017 default=None, 1018 nullable=True, 1019 ) 1020 ) 1021 self._append( 1022 Field( 1023 f"{x}SREL", 1024 "Releasing Instructions", 1025 20, 1026 charset=ECSA, 1027 converter=StringISO8859_1(), 1028 default=None, 1029 nullable=True, 1030 ) 1031 ) 1032 self._append( 1033 Field( 1034 f"{x}SDCTP", 1035 "Declassification Type", 1036 2, 1037 charset=ECSA, 1038 decoded_range=Enum(["DD", "DE", "GD", "GE", "O", "X"]), 1039 converter=StringISO8859_1(), 1040 default=None, 1041 nullable=True, 1042 ) 1043 ) 1044 self._append( 1045 Field( 1046 f"{x}SDCDT", 1047 "Declassification Date", 1048 8, 1049 charset=ECSA, 1050 decoded_range=DATE_REGEX, 1051 converter=StringISO8859_1(), 1052 default=None, 1053 nullable=True, 1054 ) 1055 ) 1056 self._append( 1057 Field( 1058 f"{x}SDCXM", 1059 "Declassification Exemption", 1060 4, 1061 charset=ECSA, 1062 converter=StringISO8859_1(), 1063 default=None, 1064 nullable=True, 1065 ) 1066 ) 1067 self._append( 1068 Field( 1069 f"{x}SDG", 1070 "Downgrade", 1071 1, 1072 charset=ECSA, 1073 decoded_range=Enum(["S", "C", "R"]), 1074 converter=StringISO8859_1(), 1075 default=None, 1076 nullable=True, 1077 ) 1078 ) 1079 self._append( 1080 Field( 1081 f"{x}SDGDT", 1082 "Downgrade Date", 1083 8, 1084 charset=ECSA, 1085 decoded_range=DATE_REGEX, 1086 converter=StringISO8859_1(), 1087 default=None, 1088 nullable=True, 1089 ) 1090 ) 1091 self._append( 1092 Field( 1093 f"{x}SCLTX", 1094 "Classification Text", 1095 43, 1096 charset=ECSA, 1097 converter=StringISO8859_1(), 1098 default=None, 1099 nullable=True, 1100 ) 1101 ) 1102 self._append( 1103 Field( 1104 f"{x}SCATP", 1105 "Classification Authority Type", 1106 1, 1107 charset=ECSA, 1108 decoded_range=Enum(["O", "D", "M"]), 1109 converter=StringISO8859_1(), 1110 default=None, 1111 nullable=True, 1112 ) 1113 ) 1114 self._append( 1115 Field( 1116 f"{x}SCAUT", 1117 "Classification Authority", 1118 40, 1119 charset=ECSA, 1120 converter=StringISO8859_1(), 1121 default=None, 1122 nullable=True, 1123 ) 1124 ) 1125 self._append( 1126 Field( 1127 f"{x}SCRSN", 1128 "Classification Reason", 1129 1, 1130 charset=ECSA, 1131 converter=StringISO8859_1(), 1132 default=None, 1133 nullable=True, 1134 ) 1135 ) 1136 self._append( 1137 Field( 1138 f"{x}SSRDT", 1139 "Security Source Date", 1140 8, 1141 charset=ECSA, 1142 decoded_range=DATE_REGEX, 1143 converter=StringISO8859_1(), 1144 default=None, 1145 nullable=True, 1146 ) 1147 ) 1148 self._append( 1149 Field( 1150 f"{x}SCTLN", 1151 "Security Control Number", 1152 15, 1153 charset=ECSA, 1154 converter=StringISO8859_1(), 1155 default=None, 1156 nullable=True, 1157 ) 1158 )
JBP security header/subheader fields
Parameters
- name (str): Name to give this component
- x (str): Value to replace leading "x" of Short Name in fields
Notes
See JBP-2025.1 Table 5.10-1 and Table 5.10-2
975 def __init__(self, name: str, x: str): 976 super().__init__(name) 977 self._append( 978 Field( 979 f"{x}SCLAS", 980 "Security Classification", 981 1, 982 charset=ECSA, 983 decoded_range=Enum(["T", "S", "C", "R", "U"]), 984 converter=StringISO8859_1(), 985 default="U", 986 ) 987 ) 988 self._append( 989 Field( 990 f"{x}SCLSY", 991 "Security Classification System", 992 2, 993 charset=ECSA, 994 converter=StringISO8859_1(), 995 default=None, 996 nullable=True, 997 ) 998 ) 999 self._append( 1000 Field( 1001 f"{x}SCODE", 1002 "Codewords", 1003 11, 1004 charset=ECSA, 1005 converter=StringISO8859_1(), 1006 default=None, 1007 nullable=True, 1008 ) 1009 ) 1010 self._append( 1011 Field( 1012 f"{x}SCTLH", 1013 "Control and Handling", 1014 2, 1015 charset=ECSA, 1016 converter=StringISO8859_1(), 1017 default=None, 1018 nullable=True, 1019 ) 1020 ) 1021 self._append( 1022 Field( 1023 f"{x}SREL", 1024 "Releasing Instructions", 1025 20, 1026 charset=ECSA, 1027 converter=StringISO8859_1(), 1028 default=None, 1029 nullable=True, 1030 ) 1031 ) 1032 self._append( 1033 Field( 1034 f"{x}SDCTP", 1035 "Declassification Type", 1036 2, 1037 charset=ECSA, 1038 decoded_range=Enum(["DD", "DE", "GD", "GE", "O", "X"]), 1039 converter=StringISO8859_1(), 1040 default=None, 1041 nullable=True, 1042 ) 1043 ) 1044 self._append( 1045 Field( 1046 f"{x}SDCDT", 1047 "Declassification Date", 1048 8, 1049 charset=ECSA, 1050 decoded_range=DATE_REGEX, 1051 converter=StringISO8859_1(), 1052 default=None, 1053 nullable=True, 1054 ) 1055 ) 1056 self._append( 1057 Field( 1058 f"{x}SDCXM", 1059 "Declassification Exemption", 1060 4, 1061 charset=ECSA, 1062 converter=StringISO8859_1(), 1063 default=None, 1064 nullable=True, 1065 ) 1066 ) 1067 self._append( 1068 Field( 1069 f"{x}SDG", 1070 "Downgrade", 1071 1, 1072 charset=ECSA, 1073 decoded_range=Enum(["S", "C", "R"]), 1074 converter=StringISO8859_1(), 1075 default=None, 1076 nullable=True, 1077 ) 1078 ) 1079 self._append( 1080 Field( 1081 f"{x}SDGDT", 1082 "Downgrade Date", 1083 8, 1084 charset=ECSA, 1085 decoded_range=DATE_REGEX, 1086 converter=StringISO8859_1(), 1087 default=None, 1088 nullable=True, 1089 ) 1090 ) 1091 self._append( 1092 Field( 1093 f"{x}SCLTX", 1094 "Classification Text", 1095 43, 1096 charset=ECSA, 1097 converter=StringISO8859_1(), 1098 default=None, 1099 nullable=True, 1100 ) 1101 ) 1102 self._append( 1103 Field( 1104 f"{x}SCATP", 1105 "Classification Authority Type", 1106 1, 1107 charset=ECSA, 1108 decoded_range=Enum(["O", "D", "M"]), 1109 converter=StringISO8859_1(), 1110 default=None, 1111 nullable=True, 1112 ) 1113 ) 1114 self._append( 1115 Field( 1116 f"{x}SCAUT", 1117 "Classification Authority", 1118 40, 1119 charset=ECSA, 1120 converter=StringISO8859_1(), 1121 default=None, 1122 nullable=True, 1123 ) 1124 ) 1125 self._append( 1126 Field( 1127 f"{x}SCRSN", 1128 "Classification Reason", 1129 1, 1130 charset=ECSA, 1131 converter=StringISO8859_1(), 1132 default=None, 1133 nullable=True, 1134 ) 1135 ) 1136 self._append( 1137 Field( 1138 f"{x}SSRDT", 1139 "Security Source Date", 1140 8, 1141 charset=ECSA, 1142 decoded_range=DATE_REGEX, 1143 converter=StringISO8859_1(), 1144 default=None, 1145 nullable=True, 1146 ) 1147 ) 1148 self._append( 1149 Field( 1150 f"{x}SCTLN", 1151 "Security Control Number", 1152 15, 1153 charset=ECSA, 1154 converter=StringISO8859_1(), 1155 default=None, 1156 nullable=True, 1157 ) 1158 )
Inherited Members
1161class FileHeader(Group): 1162 """ 1163 JBP File Header 1164 1165 Parameters 1166 ---------- 1167 name : str 1168 Name to give the object 1169 numi_callback : callable 1170 Function to call when NUMI changes 1171 lin_callback : callable 1172 Function to call when LIn changes 1173 nums_callback : callable 1174 Function to call when NUMS changes 1175 lsn_callback : callable 1176 Function to call when LSn changes 1177 numt_callback : callable 1178 Function to call when NUMT changes 1179 ltn_callback : callable 1180 Function to call when LTn changes 1181 numdes_callback : callable 1182 Function to call when NUMDES changes 1183 ldn_callback : callable 1184 Function to call when LDn changes 1185 numres_callback : callable 1186 Function to call when NUMRES changes 1187 lreshn_callback : callable 1188 Function to call when LRESHn changes 1189 lren_callback : callable 1190 Function to call when LREn changes 1191 1192 Notes 1193 ----- 1194 See JBP-2025.1 Table 5.11-1 1195 """ 1196 1197 def __init__( 1198 self, 1199 name: str, 1200 numi_callback: Callable | None = None, 1201 lin_callback: Callable | None = None, 1202 nums_callback: Callable | None = None, 1203 lsn_callback: Callable | None = None, 1204 numt_callback: Callable | None = None, 1205 ltn_callback: Callable | None = None, 1206 numdes_callback: Callable | None = None, 1207 ldn_callback: Callable | None = None, 1208 numres_callback: Callable | None = None, 1209 lreshn_callback: Callable | None = None, 1210 lren_callback: Callable | None = None, 1211 ): 1212 super().__init__(name) 1213 self.numi_callback = numi_callback 1214 self.lin_callback = lin_callback 1215 self.nums_callback = nums_callback 1216 self.lsn_callback = lsn_callback 1217 self.numt_callback = numt_callback 1218 self.ltn_callback = ltn_callback 1219 self.numdes_callback = numdes_callback 1220 self.ldn_callback = ldn_callback 1221 self.numres_callback = numres_callback 1222 self.lreshn_callback = lreshn_callback 1223 self.lren_callback = lren_callback 1224 1225 # Initialize list with required fields 1226 self._append( 1227 Field( 1228 "FHDR", 1229 "File Profile Name", 1230 4, 1231 charset=BCSA, 1232 decoded_range=Enum(["NITF", "NSIF"]), 1233 converter=StringAscii(), 1234 default="NITF", 1235 ) 1236 ) 1237 self._append( 1238 Field( 1239 "FVER", 1240 "File Version", 1241 5, 1242 charset=BCSA, 1243 decoded_range=Enum(["02.10", "01.01"]), 1244 converter=StringAscii(), 1245 default="02.10", 1246 ) 1247 ) 1248 self._append( 1249 Field( 1250 "CLEVEL", 1251 "Complexity Level", 1252 2, 1253 charset=BCSN_PI, 1254 decoded_range=MinMax(1, 99), 1255 converter=Integer(), 1256 default=99, 1257 ) 1258 ) 1259 self._append( 1260 Field( 1261 "STYPE", 1262 "Standard Type", 1263 4, 1264 charset=BCSA, 1265 decoded_range=Constant("BF01"), 1266 converter=StringAscii(), 1267 default="BF01", 1268 ) 1269 ) 1270 self._append( 1271 Field( 1272 "OSTAID", 1273 "Originating Station ID", 1274 10, 1275 charset=BCSA, 1276 decoded_range=Not(Constant("")), 1277 converter=StringAscii(), 1278 default="unknown", 1279 ) 1280 ) 1281 self._append( 1282 Field( 1283 "FDT", 1284 "File Date and Time", 1285 14, 1286 charset=BCSN_I, 1287 decoded_range=DATETIME_REGEX, 1288 converter=StringAscii(), 1289 default="-" * 14, 1290 ) 1291 ) 1292 self._append( 1293 Field( 1294 "FTITLE", 1295 "File Title", 1296 80, 1297 charset=ECSA, 1298 converter=StringISO8859_1(), 1299 default=None, 1300 nullable=True, 1301 ) 1302 ) 1303 self._extend(SecurityFields("File Header Security Fields", "F").values()) 1304 self._append( 1305 Field( 1306 "FSCOP", 1307 "File Copy Number", 1308 5, 1309 charset=BCSN_PI, 1310 converter=Integer(), 1311 default=0, 1312 ) 1313 ) 1314 self._append( 1315 Field( 1316 "FSCPYS", 1317 "File Number of Copies", 1318 5, 1319 charset=BCSN_PI, 1320 converter=Integer(), 1321 default=0, 1322 ) 1323 ) 1324 self._append( 1325 Field( 1326 "ENCRYP", 1327 "Encryption", 1328 1, 1329 charset=BCSN_PI, 1330 decoded_range=Constant(0), 1331 converter=Integer(), 1332 default=0, 1333 ) 1334 ) 1335 self._append( 1336 Field( 1337 "FBKGC", 1338 "File Background Color", 1339 3, 1340 converter=RGB(), 1341 default=(0, 0, 0), 1342 ) 1343 ) 1344 self._append( 1345 Field( 1346 "ONAME", 1347 "Originator's Name", 1348 24, 1349 charset=ECSA, 1350 converter=StringISO8859_1(), 1351 default=None, 1352 nullable=True, 1353 ) 1354 ) 1355 self._append( 1356 Field( 1357 "OPHONE", 1358 "Originator's Phone Number", 1359 18, 1360 charset=ECSA, 1361 converter=StringISO8859_1(), 1362 default=None, 1363 nullable=True, 1364 ) 1365 ) 1366 self._append( 1367 Field( 1368 "FL", 1369 "File Length", 1370 12, 1371 charset=BCSN_PI, 1372 decoded_range=MinMax(388, 999_999_999_998), 1373 converter=Integer(), 1374 default=388, 1375 ) 1376 ) 1377 self._append( 1378 Field( 1379 "HL", 1380 "JBP File Header Length", 1381 6, 1382 charset=BCSN_PI, 1383 decoded_range=MinMax(388, 999_999), 1384 converter=Integer(), 1385 default=388, 1386 ) 1387 ) 1388 self._append( 1389 Field( 1390 "NUMI", 1391 "Number of Image Segments", 1392 3, 1393 charset=BCSN_PI, 1394 converter=Integer(), 1395 default=0, 1396 setter_callback=self._numi_handler, 1397 ) 1398 ) 1399 self._append( 1400 Field( 1401 "NUMS", 1402 "Number of Graphic Segments", 1403 3, 1404 charset=BCSN_PI, 1405 converter=Integer(), 1406 default=0, 1407 setter_callback=self._nums_handler, 1408 ) 1409 ) 1410 self._append( 1411 Field( 1412 "NUMX", 1413 "Reserved for Future Use", 1414 3, 1415 charset=BCSN_PI, 1416 decoded_range=Constant(0), 1417 converter=Integer(), 1418 default=0, 1419 ) 1420 ) 1421 self._append( 1422 Field( 1423 "NUMT", 1424 "Number of Text Segments", 1425 3, 1426 charset=BCSN_PI, 1427 converter=Integer(), 1428 default=0, 1429 setter_callback=self._numt_handler, 1430 ) 1431 ) 1432 self._append( 1433 Field( 1434 "NUMDES", 1435 "Number of Data Extension Segments", 1436 3, 1437 charset=BCSN_PI, 1438 converter=Integer(), 1439 default=0, 1440 setter_callback=self._numdes_handler, 1441 ) 1442 ) 1443 self._append( 1444 Field( 1445 "NUMRES", 1446 "Number of Reserved Extension Segments", 1447 3, 1448 charset=BCSN_PI, 1449 converter=Integer(), 1450 default=0, 1451 setter_callback=self._numres_handler, 1452 ) 1453 ) 1454 self._append( 1455 Field( 1456 "UDHDL", 1457 "User Defined Header Data Length", 1458 5, 1459 charset=BCSN_PI, 1460 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1461 converter=Integer(), 1462 default=0, 1463 setter_callback=self._udhdl_handler, 1464 ) 1465 ) 1466 self._append( 1467 Field( 1468 "XHDL", 1469 "Extended Header Data Length", 1470 5, 1471 charset=BCSN_PI, 1472 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1473 converter=Integer(), 1474 default=0, 1475 setter_callback=self._xhdl_handler, 1476 ) 1477 ) 1478 1479 def _numi_handler(self, field: Field) -> None: 1480 """Handle NUMI value change""" 1481 self._remove_all("LISH\\d+") 1482 self._remove_all("LI\\d+") 1483 after: JbpIOComponent = field 1484 for idx in range(1, field.value + 1): 1485 after = self._insert_after( 1486 after, 1487 Field( 1488 f"LISH{idx:03}", 1489 "Length of nth Image Subheader", 1490 6, 1491 charset=BCSN_PI, 1492 decoded_range=MinMax(439, 999_999), 1493 converter=Integer(), 1494 default=439, 1495 ), 1496 ) 1497 after = self._insert_after( 1498 after, 1499 Field( 1500 f"LI{idx:03}", 1501 "Length of nth Image Segment", 1502 10, 1503 charset=BCSN_PI, 1504 decoded_range=MinMax(1, 10**10 - 1), 1505 converter=Integer(), 1506 setter_callback=self._lin_handler, 1507 default=1, 1508 ), 1509 ) 1510 if self.numi_callback: 1511 self.numi_callback(field) 1512 1513 def _lin_handler(self, field: Field) -> None: 1514 """Handle LIN value change""" 1515 if self.lin_callback: 1516 self.lin_callback(field) 1517 1518 def _nums_handler(self, field: Field) -> None: 1519 self._remove_all("LSSH\\d+") 1520 self._remove_all("LS\\d+") 1521 after: JbpIOComponent = field 1522 for idx in range(1, field.value + 1): 1523 after = self._insert_after( 1524 after, 1525 Field( 1526 f"LSSH{idx:03}", 1527 "Length of nth Graphic Subheader", 1528 4, 1529 charset=BCSN_PI, 1530 decoded_range=MinMax(258, 999_999), 1531 converter=Integer(), 1532 default=258, 1533 ), 1534 ) 1535 after = self._insert_after( 1536 after, 1537 Field( 1538 f"LS{idx:03}", 1539 "Length of nth Graphic Segment", 1540 6, 1541 charset=BCSN_PI, 1542 decoded_range=MinMax(1, 10**10 - 1), 1543 converter=Integer(), 1544 setter_callback=self._lsn_handler, 1545 default=1, 1546 ), 1547 ) 1548 1549 if self.nums_callback: 1550 self.nums_callback(field) 1551 1552 def _lsn_handler(self, field: Field) -> None: 1553 if self.lsn_callback: 1554 self.lsn_callback(field) 1555 1556 def _numt_handler(self, field: Field) -> None: 1557 self._remove_all("LTSH\\d+") 1558 self._remove_all("LT\\d+") 1559 after: JbpIOComponent = field 1560 for idx in range(1, field.value + 1): 1561 after = self._insert_after( 1562 after, 1563 Field( 1564 f"LTSH{idx:03}", 1565 "Length of nth Text Subheader", 1566 4, 1567 charset=BCSN_PI, 1568 decoded_range=MinMax(282, 999_999), 1569 converter=Integer(), 1570 default=282, 1571 ), 1572 ) 1573 after = self._insert_after( 1574 after, 1575 Field( 1576 f"LT{idx:03}", 1577 "Length of nth Text Segment", 1578 5, 1579 charset=BCSN_PI, 1580 decoded_range=MinMax(1, 99_999), 1581 converter=Integer(), 1582 setter_callback=self._ltn_handler, 1583 default=1, 1584 ), 1585 ) 1586 1587 if self.numt_callback: 1588 self.numt_callback(field) 1589 1590 def _ltn_handler(self, field: Field) -> None: 1591 if self.ltn_callback: 1592 self.ltn_callback(field) 1593 1594 def _numdes_handler(self, field: Field) -> None: 1595 self._remove_all("LDSH\\d+") 1596 self._remove_all("LD\\d+") 1597 after: JbpIOComponent = field 1598 for idx in range(1, field.value + 1): 1599 after = self._insert_after( 1600 after, 1601 Field( 1602 f"LDSH{idx:03}", 1603 "Length of nth Data Extension Segment Subheader", 1604 4, 1605 charset=BCSN_PI, 1606 decoded_range=MinMax(200, 999_999), 1607 converter=Integer(), 1608 default=200, 1609 ), 1610 ) 1611 after = self._insert_after( 1612 after, 1613 Field( 1614 f"LD{idx:03}", 1615 "Length of nth Data Extension Segment", 1616 9, 1617 charset=BCSN_PI, 1618 decoded_range=MinMax(1, 10**9 - 1), 1619 converter=Integer(), 1620 setter_callback=self._ldn_handler, 1621 default=1, 1622 ), 1623 ) 1624 1625 if self.numdes_callback: 1626 self.numdes_callback(field) 1627 1628 def _ldn_handler(self, field: Field) -> None: 1629 if self.ldn_callback: 1630 self.ldn_callback(field) 1631 1632 def _numres_handler(self, field: Field) -> None: 1633 self._remove_all("LRESH\\d+") 1634 self._remove_all("LRE\\d+") 1635 after: JbpIOComponent = field 1636 for idx in range(1, field.value + 1): 1637 after = self._insert_after( 1638 after, 1639 Field( 1640 f"LRESH{idx:03}", 1641 "Length of nth Reserved Extension Segment Subheader", 1642 4, 1643 charset=BCSN_PI, 1644 decoded_range=MinMax(LRESH_MIN, 999_999), 1645 converter=Integer(), 1646 default=LRESH_MIN, 1647 setter_callback=self._lreshn_handler, 1648 ), 1649 ) 1650 after = self._insert_after( 1651 after, 1652 Field( 1653 f"LRE{idx:03}", 1654 "Length of nth Reserved Extension Segment", 1655 7, 1656 charset=BCSN_PI, 1657 decoded_range=MinMax(1, 10**7 - 1), 1658 converter=Integer(), 1659 default=1, 1660 setter_callback=self._lren_handler, 1661 ), 1662 ) 1663 1664 if self.numres_callback: 1665 self.numres_callback(field) 1666 1667 def _lreshn_handler(self, field: Field) -> None: 1668 if self.lreshn_callback: 1669 self.lreshn_callback(field) 1670 1671 def _lren_handler(self, field: Field) -> None: 1672 if self.lren_callback: 1673 self.lren_callback(field) 1674 1675 def _udhdl_handler(self, field: Field) -> None: 1676 self._remove_all("UDHOFL") 1677 self._remove_all("UDHD") 1678 after: JbpIOComponent = field 1679 if field.value: 1680 after = self._insert_after( 1681 after, 1682 Field( 1683 "UDHOFL", 1684 "User Defined Header Overflow", 1685 3, 1686 charset=BCSN_PI, 1687 converter=Integer(), 1688 default=0, 1689 ), 1690 ) 1691 if field.value > 3: 1692 after = self._insert_after(after, TreSequence("UDHD", field.value - 3)) 1693 1694 def _xhdl_handler(self, field: Field) -> None: 1695 self._remove_all("XHDLOFL") 1696 self._remove_all("XHD") 1697 after: JbpIOComponent = field 1698 if field.value: 1699 after = self._insert_after( 1700 after, 1701 Field( 1702 "XHDLOFL", 1703 "Extended Header Data Overflow", 1704 3, 1705 charset=BCSN_PI, 1706 converter=Integer(), 1707 default=0, 1708 ), 1709 ) 1710 if field.value > 3: 1711 after = self._insert_after(after, TreSequence("XHD", field.value - 3)) 1712 1713 def finalize(self) -> None: 1714 super().finalize() 1715 _update_tre_lengths(self, "UDHDL", "UDHOFL", "UDHD") 1716 _update_tre_lengths(self, "XHDL", "XHDLOFL", "XHD") 1717 # Other length fields are handled by the parent Jbp class
JBP File Header
Parameters
- name (str): Name to give the object
- numi_callback (callable): Function to call when NUMI changes
- lin_callback (callable): Function to call when LIn changes
- nums_callback (callable): Function to call when NUMS changes
- lsn_callback (callable): Function to call when LSn changes
- numt_callback (callable): Function to call when NUMT changes
- ltn_callback (callable): Function to call when LTn changes
- numdes_callback (callable): Function to call when NUMDES changes
- ldn_callback (callable): Function to call when LDn changes
- numres_callback (callable): Function to call when NUMRES changes
- lreshn_callback (callable): Function to call when LRESHn changes
- lren_callback (callable): Function to call when LREn changes
Notes
See JBP-2025.1 Table 5.11-1
1197 def __init__( 1198 self, 1199 name: str, 1200 numi_callback: Callable | None = None, 1201 lin_callback: Callable | None = None, 1202 nums_callback: Callable | None = None, 1203 lsn_callback: Callable | None = None, 1204 numt_callback: Callable | None = None, 1205 ltn_callback: Callable | None = None, 1206 numdes_callback: Callable | None = None, 1207 ldn_callback: Callable | None = None, 1208 numres_callback: Callable | None = None, 1209 lreshn_callback: Callable | None = None, 1210 lren_callback: Callable | None = None, 1211 ): 1212 super().__init__(name) 1213 self.numi_callback = numi_callback 1214 self.lin_callback = lin_callback 1215 self.nums_callback = nums_callback 1216 self.lsn_callback = lsn_callback 1217 self.numt_callback = numt_callback 1218 self.ltn_callback = ltn_callback 1219 self.numdes_callback = numdes_callback 1220 self.ldn_callback = ldn_callback 1221 self.numres_callback = numres_callback 1222 self.lreshn_callback = lreshn_callback 1223 self.lren_callback = lren_callback 1224 1225 # Initialize list with required fields 1226 self._append( 1227 Field( 1228 "FHDR", 1229 "File Profile Name", 1230 4, 1231 charset=BCSA, 1232 decoded_range=Enum(["NITF", "NSIF"]), 1233 converter=StringAscii(), 1234 default="NITF", 1235 ) 1236 ) 1237 self._append( 1238 Field( 1239 "FVER", 1240 "File Version", 1241 5, 1242 charset=BCSA, 1243 decoded_range=Enum(["02.10", "01.01"]), 1244 converter=StringAscii(), 1245 default="02.10", 1246 ) 1247 ) 1248 self._append( 1249 Field( 1250 "CLEVEL", 1251 "Complexity Level", 1252 2, 1253 charset=BCSN_PI, 1254 decoded_range=MinMax(1, 99), 1255 converter=Integer(), 1256 default=99, 1257 ) 1258 ) 1259 self._append( 1260 Field( 1261 "STYPE", 1262 "Standard Type", 1263 4, 1264 charset=BCSA, 1265 decoded_range=Constant("BF01"), 1266 converter=StringAscii(), 1267 default="BF01", 1268 ) 1269 ) 1270 self._append( 1271 Field( 1272 "OSTAID", 1273 "Originating Station ID", 1274 10, 1275 charset=BCSA, 1276 decoded_range=Not(Constant("")), 1277 converter=StringAscii(), 1278 default="unknown", 1279 ) 1280 ) 1281 self._append( 1282 Field( 1283 "FDT", 1284 "File Date and Time", 1285 14, 1286 charset=BCSN_I, 1287 decoded_range=DATETIME_REGEX, 1288 converter=StringAscii(), 1289 default="-" * 14, 1290 ) 1291 ) 1292 self._append( 1293 Field( 1294 "FTITLE", 1295 "File Title", 1296 80, 1297 charset=ECSA, 1298 converter=StringISO8859_1(), 1299 default=None, 1300 nullable=True, 1301 ) 1302 ) 1303 self._extend(SecurityFields("File Header Security Fields", "F").values()) 1304 self._append( 1305 Field( 1306 "FSCOP", 1307 "File Copy Number", 1308 5, 1309 charset=BCSN_PI, 1310 converter=Integer(), 1311 default=0, 1312 ) 1313 ) 1314 self._append( 1315 Field( 1316 "FSCPYS", 1317 "File Number of Copies", 1318 5, 1319 charset=BCSN_PI, 1320 converter=Integer(), 1321 default=0, 1322 ) 1323 ) 1324 self._append( 1325 Field( 1326 "ENCRYP", 1327 "Encryption", 1328 1, 1329 charset=BCSN_PI, 1330 decoded_range=Constant(0), 1331 converter=Integer(), 1332 default=0, 1333 ) 1334 ) 1335 self._append( 1336 Field( 1337 "FBKGC", 1338 "File Background Color", 1339 3, 1340 converter=RGB(), 1341 default=(0, 0, 0), 1342 ) 1343 ) 1344 self._append( 1345 Field( 1346 "ONAME", 1347 "Originator's Name", 1348 24, 1349 charset=ECSA, 1350 converter=StringISO8859_1(), 1351 default=None, 1352 nullable=True, 1353 ) 1354 ) 1355 self._append( 1356 Field( 1357 "OPHONE", 1358 "Originator's Phone Number", 1359 18, 1360 charset=ECSA, 1361 converter=StringISO8859_1(), 1362 default=None, 1363 nullable=True, 1364 ) 1365 ) 1366 self._append( 1367 Field( 1368 "FL", 1369 "File Length", 1370 12, 1371 charset=BCSN_PI, 1372 decoded_range=MinMax(388, 999_999_999_998), 1373 converter=Integer(), 1374 default=388, 1375 ) 1376 ) 1377 self._append( 1378 Field( 1379 "HL", 1380 "JBP File Header Length", 1381 6, 1382 charset=BCSN_PI, 1383 decoded_range=MinMax(388, 999_999), 1384 converter=Integer(), 1385 default=388, 1386 ) 1387 ) 1388 self._append( 1389 Field( 1390 "NUMI", 1391 "Number of Image Segments", 1392 3, 1393 charset=BCSN_PI, 1394 converter=Integer(), 1395 default=0, 1396 setter_callback=self._numi_handler, 1397 ) 1398 ) 1399 self._append( 1400 Field( 1401 "NUMS", 1402 "Number of Graphic Segments", 1403 3, 1404 charset=BCSN_PI, 1405 converter=Integer(), 1406 default=0, 1407 setter_callback=self._nums_handler, 1408 ) 1409 ) 1410 self._append( 1411 Field( 1412 "NUMX", 1413 "Reserved for Future Use", 1414 3, 1415 charset=BCSN_PI, 1416 decoded_range=Constant(0), 1417 converter=Integer(), 1418 default=0, 1419 ) 1420 ) 1421 self._append( 1422 Field( 1423 "NUMT", 1424 "Number of Text Segments", 1425 3, 1426 charset=BCSN_PI, 1427 converter=Integer(), 1428 default=0, 1429 setter_callback=self._numt_handler, 1430 ) 1431 ) 1432 self._append( 1433 Field( 1434 "NUMDES", 1435 "Number of Data Extension Segments", 1436 3, 1437 charset=BCSN_PI, 1438 converter=Integer(), 1439 default=0, 1440 setter_callback=self._numdes_handler, 1441 ) 1442 ) 1443 self._append( 1444 Field( 1445 "NUMRES", 1446 "Number of Reserved Extension Segments", 1447 3, 1448 charset=BCSN_PI, 1449 converter=Integer(), 1450 default=0, 1451 setter_callback=self._numres_handler, 1452 ) 1453 ) 1454 self._append( 1455 Field( 1456 "UDHDL", 1457 "User Defined Header Data Length", 1458 5, 1459 charset=BCSN_PI, 1460 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1461 converter=Integer(), 1462 default=0, 1463 setter_callback=self._udhdl_handler, 1464 ) 1465 ) 1466 self._append( 1467 Field( 1468 "XHDL", 1469 "Extended Header Data Length", 1470 5, 1471 charset=BCSN_PI, 1472 decoded_range=AnyOf(Constant(0), MinMax(3, 10**5 - 1)), 1473 converter=Integer(), 1474 default=0, 1475 setter_callback=self._xhdl_handler, 1476 ) 1477 )
1713 def finalize(self) -> None: 1714 super().finalize() 1715 _update_tre_lengths(self, "UDHDL", "UDHOFL", "UDHD") 1716 _update_tre_lengths(self, "XHDL", "XHDLOFL", "XHD") 1717 # Other length fields are handled by the parent Jbp class
Perform any necessary final updates
Inherited Members
1720class ImageSubheader(Group): 1721 """ 1722 Image Subheader fields 1723 1724 Parameters 1725 ---------- 1726 name : str 1727 Name to give this component 1728 1729 Notes 1730 ----- 1731 See JBP-2025.1 Table 5.13-1 1732 """ 1733 1734 def __init__(self, name: str): 1735 super().__init__(name) 1736 1737 self._append( 1738 Field( 1739 "IM", 1740 "File Part Type", 1741 2, 1742 charset=BCSA, 1743 decoded_range=Constant("IM"), 1744 converter=StringAscii(), 1745 default="IM", 1746 ) 1747 ) 1748 self._append( 1749 Field( 1750 "IID1", 1751 "Image Identifier 1", 1752 10, 1753 charset=BCSA, 1754 converter=StringAscii(), 1755 default="", 1756 ) 1757 ) 1758 self._append( 1759 Field( 1760 "IDATIM", 1761 "Image Date and Time", 1762 14, 1763 charset=BCSN, 1764 decoded_range=DATETIME_REGEX, 1765 converter=StringAscii(), 1766 default="-" * 14, 1767 ) 1768 ) 1769 self._append( 1770 Field( 1771 "TGTID", 1772 "Target Identifier", 1773 17, 1774 charset=BCSA, 1775 converter=StringISO8859_1(), 1776 default=None, 1777 nullable=True, 1778 ) 1779 ) 1780 self._append( 1781 Field( 1782 "IID2", 1783 "Image Identifier 2", 1784 80, 1785 charset=ECSA, 1786 converter=StringISO8859_1(), 1787 default=None, 1788 nullable=True, 1789 ) 1790 ) 1791 self._extend(SecurityFields("Security Fields Image", "I").values()) 1792 self._append( 1793 Field( 1794 "ENCRYP", 1795 "Encryption", 1796 1, 1797 charset=BCSN_PI, 1798 decoded_range=Constant(0), 1799 converter=Integer(), 1800 default=0, 1801 ) 1802 ) 1803 self._append( 1804 Field( 1805 "ISORCE", 1806 "Image Source", 1807 42, 1808 charset=ECSA, 1809 converter=StringISO8859_1(), 1810 default=None, 1811 nullable=True, 1812 ) 1813 ) 1814 self._append( 1815 Field( 1816 "NROWS", 1817 "Number of Significant Rows in Image", 1818 8, 1819 charset=BCSN_PI, 1820 decoded_range=MinMax(1, None), 1821 converter=Integer(), 1822 default=1, 1823 ) 1824 ) 1825 self._append( 1826 Field( 1827 "NCOLS", 1828 "Number of Significant Columns in Image", 1829 8, 1830 charset=BCSN_PI, 1831 decoded_range=MinMax(1, None), 1832 converter=Integer(), 1833 default=1, 1834 ) 1835 ) 1836 self._append( 1837 Field( 1838 "PVTYPE", 1839 "Pixel Value Type", 1840 3, 1841 charset=BCSA, 1842 decoded_range=Enum(["INT", "B", "SI", "R", "C"]), 1843 converter=StringAscii(), 1844 default="INT", 1845 ) 1846 ) 1847 self._append( 1848 Field( 1849 "IREP", 1850 "Image Representation", 1851 8, 1852 charset=BCSA, 1853 decoded_range=Enum( 1854 [ 1855 "MONO", 1856 "RGB", 1857 "RGB/LUT", 1858 "MULTI", 1859 "NODISPLY", 1860 "NVECTOR", 1861 "POLAR", 1862 "VPH", 1863 "YCbCr601", 1864 ] 1865 ), 1866 converter=StringAscii(), 1867 default="MONO", 1868 ) 1869 ) 1870 self._append( 1871 Field( 1872 "ICAT", 1873 "Image Category", 1874 8, 1875 charset=BCSA, 1876 converter=StringAscii(), 1877 default="VIS", 1878 ) 1879 ) 1880 self._append( 1881 Field( 1882 "ABPP", 1883 "Actual Bits-Per-Pixel Per Band", 1884 2, 1885 charset=BCSN_PI, 1886 decoded_range=MinMax(1, 96), 1887 converter=Integer(), 1888 default=1, 1889 ) 1890 ) 1891 self._append( 1892 Field( 1893 "PJUST", 1894 "Pixel Justification", 1895 1, 1896 charset=BCSA, 1897 decoded_range=Enum(["L", "R"]), 1898 converter=StringAscii(), 1899 default="R", 1900 ) 1901 ) 1902 self._append( 1903 Field( 1904 "ICORDS", 1905 "Image Coordinate Representation", 1906 1, 1907 charset=BCSA, 1908 decoded_range=Enum(["U", "G", "N", "S", "D"]), 1909 converter=StringAscii(), 1910 default=None, 1911 nullable=True, 1912 setter_callback=self._icords_handler, 1913 ) 1914 ) 1915 # IGEOLO 1916 self._append( 1917 Field( 1918 "NICOM", 1919 "Number of Image Comments", 1920 1, 1921 charset=BCSN_PI, 1922 converter=Integer(), 1923 default=0, 1924 setter_callback=self._nicom_handler, 1925 ) 1926 ) 1927 # ICOMn 1928 self._append( 1929 Field( 1930 "IC", 1931 "Image Compression", 1932 2, 1933 charset=BCSA, 1934 decoded_range=Enum( 1935 [ 1936 "NC", 1937 "NM", 1938 "C1", 1939 "C3", 1940 "C4", 1941 "C5", 1942 "C6", 1943 "C7", 1944 "C8", 1945 "I1", 1946 "M1", 1947 "M3", 1948 "M4", 1949 "M5", 1950 "M6", 1951 "M7", 1952 "M8", 1953 ] 1954 ), 1955 converter=StringAscii(), 1956 setter_callback=self._ic_handler, 1957 default="NC", 1958 ) 1959 ) 1960 # COMRAT 1961 self._append( 1962 Field( 1963 "NBANDS", 1964 "Number of Bands", 1965 1, 1966 charset=BCSN_PI, 1967 converter=Integer(), 1968 setter_callback=self._nbands_handler, 1969 default=1, 1970 ) 1971 ) 1972 # XBANDS 1973 # IREPBANDn 1974 # ISUBCATn 1975 # IFCn 1976 # IMFLTn 1977 # NLUTSn 1978 # NELUTn 1979 # LUTDn 1980 self._append( 1981 Field( 1982 "ISYNC", 1983 "Image Sync Code", 1984 1, 1985 charset=BCSN_PI, 1986 decoded_range=Constant(0), 1987 converter=Integer(), 1988 default=0, 1989 ) 1990 ) 1991 self._append( 1992 Field( 1993 "IMODE", 1994 "Image Mode", 1995 1, 1996 charset=BCSA, 1997 decoded_range=Enum(["B", "P", "R", "S"]), 1998 converter=StringAscii(), 1999 default="B", 2000 ) 2001 ) 2002 self._append( 2003 Field( 2004 "NBPR", 2005 "Number of Blocks Per Row", 2006 4, 2007 charset=BCSN_PI, 2008 decoded_range=MinMax(1, None), 2009 converter=Integer(), 2010 default=1, 2011 ) 2012 ) 2013 self._append( 2014 Field( 2015 "NBPC", 2016 "Number of Blocks Per Column", 2017 4, 2018 charset=BCSN_PI, 2019 decoded_range=MinMax(1, None), 2020 converter=Integer(), 2021 default=1, 2022 ) 2023 ) 2024 self._append( 2025 Field( 2026 "NPPBH", 2027 "Number of Pixels Per Block Horizontal", 2028 4, 2029 charset=BCSN_PI, 2030 decoded_range=MinMax(0, 8192), 2031 converter=Integer(), 2032 default=0, 2033 ) 2034 ) 2035 self._append( 2036 Field( 2037 "NPPBV", 2038 "Number of Pixels Per Block Vertical", 2039 4, 2040 charset=BCSN_PI, 2041 decoded_range=MinMax(0, 8192), 2042 converter=Integer(), 2043 default=0, 2044 ) 2045 ) 2046 self._append( 2047 Field( 2048 "NBPP", 2049 "Number of Bits Per Pixel Per Band", 2050 2, 2051 charset=BCSN_PI, 2052 decoded_range=MinMax(1, 96), 2053 converter=Integer(), 2054 default=1, 2055 ) 2056 ) 2057 self._append( 2058 Field( 2059 "IDLVL", 2060 "Image Display Level", 2061 3, 2062 charset=BCSN_PI, 2063 decoded_range=MinMax(1, None), 2064 converter=Integer(), 2065 default=1, 2066 ) 2067 ) 2068 self._append( 2069 Field( 2070 "IALVL", 2071 "Attachment Level", 2072 3, 2073 charset=BCSN_PI, 2074 decoded_range=MinMax(0, 998), 2075 converter=Integer(), 2076 default=0, 2077 ) 2078 ) 2079 self._append( 2080 Field( 2081 "ILOC", 2082 "Image Location", 2083 10, 2084 charset=BCSN, 2085 converter=IntPair(), 2086 default=(0, 0), 2087 ) 2088 ) 2089 self._append( 2090 Field( 2091 "IMAG", 2092 "Image Magnification", 2093 4, 2094 charset=BCSA, 2095 decoded_range=Regex(r"(\d+\.?\d*)|(\d*\.?\d+)|(\/\d+)"), 2096 converter=StringAscii(), 2097 default="1.0 ", 2098 ) 2099 ) 2100 self._append( 2101 Field( 2102 "UDIDL", 2103 "User Defined Image Data Length", 2104 5, 2105 charset=BCSN_PI, 2106 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2107 converter=Integer(), 2108 default=0, 2109 setter_callback=self._udidl_handler, 2110 ) 2111 ) 2112 self._append( 2113 Field( 2114 "IXSHDL", 2115 "Image Extended Subheader Data Length", 2116 5, 2117 charset=BCSN_PI, 2118 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2119 converter=Integer(), 2120 default=0, 2121 setter_callback=self._ixshdl_handler, 2122 ) 2123 ) 2124 2125 def _icords_handler(self, field: Field) -> None: 2126 self._remove_all("IGEOLO") 2127 if field.value: 2128 self._insert_after( 2129 field, 2130 Field( 2131 "IGEOLO", 2132 "Image Geographic Location", 2133 60, 2134 charset=BCSA, 2135 converter=StringAscii(), 2136 default="", 2137 ), 2138 ) 2139 2140 def _nicom_handler(self, field: Field) -> None: 2141 self._remove_all("ICOM\\d+") 2142 after = self["NICOM"] 2143 for idx in range(1, field.value + 1): 2144 after = self._insert_after( 2145 after, 2146 Field( 2147 f"ICOM{idx}", 2148 "Image Comment {n}", 2149 80, 2150 charset=ECSA, 2151 converter=StringISO8859_1(), 2152 default="", 2153 ), 2154 ) 2155 2156 def _ic_handler(self, field: Field) -> None: 2157 self._remove_all("COMRAT") 2158 if field.value not in ("NC", "NM"): 2159 self._insert_after( 2160 self["IC"], 2161 Field( 2162 "COMRAT", 2163 "Compression Rate Code", 2164 4, 2165 charset=BCSA, 2166 converter=StringAscii(), 2167 default="", 2168 ), 2169 ) 2170 2171 def _nbands_handler(self, field: Field) -> None: 2172 self._remove_all("XBANDS") 2173 if field.value == 0: 2174 self._insert_after( 2175 self["NBANDS"], 2176 Field( 2177 "XBANDS", 2178 "Number of Multispectral Bands", 2179 5, 2180 charset=BCSN_PI, 2181 decoded_range=MinMax(10, None), 2182 converter=Integer(), 2183 default=10, 2184 setter_callback=self._xbands_handler, 2185 ), 2186 ) 2187 self._set_num_band_groups(field.value) 2188 2189 def _xbands_handler(self, field: Field) -> None: 2190 self._set_num_band_groups(field.value) 2191 2192 def _set_num_band_groups(self, count: int) -> None: 2193 self._remove_all("IREPBAND\\d+") 2194 self._remove_all("ISUBCAT\\d+") 2195 self._remove_all("IFC\\d+") 2196 self._remove_all("IMFLT\\d+") 2197 self._remove_all("NLUTS\\d+") 2198 self._remove_all("NELUT\\d+") 2199 self._remove_all("LUTD\\d+") 2200 2201 after = self.get("XBANDS", self["NBANDS"]) 2202 for idx in range(1, count + 1): 2203 after = self._insert_after( 2204 after, 2205 Field( 2206 f"IREPBAND{idx:05d}", 2207 "nth Band Representation", 2208 2, 2209 charset=BCSA, 2210 converter=StringAscii(), 2211 default=None, 2212 nullable=True, 2213 ), 2214 ) 2215 after = self._insert_after( 2216 after, 2217 Field( 2218 f"ISUBCAT{idx:05d}", 2219 "nth Band Subcategory", 2220 6, 2221 charset=BCSA, 2222 converter=StringAscii(), 2223 default=None, 2224 nullable=True, 2225 ), 2226 ) 2227 after = self._insert_after( 2228 after, 2229 Field( 2230 f"IFC{idx:05d}", 2231 "nth Band Image Filter Condition", 2232 1, 2233 charset=BCSA, 2234 converter=StringAscii(), 2235 default="N", 2236 ), 2237 ) 2238 after = self._insert_after( 2239 after, 2240 Field( 2241 f"IMFLT{idx:05d}", 2242 "nth Band Standard Image Filter Code", 2243 3, 2244 charset=BCSA, 2245 converter=StringAscii(), 2246 default=None, 2247 nullable=True, 2248 ), 2249 ) 2250 after = self._insert_after( 2251 after, 2252 Field( 2253 f"NLUTS{idx:05d}", 2254 "Number of LUTS for the nth Image Band", 2255 1, 2256 charset=BCSN_PI, 2257 decoded_range=MinMax(0, 4), 2258 converter=Integer(), 2259 default=0, 2260 setter_callback=self._nluts_handler, 2261 ), 2262 ) 2263 2264 def _udidl_handler(self, field: Field) -> None: 2265 self._remove_all("UDOFL") 2266 self._remove_all("UDID") 2267 if field.value > 0: 2268 after = self._insert_after( 2269 field, 2270 Field( 2271 "UDOFL", 2272 "User Defined Overflow", 2273 3, 2274 charset=BCSN_PI, 2275 converter=Integer(), 2276 default=0, 2277 ), 2278 ) 2279 if field.value > 3: 2280 after = self._insert_after(after, TreSequence("UDID", field.value - 3)) 2281 2282 def _ixshdl_handler(self, field: Field) -> None: 2283 self._remove_all("IXSOFL") 2284 self._remove_all("IXSHD") 2285 if field.value > 0: 2286 after = self._insert_after( 2287 field, 2288 Field( 2289 "IXSOFL", 2290 "Image Extended Subheader Overflow", 2291 3, 2292 charset=BCSN_PI, 2293 converter=Integer(), 2294 default=0, 2295 ), 2296 ) 2297 if field.value > 3: 2298 after = self._insert_after(after, TreSequence("IXSHD", field.value - 3)) 2299 2300 def _nluts_handler(self, field: Field) -> None: 2301 idx = int(field.name.removeprefix("NLUTS")) 2302 self._remove_all(f"NELUT{idx:05d}\\d+") 2303 self._remove_all(f"LUTD{idx:05d}\\d+") 2304 if field.value > 0: 2305 after = self._insert_after( 2306 field, 2307 Field( 2308 f"NELUT{idx:05d}", 2309 "Number of LUT Entries for the nth Image Band", 2310 5, 2311 charset=BCSN_PI, 2312 decoded_range=MinMax(1, 65536), 2313 converter=Integer(), 2314 default=1, 2315 setter_callback=self._nelut_handler, 2316 ), 2317 ) 2318 for lutidx in range(1, field.value + 1): 2319 after = self._insert_after( 2320 after, 2321 Field( 2322 f"LUTD{idx:05d}{lutidx}", 2323 "nth Image Band, mth LUT", 2324 1, 2325 converter=Bytes(), 2326 default=b"\x00", 2327 ), 2328 ) 2329 2330 def _nelut_handler(self, field: Field) -> None: 2331 idx = int(field.name.removeprefix("NELUT")) 2332 for lutd in self.find_all(f"LUTD{idx:05d}\\d+"): 2333 assert isinstance(lutd, Field) 2334 lutd.size = field.value 2335 2336 def finalize(self) -> None: 2337 super().finalize() 2338 _update_tre_lengths(self, "UDIDL", "UDOFL", "UDID") 2339 _update_tre_lengths(self, "IXSHDL", "IXSOFL", "IXSHD")
Image Subheader fields
Parameters
- name (str): Name to give this component
Notes
See JBP-2025.1 Table 5.13-1
1734 def __init__(self, name: str): 1735 super().__init__(name) 1736 1737 self._append( 1738 Field( 1739 "IM", 1740 "File Part Type", 1741 2, 1742 charset=BCSA, 1743 decoded_range=Constant("IM"), 1744 converter=StringAscii(), 1745 default="IM", 1746 ) 1747 ) 1748 self._append( 1749 Field( 1750 "IID1", 1751 "Image Identifier 1", 1752 10, 1753 charset=BCSA, 1754 converter=StringAscii(), 1755 default="", 1756 ) 1757 ) 1758 self._append( 1759 Field( 1760 "IDATIM", 1761 "Image Date and Time", 1762 14, 1763 charset=BCSN, 1764 decoded_range=DATETIME_REGEX, 1765 converter=StringAscii(), 1766 default="-" * 14, 1767 ) 1768 ) 1769 self._append( 1770 Field( 1771 "TGTID", 1772 "Target Identifier", 1773 17, 1774 charset=BCSA, 1775 converter=StringISO8859_1(), 1776 default=None, 1777 nullable=True, 1778 ) 1779 ) 1780 self._append( 1781 Field( 1782 "IID2", 1783 "Image Identifier 2", 1784 80, 1785 charset=ECSA, 1786 converter=StringISO8859_1(), 1787 default=None, 1788 nullable=True, 1789 ) 1790 ) 1791 self._extend(SecurityFields("Security Fields Image", "I").values()) 1792 self._append( 1793 Field( 1794 "ENCRYP", 1795 "Encryption", 1796 1, 1797 charset=BCSN_PI, 1798 decoded_range=Constant(0), 1799 converter=Integer(), 1800 default=0, 1801 ) 1802 ) 1803 self._append( 1804 Field( 1805 "ISORCE", 1806 "Image Source", 1807 42, 1808 charset=ECSA, 1809 converter=StringISO8859_1(), 1810 default=None, 1811 nullable=True, 1812 ) 1813 ) 1814 self._append( 1815 Field( 1816 "NROWS", 1817 "Number of Significant Rows in Image", 1818 8, 1819 charset=BCSN_PI, 1820 decoded_range=MinMax(1, None), 1821 converter=Integer(), 1822 default=1, 1823 ) 1824 ) 1825 self._append( 1826 Field( 1827 "NCOLS", 1828 "Number of Significant Columns in Image", 1829 8, 1830 charset=BCSN_PI, 1831 decoded_range=MinMax(1, None), 1832 converter=Integer(), 1833 default=1, 1834 ) 1835 ) 1836 self._append( 1837 Field( 1838 "PVTYPE", 1839 "Pixel Value Type", 1840 3, 1841 charset=BCSA, 1842 decoded_range=Enum(["INT", "B", "SI", "R", "C"]), 1843 converter=StringAscii(), 1844 default="INT", 1845 ) 1846 ) 1847 self._append( 1848 Field( 1849 "IREP", 1850 "Image Representation", 1851 8, 1852 charset=BCSA, 1853 decoded_range=Enum( 1854 [ 1855 "MONO", 1856 "RGB", 1857 "RGB/LUT", 1858 "MULTI", 1859 "NODISPLY", 1860 "NVECTOR", 1861 "POLAR", 1862 "VPH", 1863 "YCbCr601", 1864 ] 1865 ), 1866 converter=StringAscii(), 1867 default="MONO", 1868 ) 1869 ) 1870 self._append( 1871 Field( 1872 "ICAT", 1873 "Image Category", 1874 8, 1875 charset=BCSA, 1876 converter=StringAscii(), 1877 default="VIS", 1878 ) 1879 ) 1880 self._append( 1881 Field( 1882 "ABPP", 1883 "Actual Bits-Per-Pixel Per Band", 1884 2, 1885 charset=BCSN_PI, 1886 decoded_range=MinMax(1, 96), 1887 converter=Integer(), 1888 default=1, 1889 ) 1890 ) 1891 self._append( 1892 Field( 1893 "PJUST", 1894 "Pixel Justification", 1895 1, 1896 charset=BCSA, 1897 decoded_range=Enum(["L", "R"]), 1898 converter=StringAscii(), 1899 default="R", 1900 ) 1901 ) 1902 self._append( 1903 Field( 1904 "ICORDS", 1905 "Image Coordinate Representation", 1906 1, 1907 charset=BCSA, 1908 decoded_range=Enum(["U", "G", "N", "S", "D"]), 1909 converter=StringAscii(), 1910 default=None, 1911 nullable=True, 1912 setter_callback=self._icords_handler, 1913 ) 1914 ) 1915 # IGEOLO 1916 self._append( 1917 Field( 1918 "NICOM", 1919 "Number of Image Comments", 1920 1, 1921 charset=BCSN_PI, 1922 converter=Integer(), 1923 default=0, 1924 setter_callback=self._nicom_handler, 1925 ) 1926 ) 1927 # ICOMn 1928 self._append( 1929 Field( 1930 "IC", 1931 "Image Compression", 1932 2, 1933 charset=BCSA, 1934 decoded_range=Enum( 1935 [ 1936 "NC", 1937 "NM", 1938 "C1", 1939 "C3", 1940 "C4", 1941 "C5", 1942 "C6", 1943 "C7", 1944 "C8", 1945 "I1", 1946 "M1", 1947 "M3", 1948 "M4", 1949 "M5", 1950 "M6", 1951 "M7", 1952 "M8", 1953 ] 1954 ), 1955 converter=StringAscii(), 1956 setter_callback=self._ic_handler, 1957 default="NC", 1958 ) 1959 ) 1960 # COMRAT 1961 self._append( 1962 Field( 1963 "NBANDS", 1964 "Number of Bands", 1965 1, 1966 charset=BCSN_PI, 1967 converter=Integer(), 1968 setter_callback=self._nbands_handler, 1969 default=1, 1970 ) 1971 ) 1972 # XBANDS 1973 # IREPBANDn 1974 # ISUBCATn 1975 # IFCn 1976 # IMFLTn 1977 # NLUTSn 1978 # NELUTn 1979 # LUTDn 1980 self._append( 1981 Field( 1982 "ISYNC", 1983 "Image Sync Code", 1984 1, 1985 charset=BCSN_PI, 1986 decoded_range=Constant(0), 1987 converter=Integer(), 1988 default=0, 1989 ) 1990 ) 1991 self._append( 1992 Field( 1993 "IMODE", 1994 "Image Mode", 1995 1, 1996 charset=BCSA, 1997 decoded_range=Enum(["B", "P", "R", "S"]), 1998 converter=StringAscii(), 1999 default="B", 2000 ) 2001 ) 2002 self._append( 2003 Field( 2004 "NBPR", 2005 "Number of Blocks Per Row", 2006 4, 2007 charset=BCSN_PI, 2008 decoded_range=MinMax(1, None), 2009 converter=Integer(), 2010 default=1, 2011 ) 2012 ) 2013 self._append( 2014 Field( 2015 "NBPC", 2016 "Number of Blocks Per Column", 2017 4, 2018 charset=BCSN_PI, 2019 decoded_range=MinMax(1, None), 2020 converter=Integer(), 2021 default=1, 2022 ) 2023 ) 2024 self._append( 2025 Field( 2026 "NPPBH", 2027 "Number of Pixels Per Block Horizontal", 2028 4, 2029 charset=BCSN_PI, 2030 decoded_range=MinMax(0, 8192), 2031 converter=Integer(), 2032 default=0, 2033 ) 2034 ) 2035 self._append( 2036 Field( 2037 "NPPBV", 2038 "Number of Pixels Per Block Vertical", 2039 4, 2040 charset=BCSN_PI, 2041 decoded_range=MinMax(0, 8192), 2042 converter=Integer(), 2043 default=0, 2044 ) 2045 ) 2046 self._append( 2047 Field( 2048 "NBPP", 2049 "Number of Bits Per Pixel Per Band", 2050 2, 2051 charset=BCSN_PI, 2052 decoded_range=MinMax(1, 96), 2053 converter=Integer(), 2054 default=1, 2055 ) 2056 ) 2057 self._append( 2058 Field( 2059 "IDLVL", 2060 "Image Display Level", 2061 3, 2062 charset=BCSN_PI, 2063 decoded_range=MinMax(1, None), 2064 converter=Integer(), 2065 default=1, 2066 ) 2067 ) 2068 self._append( 2069 Field( 2070 "IALVL", 2071 "Attachment Level", 2072 3, 2073 charset=BCSN_PI, 2074 decoded_range=MinMax(0, 998), 2075 converter=Integer(), 2076 default=0, 2077 ) 2078 ) 2079 self._append( 2080 Field( 2081 "ILOC", 2082 "Image Location", 2083 10, 2084 charset=BCSN, 2085 converter=IntPair(), 2086 default=(0, 0), 2087 ) 2088 ) 2089 self._append( 2090 Field( 2091 "IMAG", 2092 "Image Magnification", 2093 4, 2094 charset=BCSA, 2095 decoded_range=Regex(r"(\d+\.?\d*)|(\d*\.?\d+)|(\/\d+)"), 2096 converter=StringAscii(), 2097 default="1.0 ", 2098 ) 2099 ) 2100 self._append( 2101 Field( 2102 "UDIDL", 2103 "User Defined Image Data Length", 2104 5, 2105 charset=BCSN_PI, 2106 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2107 converter=Integer(), 2108 default=0, 2109 setter_callback=self._udidl_handler, 2110 ) 2111 ) 2112 self._append( 2113 Field( 2114 "IXSHDL", 2115 "Image Extended Subheader Data Length", 2116 5, 2117 charset=BCSN_PI, 2118 decoded_range=AnyOf(Constant(0), MinMax(3, None)), 2119 converter=Integer(), 2120 default=0, 2121 setter_callback=self._ixshdl_handler, 2122 ) 2123 )
2336 def finalize(self) -> None: 2337 super().finalize() 2338 _update_tre_lengths(self, "UDIDL", "UDOFL", "UDID") 2339 _update_tre_lengths(self, "IXSHDL", "IXSOFL", "IXSHD")
Perform any necessary final updates
Inherited Members
2342class ImageSegment(Group): 2343 def __init__(self, name: str, data_size: int = 1): 2344 super().__init__(name) 2345 self._append(ImageSubheader("subheader")) 2346 self._append(BinaryPlaceholder("Data", data_size)) 2347 2348 def print(self, *, file=None) -> None: 2349 print(f"# ImageSegment {self.name}", file=file) 2350 super().print(file=file)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
2348 def print(self, *, file=None) -> None: 2349 print(f"# ImageSegment {self.name}", file=file) 2350 super().print(file=file)
Print information about the component to stdout
Inherited Members
2353class GraphicSubheader(Group): 2354 """ 2355 Graphic Subheader fields 2356 2357 Parameters 2358 ---------- 2359 name : str 2360 Name to give this component 2361 2362 Notes 2363 ----- 2364 See JBP-2025.1 Table 5.15-1 2365 """ 2366 2367 def __init__(self, name: str): 2368 super().__init__(name) 2369 2370 self._append( 2371 Field( 2372 "SY", 2373 "File Part Type", 2374 2, 2375 charset=BCSA, 2376 decoded_range=Constant("SY"), 2377 converter=StringAscii(), 2378 default="SY", 2379 ) 2380 ) 2381 self._append( 2382 Field( 2383 "SID", 2384 "Graphic Identifier", 2385 10, 2386 charset=BCSA, 2387 converter=StringAscii(), 2388 default="", 2389 ) 2390 ) 2391 self._append( 2392 Field( 2393 "SNAME", 2394 "Graphic Name", 2395 20, 2396 charset=ECSA, 2397 converter=StringISO8859_1(), 2398 default=None, 2399 nullable=True, 2400 ) 2401 ) 2402 self._extend(SecurityFields("Security Fields Graphic", "S").values()) 2403 self._append( 2404 Field( 2405 "ENCRYP", 2406 "Encryption", 2407 1, 2408 charset=BCSN_PI, 2409 decoded_range=Constant(0), 2410 converter=Integer(), 2411 default=0, 2412 ) 2413 ) 2414 self._append( 2415 Field( 2416 "SFMT", 2417 "Graphic Type", 2418 1, 2419 charset=BCSA, 2420 decoded_range=Constant("C"), 2421 converter=StringAscii(), 2422 default="C", 2423 ) 2424 ) 2425 self._append( 2426 Field( 2427 "SSTRUCT", 2428 "Reserved for Future Use", 2429 13, 2430 charset=BCSN_PI, 2431 decoded_range=Constant(0), 2432 converter=Integer(), 2433 default=0, 2434 ) 2435 ) 2436 self._append( 2437 Field( 2438 "SDLVL", 2439 "Graphic Display Level", 2440 3, 2441 charset=BCSN_PI, 2442 decoded_range=MinMax(1, None), 2443 converter=Integer(), 2444 default=1, 2445 ) 2446 ) 2447 self._append( 2448 Field( 2449 "SALVL", 2450 "Graphic Attachment Level", 2451 3, 2452 charset=BCSN_PI, 2453 decoded_range=MinMax(0, 998), 2454 converter=Integer(), 2455 default=0, 2456 ) 2457 ) 2458 self._append( 2459 Field( 2460 "SLOC", 2461 "Graphic Location", 2462 10, 2463 charset=BCSN, 2464 converter=IntPair(), 2465 default=(0, 0), 2466 ) 2467 ) 2468 self._append( 2469 Field( 2470 "SBND1", 2471 "First Graphic Bound Location", 2472 10, 2473 charset=BCSN, 2474 converter=IntPair(), 2475 default=(0, 0), 2476 ) 2477 ) 2478 self._append( 2479 Field( 2480 "SCOLOR", 2481 "Graphic Color", 2482 1, 2483 charset=BCSA, 2484 decoded_range=Enum(["C", "M"]), 2485 converter=StringAscii(), 2486 default="", # should this have a default? 2487 ) 2488 ) 2489 self._append( 2490 Field( 2491 "SBND2", 2492 "Second Graphic Bound Location", 2493 10, 2494 charset=BCSN, 2495 converter=IntPair(), 2496 default=(0, 0), 2497 ) 2498 ) 2499 self._append( 2500 Field( 2501 "SRES2", 2502 "Reserved for Future Use", 2503 2, 2504 charset=BCSN_PI, 2505 decoded_range=Constant(0), 2506 converter=Integer(), 2507 default=0, 2508 ) 2509 ) 2510 self._append( 2511 Field( 2512 "SXSHDL", 2513 "Graphic Extended Subheader Data Length", 2514 5, 2515 charset=BCSN_PI, 2516 decoded_range=AnyOf( 2517 Constant(0), 2518 MinMax(3, 9741), 2519 ), 2520 converter=Integer(), 2521 default=0, 2522 setter_callback=self._sxshdl_handler, 2523 ) 2524 ) 2525 2526 def _sxshdl_handler(self, field: Field) -> None: 2527 self._remove_all("SXSOFL") 2528 self._remove_all("SXSHD") 2529 if field.value > 0: 2530 after = self._insert_after( 2531 field, 2532 Field( 2533 "SXSOFL", 2534 "Graphic Extended Subheader Overflow", 2535 3, 2536 charset=BCSN_PI, 2537 converter=Integer(), 2538 default=0, 2539 ), 2540 ) 2541 if field.value > 3: 2542 after = self._insert_after(after, TreSequence("SXSHD", field.value - 3)) 2543 2544 def finalize(self) -> None: 2545 super().finalize() 2546 _update_tre_lengths(self, "SXSHDL", "SXSOFL", "SXSHD")
Graphic Subheader fields
Parameters
- name (str): Name to give this component
Notes
See JBP-2025.1 Table 5.15-1
2367 def __init__(self, name: str): 2368 super().__init__(name) 2369 2370 self._append( 2371 Field( 2372 "SY", 2373 "File Part Type", 2374 2, 2375 charset=BCSA, 2376 decoded_range=Constant("SY"), 2377 converter=StringAscii(), 2378 default="SY", 2379 ) 2380 ) 2381 self._append( 2382 Field( 2383 "SID", 2384 "Graphic Identifier", 2385 10, 2386 charset=BCSA, 2387 converter=StringAscii(), 2388 default="", 2389 ) 2390 ) 2391 self._append( 2392 Field( 2393 "SNAME", 2394 "Graphic Name", 2395 20, 2396 charset=ECSA, 2397 converter=StringISO8859_1(), 2398 default=None, 2399 nullable=True, 2400 ) 2401 ) 2402 self._extend(SecurityFields("Security Fields Graphic", "S").values()) 2403 self._append( 2404 Field( 2405 "ENCRYP", 2406 "Encryption", 2407 1, 2408 charset=BCSN_PI, 2409 decoded_range=Constant(0), 2410 converter=Integer(), 2411 default=0, 2412 ) 2413 ) 2414 self._append( 2415 Field( 2416 "SFMT", 2417 "Graphic Type", 2418 1, 2419 charset=BCSA, 2420 decoded_range=Constant("C"), 2421 converter=StringAscii(), 2422 default="C", 2423 ) 2424 ) 2425 self._append( 2426 Field( 2427 "SSTRUCT", 2428 "Reserved for Future Use", 2429 13, 2430 charset=BCSN_PI, 2431 decoded_range=Constant(0), 2432 converter=Integer(), 2433 default=0, 2434 ) 2435 ) 2436 self._append( 2437 Field( 2438 "SDLVL", 2439 "Graphic Display Level", 2440 3, 2441 charset=BCSN_PI, 2442 decoded_range=MinMax(1, None), 2443 converter=Integer(), 2444 default=1, 2445 ) 2446 ) 2447 self._append( 2448 Field( 2449 "SALVL", 2450 "Graphic Attachment Level", 2451 3, 2452 charset=BCSN_PI, 2453 decoded_range=MinMax(0, 998), 2454 converter=Integer(), 2455 default=0, 2456 ) 2457 ) 2458 self._append( 2459 Field( 2460 "SLOC", 2461 "Graphic Location", 2462 10, 2463 charset=BCSN, 2464 converter=IntPair(), 2465 default=(0, 0), 2466 ) 2467 ) 2468 self._append( 2469 Field( 2470 "SBND1", 2471 "First Graphic Bound Location", 2472 10, 2473 charset=BCSN, 2474 converter=IntPair(), 2475 default=(0, 0), 2476 ) 2477 ) 2478 self._append( 2479 Field( 2480 "SCOLOR", 2481 "Graphic Color", 2482 1, 2483 charset=BCSA, 2484 decoded_range=Enum(["C", "M"]), 2485 converter=StringAscii(), 2486 default="", # should this have a default? 2487 ) 2488 ) 2489 self._append( 2490 Field( 2491 "SBND2", 2492 "Second Graphic Bound Location", 2493 10, 2494 charset=BCSN, 2495 converter=IntPair(), 2496 default=(0, 0), 2497 ) 2498 ) 2499 self._append( 2500 Field( 2501 "SRES2", 2502 "Reserved for Future Use", 2503 2, 2504 charset=BCSN_PI, 2505 decoded_range=Constant(0), 2506 converter=Integer(), 2507 default=0, 2508 ) 2509 ) 2510 self._append( 2511 Field( 2512 "SXSHDL", 2513 "Graphic Extended Subheader Data Length", 2514 5, 2515 charset=BCSN_PI, 2516 decoded_range=AnyOf( 2517 Constant(0), 2518 MinMax(3, 9741), 2519 ), 2520 converter=Integer(), 2521 default=0, 2522 setter_callback=self._sxshdl_handler, 2523 ) 2524 )
2544 def finalize(self) -> None: 2545 super().finalize() 2546 _update_tre_lengths(self, "SXSHDL", "SXSOFL", "SXSHD")
Perform any necessary final updates
Inherited Members
2549class GraphicSegment(Group): 2550 def __init__(self, name: str, data_size: int = 1): 2551 super().__init__(name) 2552 self._append(GraphicSubheader("subheader")) 2553 self._append(BinaryPlaceholder("Data", data_size)) 2554 2555 def print(self, *, file=None) -> None: 2556 print(f"# GraphicSegment {self.name}", file=file) 2557 super().print(file=file)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
2555 def print(self, *, file=None) -> None: 2556 print(f"# GraphicSegment {self.name}", file=file) 2557 super().print(file=file)
Print information about the component to stdout
Inherited Members
2560class TextSubheader(Group): 2561 """ 2562 Text Subheader fields 2563 2564 Parameters 2565 ---------- 2566 name : str 2567 Name to give this component 2568 2569 Notes 2570 ----- 2571 See JBP-2025.1 Table 5.17-1 2572 """ 2573 2574 def __init__(self, name: str): 2575 super().__init__(name) 2576 2577 self._append( 2578 Field( 2579 "TE", 2580 "File Part Type", 2581 2, 2582 charset=BCSA, 2583 decoded_range=Constant("TE"), 2584 converter=StringAscii(), 2585 default="TE", 2586 ) 2587 ) 2588 self._append( 2589 Field( 2590 "TEXTID", 2591 "Text Identifier", 2592 7, 2593 charset=BCSA, 2594 converter=StringAscii(), 2595 default="", 2596 ) 2597 ) 2598 self._append( 2599 Field( 2600 "TXTALVL", 2601 "Text Attachment Level", 2602 3, 2603 charset=BCSN_PI, 2604 decoded_range=MinMax(0, 998), 2605 converter=Integer(), 2606 default=0, 2607 ) 2608 ) 2609 self._append( 2610 Field( 2611 "TXTDT", 2612 "Text Date and Time", 2613 14, 2614 charset=BCSN, 2615 decoded_range=DATETIME_REGEX, 2616 converter=StringAscii(), 2617 default="-" * 14, 2618 ) 2619 ) 2620 self._append( 2621 Field( 2622 "TXTITL", 2623 "Text Title", 2624 80, 2625 charset=ECSA, 2626 converter=StringISO8859_1(), 2627 default=None, 2628 nullable=True, 2629 ) 2630 ) 2631 self._extend(SecurityFields("Security Fields Text", "T").values()) 2632 self._append( 2633 Field( 2634 "ENCRYP", 2635 "Encryption", 2636 1, 2637 charset=BCSN_PI, 2638 decoded_range=Constant(0), 2639 converter=Integer(), 2640 default=0, 2641 ) 2642 ) 2643 self._append( 2644 Field( 2645 "TXTFMT", 2646 "Text Format", 2647 3, 2648 charset=BCSA, 2649 decoded_range=Enum(["MTF", "STA", "UT1", "U8S"]), 2650 converter=StringAscii(), 2651 default="", 2652 ) 2653 ) 2654 self._append( 2655 Field( 2656 "TXSHDL", 2657 "Text Extended Subheader Data Length", 2658 5, 2659 charset=BCSN_PI, 2660 decoded_range=AnyOf( 2661 Constant(0), 2662 MinMax(3, 9717), 2663 ), 2664 converter=Integer(), 2665 default=0, 2666 setter_callback=self._txshdl_handler, 2667 ) 2668 ) 2669 2670 def _txshdl_handler(self, field: Field) -> None: 2671 self._remove_all("TXSOFL") 2672 self._remove_all("TXSHD") 2673 if field.value > 0: 2674 after = self._insert_after( 2675 field, 2676 Field( 2677 "TXSOFL", 2678 "Text Extended Subheader Overflow", 2679 3, 2680 charset=BCSN_PI, 2681 converter=Integer(), 2682 default=0, 2683 ), 2684 ) 2685 if field.value > 3: 2686 after = self._insert_after(after, TreSequence("TXSHD", field.value - 3)) 2687 2688 def finalize(self) -> None: 2689 super().finalize() 2690 _update_tre_lengths(self, "TXSHDL", "TXSOFL", "TXSHD")
Text Subheader fields
Parameters
- name (str): Name to give this component
Notes
See JBP-2025.1 Table 5.17-1
2574 def __init__(self, name: str): 2575 super().__init__(name) 2576 2577 self._append( 2578 Field( 2579 "TE", 2580 "File Part Type", 2581 2, 2582 charset=BCSA, 2583 decoded_range=Constant("TE"), 2584 converter=StringAscii(), 2585 default="TE", 2586 ) 2587 ) 2588 self._append( 2589 Field( 2590 "TEXTID", 2591 "Text Identifier", 2592 7, 2593 charset=BCSA, 2594 converter=StringAscii(), 2595 default="", 2596 ) 2597 ) 2598 self._append( 2599 Field( 2600 "TXTALVL", 2601 "Text Attachment Level", 2602 3, 2603 charset=BCSN_PI, 2604 decoded_range=MinMax(0, 998), 2605 converter=Integer(), 2606 default=0, 2607 ) 2608 ) 2609 self._append( 2610 Field( 2611 "TXTDT", 2612 "Text Date and Time", 2613 14, 2614 charset=BCSN, 2615 decoded_range=DATETIME_REGEX, 2616 converter=StringAscii(), 2617 default="-" * 14, 2618 ) 2619 ) 2620 self._append( 2621 Field( 2622 "TXTITL", 2623 "Text Title", 2624 80, 2625 charset=ECSA, 2626 converter=StringISO8859_1(), 2627 default=None, 2628 nullable=True, 2629 ) 2630 ) 2631 self._extend(SecurityFields("Security Fields Text", "T").values()) 2632 self._append( 2633 Field( 2634 "ENCRYP", 2635 "Encryption", 2636 1, 2637 charset=BCSN_PI, 2638 decoded_range=Constant(0), 2639 converter=Integer(), 2640 default=0, 2641 ) 2642 ) 2643 self._append( 2644 Field( 2645 "TXTFMT", 2646 "Text Format", 2647 3, 2648 charset=BCSA, 2649 decoded_range=Enum(["MTF", "STA", "UT1", "U8S"]), 2650 converter=StringAscii(), 2651 default="", 2652 ) 2653 ) 2654 self._append( 2655 Field( 2656 "TXSHDL", 2657 "Text Extended Subheader Data Length", 2658 5, 2659 charset=BCSN_PI, 2660 decoded_range=AnyOf( 2661 Constant(0), 2662 MinMax(3, 9717), 2663 ), 2664 converter=Integer(), 2665 default=0, 2666 setter_callback=self._txshdl_handler, 2667 ) 2668 )
2688 def finalize(self) -> None: 2689 super().finalize() 2690 _update_tre_lengths(self, "TXSHDL", "TXSOFL", "TXSHD")
Perform any necessary final updates
Inherited Members
2693class TextSegment(Group): 2694 def __init__(self, name: str, data_size: int = 1): 2695 super().__init__(name) 2696 self._append(TextSubheader("subheader")) 2697 self._append(BinaryPlaceholder("Data", data_size)) 2698 2699 def print(self, *, file=None) -> None: 2700 print(f"# TextSegment {self.name}", file=file) 2701 super().print(file=file)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
2699 def print(self, *, file=None) -> None: 2700 print(f"# TextSegment {self.name}", file=file) 2701 super().print(file=file)
Print information about the component to stdout
Inherited Members
2704class ReservedExtensionSegment(Group): 2705 def __init__(self, name: str, subheader_size: int = LRESH_MIN, data_size: int = 1): 2706 super().__init__(name) 2707 self._append( 2708 Field( 2709 "subheader", 2710 "Placeholder", 2711 subheader_size, 2712 converter=Bytes(), 2713 default=b"\x00" * subheader_size, 2714 ) 2715 ) 2716 self._append(BinaryPlaceholder("RESDATA", data_size)) 2717 2718 def print(self, *, file=None) -> None: 2719 print(f"# ReservedExtensionSegment {self.name}", file=file) 2720 super().print(file=file)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
2705 def __init__(self, name: str, subheader_size: int = LRESH_MIN, data_size: int = 1): 2706 super().__init__(name) 2707 self._append( 2708 Field( 2709 "subheader", 2710 "Placeholder", 2711 subheader_size, 2712 converter=Bytes(), 2713 default=b"\x00" * subheader_size, 2714 ) 2715 ) 2716 self._append(BinaryPlaceholder("RESDATA", data_size))
2718 def print(self, *, file=None) -> None: 2719 print(f"# ReservedExtensionSegment {self.name}", file=file) 2720 super().print(file=file)
Print information about the component to stdout
Inherited Members
2723class DataExtensionSubheader(Group): 2724 """ 2725 Data Extension Segment (DES) Subheader with unrecognized user-defined subheader fields 2726 2727 Parameters 2728 ---------- 2729 name : str 2730 Name to give this component 2731 desid_constraint : RangeCheck or None, optional 2732 Decoded range check for 'DESID' 2733 desver_constraint : RangeCheck or None, optional 2734 Decoded range check for 'DESVER' 2735 desshl_constraint : RangeCheck or None, optional 2736 Decoded range check for 'DESSHL' 2737 2738 Notes 2739 ----- 2740 See JBP-2025.1 Table 5.18-1 2741 """ 2742 2743 def __init__( 2744 self, 2745 name: str, 2746 *, 2747 desid_constraint: RangeCheck | None = None, 2748 desver_constraint: RangeCheck | None = None, 2749 desshl_constraint: RangeCheck | None = None, 2750 ): 2751 super().__init__(name) 2752 self._append( 2753 Field( 2754 "DE", 2755 "File Part Type", 2756 2, 2757 charset=BCSA, 2758 decoded_range=Constant("DE"), 2759 converter=StringAscii(), 2760 default="DE", 2761 ) 2762 ) 2763 self._append( 2764 Field( 2765 "DESID", 2766 "Unique DES Type Identifier", 2767 25, 2768 charset=BCSA, 2769 decoded_range=desid_constraint, 2770 converter=StringAscii(), 2771 default="", 2772 ) 2773 ) 2774 self._append( 2775 Field( 2776 "DESVER", 2777 "Version of the Data Definition", 2778 2, 2779 charset=BCSN_PI, 2780 decoded_range=desver_constraint or MinMax(1, None), 2781 converter=Integer(), 2782 default=1, 2783 ) 2784 ) 2785 self._extend(SecurityFields("Security Fields DES", "DE").values()) 2786 # DESOFLW/DESITEM only in TRE_OVERFLOW DES 2787 self._append( 2788 Field( 2789 "DESSHL", 2790 "DES User-defined Subheader Length", 2791 4, 2792 charset=BCSN_PI, 2793 converter=Integer(), 2794 decoded_range=desshl_constraint, 2795 default=0, 2796 setter_callback=self._populate_user_defined_subheader, 2797 ) 2798 ) 2799 # DESSHF handled by DESSHL callback 2800 2801 def _populate_user_defined_subheader(self, desshl_field: Field): 2802 """Populate user-defined subheader fields 2803 2804 Subclasses should override this method with their own definition. 2805 """ 2806 self._remove_all("DESSHF") 2807 if desshl_field.value > 0: 2808 # JBP claims DESSHF C-set is BCS-A, but there are some violations in STDI-0002 so we'll treat as bytes 2809 self._insert_after( 2810 desshl_field, 2811 Field( 2812 "DESSHF", 2813 "DES User-defined Subheader Fields", 2814 desshl_field.value, 2815 converter=Bytes(), 2816 default=b"\x00" * desshl_field.value, 2817 ), 2818 )
Data Extension Segment (DES) Subheader with unrecognized user-defined subheader fields
Parameters
- name (str): Name to give this component
- desid_constraint (RangeCheck or None, optional): Decoded range check for 'DESID'
- desver_constraint (RangeCheck or None, optional): Decoded range check for 'DESVER'
- desshl_constraint (RangeCheck or None, optional): Decoded range check for 'DESSHL'
Notes
See JBP-2025.1 Table 5.18-1
2743 def __init__( 2744 self, 2745 name: str, 2746 *, 2747 desid_constraint: RangeCheck | None = None, 2748 desver_constraint: RangeCheck | None = None, 2749 desshl_constraint: RangeCheck | None = None, 2750 ): 2751 super().__init__(name) 2752 self._append( 2753 Field( 2754 "DE", 2755 "File Part Type", 2756 2, 2757 charset=BCSA, 2758 decoded_range=Constant("DE"), 2759 converter=StringAscii(), 2760 default="DE", 2761 ) 2762 ) 2763 self._append( 2764 Field( 2765 "DESID", 2766 "Unique DES Type Identifier", 2767 25, 2768 charset=BCSA, 2769 decoded_range=desid_constraint, 2770 converter=StringAscii(), 2771 default="", 2772 ) 2773 ) 2774 self._append( 2775 Field( 2776 "DESVER", 2777 "Version of the Data Definition", 2778 2, 2779 charset=BCSN_PI, 2780 decoded_range=desver_constraint or MinMax(1, None), 2781 converter=Integer(), 2782 default=1, 2783 ) 2784 ) 2785 self._extend(SecurityFields("Security Fields DES", "DE").values()) 2786 # DESOFLW/DESITEM only in TRE_OVERFLOW DES 2787 self._append( 2788 Field( 2789 "DESSHL", 2790 "DES User-defined Subheader Length", 2791 4, 2792 charset=BCSN_PI, 2793 converter=Integer(), 2794 decoded_range=desshl_constraint, 2795 default=0, 2796 setter_callback=self._populate_user_defined_subheader, 2797 ) 2798 ) 2799 # DESSHF handled by DESSHL callback
Inherited Members
2821class TreOverflowDesSubheader(DataExtensionSubheader): 2822 """Tagged Record Extension Overflow (TRE-OVERFLOW) DES 2823 2824 See JBP-2025.1 Table 5.18.2 2825 """ 2826 2827 def __init__(self, name): 2828 super().__init__( 2829 name, 2830 desid_constraint=Constant("TRE_OVERFLOW"), 2831 desver_constraint=Constant(1), 2832 desshl_constraint=Constant(0), 2833 ) 2834 2835 # For some reason, the TRE_OVERFLOW fields are not in the user-defined subheader area 2836 self._insert_after( 2837 self["DESCTLN"], 2838 Field( 2839 "DESOFLW", 2840 "DES Overflowed Header Type", 2841 6, 2842 charset=BCSA, 2843 decoded_range=Enum(["XHD", "IXSHD", "SXSHD", "TXSHD", "UDHD", "UDID"]), 2844 converter=StringAscii(), 2845 default="", 2846 ), 2847 Field( 2848 "DESITEM", 2849 "DES Data Item Overflowed", 2850 3, 2851 charset=BCSN_PI, 2852 converter=Integer(), 2853 default=0, 2854 ), 2855 ) 2856 2857 def _populate_user_defined_subheader(self, desshl_field): 2858 """TRE-OVERFLOW doesn't have used-defined subheader fields"""
Tagged Record Extension Overflow (TRE-OVERFLOW) DES
See JBP-2025.1 Table 5.18.2
2827 def __init__(self, name): 2828 super().__init__( 2829 name, 2830 desid_constraint=Constant("TRE_OVERFLOW"), 2831 desver_constraint=Constant(1), 2832 desshl_constraint=Constant(0), 2833 ) 2834 2835 # For some reason, the TRE_OVERFLOW fields are not in the user-defined subheader area 2836 self._insert_after( 2837 self["DESCTLN"], 2838 Field( 2839 "DESOFLW", 2840 "DES Overflowed Header Type", 2841 6, 2842 charset=BCSA, 2843 decoded_range=Enum(["XHD", "IXSHD", "SXSHD", "TXSHD", "UDHD", "UDID"]), 2844 converter=StringAscii(), 2845 default="", 2846 ), 2847 Field( 2848 "DESITEM", 2849 "DES Data Item Overflowed", 2850 3, 2851 charset=BCSN_PI, 2852 converter=Integer(), 2853 default=0, 2854 ), 2855 )
Inherited Members
2864def available_des_subheaders() -> DesSubheaderDefs: 2865 """All discovered and available Data Extension Segment (DES) subheaders 2866 2867 Returns 2868 ------- 2869 dict of {(str, int) : callable} 2870 Mapping of (desid, desver) pairs to a function that accepts a string-valued name and 2871 instantiates the appropriate DES subheader 2872 """ 2873 d: DesSubheaderDefs = {} 2874 for plugin in importlib.metadata.entry_points( 2875 group="jbpy.extensions.des_subheader" 2876 ): 2877 try: 2878 assert len(plugin.name) == 27 2879 desid = plugin.name[:25].rstrip() 2880 desver = int(plugin.name[-2:]) 2881 d[(desid, desver)] = plugin.load() 2882 except (AssertionError, ValueError): 2883 logger.warning(f"Skipping {plugin=}; unable to parse") 2884 return d
All discovered and available Data Extension Segment (DES) subheaders
Returns
- dict of {(str, int) (callable}): Mapping of (desid, desver) pairs to a function that accepts a string-valued name and instantiates the appropriate DES subheader
2887def des_subheader_factory( 2888 desid: str, desver: int, name: str = "subheader" 2889) -> DataExtensionSubheader: 2890 """Create a Data Extension Segment (DES) subheader 2891 2892 Parameters 2893 ---------- 2894 desid : str 2895 Unique DES type identifier 2896 desver : int 2897 Version of the data definition 2898 name : str, optional 2899 Name to give component 2900 2901 Returns 2902 ------- 2903 DataExtensionSubheader 2904 If the DES data definition is available, an object of the appropriate DataExtensionSubheader subclass. 2905 Otherwise, a DataExtensionSubheader object with generic DES subheader. 2906 """ 2907 des_subheaders = available_des_subheaders() 2908 subheader = des_subheaders.get((desid, desver), DataExtensionSubheader)(name) 2909 subheader["DESID"].value = desid 2910 subheader["DESVER"].value = desver 2911 return subheader
Create a Data Extension Segment (DES) subheader
Parameters
- desid (str): Unique DES type identifier
- desver (int): Version of the data definition
- name (str, optional): Name to give component
Returns
- DataExtensionSubheader: If the DES data definition is available, an object of the appropriate DataExtensionSubheader subclass. Otherwise, a DataExtensionSubheader object with generic DES subheader.
2914class DataExtensionSegment(Group): 2915 def __init__(self, name: str, data_size: int = 1): 2916 super().__init__(name) 2917 self._append(DataExtensionSubheader("subheader")) 2918 self._append(BinaryPlaceholder("DESDATA", data_size)) 2919 2920 def set_subheader(self, subhdr: DataExtensionSubheader) -> None: 2921 """Set this segment's subheader to ``subhdr``""" 2922 if not isinstance(subhdr, DataExtensionSubheader): 2923 raise TypeError(f"unexpected {type(subhdr)=}") 2924 if subhdr._parent is not None: 2925 subhdr = copy.deepcopy(subhdr) 2926 subhdr._parent = None 2927 subhdr.name = "subheader" 2928 self._replace( 2929 self["subheader"], 2930 subhdr, 2931 ) 2932 if isinstance(self["subheader"], TreOverflowDesSubheader): 2933 self._replace( 2934 self["DESDATA"], TreSequence("DESDATA", self["DESDATA"].get_size()) 2935 ) 2936 2937 def _load_impl(self, fd): 2938 for fld in ("DE", "DESID", "DESVER"): 2939 self["subheader"][fld].load(fd) 2940 assert self["subheader"]["DE"].value == "DE" 2941 self.set_subheader( 2942 des_subheader_factory( 2943 self["subheader"]["DESID"].value, self["subheader"]["DESVER"].value 2944 ) 2945 ) 2946 fd.seek(self.get_offset()) 2947 super()._load_impl(fd) 2948 2949 def print(self, *, file=None) -> None: 2950 print(f"# DESegment {self.name}", file=file) 2951 super().print(file=file)
A Collection of JBP fields. Indexed by JBP short names.
Parameters
- name (str): Name to give the group of fields
2920 def set_subheader(self, subhdr: DataExtensionSubheader) -> None: 2921 """Set this segment's subheader to ``subhdr``""" 2922 if not isinstance(subhdr, DataExtensionSubheader): 2923 raise TypeError(f"unexpected {type(subhdr)=}") 2924 if subhdr._parent is not None: 2925 subhdr = copy.deepcopy(subhdr) 2926 subhdr._parent = None 2927 subhdr.name = "subheader" 2928 self._replace( 2929 self["subheader"], 2930 subhdr, 2931 ) 2932 if isinstance(self["subheader"], TreOverflowDesSubheader): 2933 self._replace( 2934 self["DESDATA"], TreSequence("DESDATA", self["DESDATA"].get_size()) 2935 )
Set this segment's subheader to subhdr
2949 def print(self, *, file=None) -> None: 2950 print(f"# DESegment {self.name}", file=file) 2951 super().print(file=file)
Print information about the component to stdout
Inherited Members
2963class Jbp(Group): 2964 """Class representing an entire NITF/NSIF 2965 2966 Contains the following keys: 2967 * FileHeader 2968 * ImageSegments 2969 * GraphicSegments 2970 * TextSegments 2971 * DataExtensionSegments 2972 * ReservedExtensionSegments 2973 """ 2974 2975 def __init__(self): 2976 super().__init__("Root") 2977 self._append( 2978 FileHeader( 2979 "FileHeader", 2980 numi_callback=self._numi_handler, 2981 lin_callback=self._lin_handler, 2982 nums_callback=self._nums_handler, 2983 lsn_callback=self._lsn_handler, 2984 numt_callback=self._numt_handler, 2985 ltn_callback=self._ltn_handler, 2986 numdes_callback=self._numdes_handler, 2987 ldn_callback=self._ldn_handler, 2988 numres_callback=self._numres_handler, 2989 lreshn_callback=self._lreshn_handler, 2990 lren_callback=self._lren_handler, 2991 ) 2992 ) 2993 self._append( 2994 SegmentList( 2995 "ImageSegments", 2996 ImageSegment, 2997 maximum=999, 2998 ) 2999 ) 3000 self._append( 3001 SegmentList( 3002 "GraphicSegments", 3003 GraphicSegment, 3004 maximum=999, 3005 ) 3006 ) 3007 self._append( 3008 SegmentList( 3009 "TextSegments", 3010 TextSegment, 3011 maximum=999, 3012 ) 3013 ) 3014 self._append( 3015 SegmentList( 3016 "DataExtensionSegments", 3017 DataExtensionSegment, 3018 maximum=999, 3019 ) 3020 ) 3021 self._append( 3022 SegmentList( 3023 "ReservedExtensionSegments", 3024 ReservedExtensionSegment, 3025 maximum=999, 3026 ) 3027 ) 3028 3029 def _numi_handler(self, field: Field) -> None: 3030 self["ImageSegments"].set_count(field.value) 3031 3032 def _lin_handler(self, field: Field) -> None: 3033 idx = int(field.name.removeprefix("LI")) - 1 3034 self["ImageSegments"][idx]["Data"].size = field.value 3035 3036 def _nums_handler(self, field: Field) -> None: 3037 self["GraphicSegments"].set_count(field.value) 3038 3039 def _lsn_handler(self, field: Field) -> None: 3040 idx = int(field.name.removeprefix("LS")) - 1 3041 self["GraphicSegments"][idx]["Data"].size = field.value 3042 3043 def _numt_handler(self, field: Field) -> None: 3044 self["TextSegments"].set_count(field.value) 3045 3046 def _ltn_handler(self, field: Field) -> None: 3047 idx = int(field.name.removeprefix("LT")) - 1 3048 self["TextSegments"][idx]["Data"].size = field.value 3049 3050 def _numdes_handler(self, field: Field) -> None: 3051 self["DataExtensionSegments"].set_count(field.value) 3052 3053 def _ldn_handler(self, field: Field) -> None: 3054 idx = int(field.name.removeprefix("LD")) - 1 3055 self["DataExtensionSegments"][idx]["DESDATA"].size = field.value 3056 3057 def _numres_handler(self, field: Field) -> None: 3058 self["ReservedExtensionSegments"].set_count(field.value) 3059 3060 def _lreshn_handler(self, field: Field) -> None: 3061 # this callback should be removed if the Reserved Subheader is implemented 3062 idx = int(field.name.removeprefix("LRESH")) - 1 3063 self["ReservedExtensionSegments"][idx]["subheader"].size = field.value 3064 3065 def _lren_handler(self, field: Field) -> None: 3066 idx = int(field.name.removeprefix("LRE")) - 1 3067 self["ReservedExtensionSegments"][idx]["RESDATA"].size = field.value 3068 3069 def update_lengths(self) -> None: 3070 """Compute and set the segment lengths""" 3071 self["FileHeader"]["FL"]._set_value(self.get_size()) 3072 self["FileHeader"]["HL"]._set_value(self["FileHeader"].get_size()) 3073 3074 for idx, seg in enumerate(self["ImageSegments"]): 3075 self["FileHeader"][f"LISH{idx + 1:03d}"]._set_value( 3076 seg["subheader"].get_size() 3077 ) 3078 self["FileHeader"][f"LI{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3079 3080 for idx, seg in enumerate(self["GraphicSegments"]): 3081 self["FileHeader"][f"LSSH{idx + 1:03d}"]._set_value( 3082 seg["subheader"].get_size() 3083 ) 3084 self["FileHeader"][f"LS{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3085 3086 for idx, seg in enumerate(self["TextSegments"]): 3087 self["FileHeader"][f"LTSH{idx + 1:03d}"]._set_value( 3088 seg["subheader"].get_size() 3089 ) 3090 self["FileHeader"][f"LT{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3091 3092 for idx, seg in enumerate(self["DataExtensionSegments"]): 3093 self["FileHeader"][f"LDSH{idx + 1:03d}"]._set_value( 3094 seg["subheader"].get_size() 3095 ) 3096 self["FileHeader"][f"LD{idx + 1:03d}"]._set_value(seg["DESDATA"].get_size()) 3097 3098 for idx, seg in enumerate(self["ReservedExtensionSegments"]): 3099 self["FileHeader"][f"LRESH{idx + 1:03d}"]._set_value( 3100 seg["subheader"].get_size() 3101 ) 3102 self["FileHeader"][f"LRE{idx + 1:03d}"]._set_value( 3103 seg["RESDATA"].get_size() 3104 ) 3105 3106 def update_fdt(self) -> None: 3107 """Set the FDT field to the current time""" 3108 now = datetime.datetime.now(datetime.timezone.utc) 3109 self["FileHeader"]["FDT"].value = now.strftime("%Y%m%d%H%M%S") 3110 3111 def finalize(self) -> None: 3112 """Compute derived values such as lengths, and CLEVEL""" 3113 super().finalize() 3114 self.update_lengths() 3115 self.update_fdt() 3116 self.update_clevel() # must be after lengths 3117 3118 def _clevel_ccs_extent(self) -> int: 3119 min_ccs_row = min_ccs_col = 0 3120 max_ccs_row = max_ccs_col = 0 3121 3122 level_origin = {0: {"row": 0, "col": 0}} 3123 for imseg in self["ImageSegments"]: 3124 alvl = imseg["subheader"]["IALVL"].value 3125 dlvl = imseg["subheader"]["IDLVL"].value 3126 iloc_row, iloc_col = imseg["subheader"]["ILOC"].value 3127 nrows = imseg["subheader"]["NROWS"].value 3128 ncols = imseg["subheader"]["NCOLS"].value 3129 level_origin[dlvl] = { 3130 "row": level_origin[alvl]["row"] + iloc_row, 3131 "col": level_origin[alvl]["col"] + iloc_col, 3132 } 3133 3134 min_ccs_row = min(min_ccs_row, level_origin[dlvl]["row"]) 3135 min_ccs_col = min(min_ccs_col, level_origin[dlvl]["col"]) 3136 3137 max_ccs_row = max(max_ccs_row, level_origin[dlvl]["row"] + nrows) 3138 max_ccs_col = max(max_ccs_col, level_origin[dlvl]["col"] + ncols) 3139 3140 if len(self["GraphicSegments"]): 3141 logger.warning("CLEVEL of JBPs with Graphic Segments is not supported") 3142 3143 max_extent = max(max_ccs_row - min_ccs_row, max_ccs_col - min_ccs_col) 3144 if max_extent <= 2047: 3145 return 3 3146 if max_extent <= 8191: 3147 return 5 3148 if max_extent <= 65535: 3149 return 6 3150 if max_extent <= 99_999_999: 3151 return 7 3152 return 9 3153 3154 def _clevel_file_size(self) -> int: 3155 if self["FileHeader"]["FL"].value < 50 * (1 << 20): 3156 return 3 3157 if self["FileHeader"]["FL"].value < 1 * (1 << 30): 3158 return 5 3159 if self["FileHeader"]["FL"].value < 2 * (1 << 30): 3160 return 6 3161 if self["FileHeader"]["FL"].value < 10 * (1 << 30): 3162 return 7 3163 return 9 3164 3165 def _clevel_image_size(self) -> int: 3166 clevel = 3 3167 for imseg in self["ImageSegments"]: 3168 nrows = imseg["subheader"]["NROWS"].value 3169 ncols = imseg["subheader"]["NCOLS"].value 3170 3171 if nrows <= 2048 and ncols <= 2048: 3172 clevel = max(clevel, 3) 3173 elif nrows <= 8192 and ncols <= 8192: 3174 clevel = max(clevel, 5) 3175 elif nrows <= 65536 and ncols <= 65536: 3176 clevel = max(clevel, 6) 3177 elif nrows <= 99_999_999 and ncols <= 99_999_999: 3178 clevel = max(clevel, 7) 3179 return clevel 3180 3181 def _clevel_image_blocking(self) -> int: 3182 clevel = 3 3183 for imseg in self["ImageSegments"]: 3184 horiz = imseg["subheader"]["NPPBH"].value 3185 vert = imseg["subheader"]["NPPBV"].value 3186 3187 if horiz <= 2048 and vert <= 2048: 3188 clevel = max(clevel, 3) 3189 elif horiz <= 8192 and vert <= 8192: 3190 clevel = max(clevel, 5) 3191 return clevel 3192 3193 def _clevel_irep(self) -> int: 3194 clevel = 0 3195 for imseg in self["ImageSegments"]: 3196 has_lut = bool(imseg["subheader"].find_all("NLUT.*")) 3197 num_bands = ( 3198 imseg["subheader"].get("XBANDS", imseg["subheader"]["NBANDS"]).value 3199 ) 3200 # Color (RGB) No Compression 3201 if ( 3202 imseg["subheader"]["IREP"].value == "RGB" 3203 and num_bands == 3 3204 and not has_lut 3205 and imseg["subheader"]["IC"].value in ("NC", "NM") 3206 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3207 ): 3208 if imseg["subheader"]["NBPP"].value == 8: 3209 clevel = max(clevel, 3) 3210 3211 if imseg["subheader"]["NBPP"].value in (8, 16, 32): 3212 clevel = max(clevel, 6) 3213 3214 # Multiband (MULTI) No Compression 3215 if ( 3216 imseg["subheader"]["IREP"].value == "MULTI" 3217 and imseg["subheader"]["NBPP"].value in (1, 8, 16, 32, 64) 3218 and imseg["subheader"]["IC"].value in ("NC", "NM") 3219 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3220 ): 3221 if 2 <= num_bands <= 9: 3222 clevel = max(clevel, 3) 3223 3224 if 10 <= num_bands <= 255: 3225 clevel = max(clevel, 5) 3226 3227 if 255 <= num_bands <= 999: 3228 clevel = max(clevel, 7) 3229 3230 # JPEG2000 Compression Multiband (MULTI) 3231 if ( 3232 imseg["subheader"]["IREP"].value == "MULTI" 3233 and imseg["subheader"]["NBPP"].value <= 32 3234 and imseg["subheader"]["IC"].value in ("C8", "M8") 3235 and imseg["subheader"]["IMODE"].value == "B" 3236 ): 3237 if 1 <= num_bands <= 9: 3238 clevel = max(clevel, 3) 3239 3240 if 10 <= num_bands <= 255: 3241 clevel = max(clevel, 5) 3242 3243 if 256 <= num_bands <= 999: 3244 clevel = max(clevel, 7) 3245 3246 # Multiband (MULTI) Individual Band JPEG Compression 3247 if ( 3248 imseg["subheader"]["IREP"].value == "MULTI" 3249 and imseg["subheader"]["NBPP"].value in (8, 12) 3250 and not has_lut 3251 and imseg["subheader"]["IC"].value in ("C3", "M3") 3252 and imseg["subheader"]["IMODE"].value in ("B", "S") 3253 ): 3254 if 2 <= num_bands <= 9: 3255 clevel = max(clevel, 3) 3256 3257 if 10 <= num_bands <= 255: 3258 clevel = max(clevel, 5) 3259 3260 if 256 <= num_bands <= 999: 3261 clevel = max(clevel, 7) 3262 3263 # Multiband (MULTI) Multi-Component Compression 3264 if ( 3265 imseg["subheader"]["IREP"].value == "MULTI" 3266 and imseg["subheader"]["NBPP"].value in (8, 12) 3267 and not has_lut 3268 and imseg["subheader"]["IC"].value in ("C6", "M6") 3269 and imseg["subheader"]["IMODE"].value in ("B", "P", "S") 3270 ): 3271 if 2 <= num_bands <= 9: 3272 clevel = max(clevel, 3) 3273 3274 if 10 <= num_bands <= 255: 3275 clevel = max(clevel, 5) 3276 3277 if 256 <= num_bands <= 999: 3278 clevel = max(clevel, 7) 3279 3280 # Matrix Data (NODISPLY) 3281 if ( 3282 imseg["subheader"]["IREP"].value == "NODISPLY" 3283 and imseg["subheader"]["NBPP"].value in (8, 16, 32, 64) 3284 and not has_lut 3285 and imseg["subheader"]["IMODE"].value in ("B", "P", "R", "S") 3286 ): 3287 if 2 <= num_bands <= 9: 3288 clevel = max(clevel, 3) 3289 3290 if 10 <= num_bands <= 255: 3291 clevel = max(clevel, 5) 3292 3293 if 256 <= num_bands <= 999: 3294 clevel = max(clevel, 7) 3295 3296 return clevel 3297 3298 def _clevel_num_imseg(self) -> int: 3299 if len(self["ImageSegments"]) <= 20: 3300 return 3 3301 if 20 < len(self["ImageSegments"]) <= 100: 3302 return 5 3303 return 9 3304 3305 def _clevel_aggregate_size_of_graphic_segments(self) -> int: 3306 size = 0 3307 for field in self["FileHeader"].find_all("LS\\d+"): 3308 size += field.value 3309 3310 if size <= 1 * (1 << 20): 3311 return 3 3312 if size <= 2 * (1 << 20): 3313 return 5 3314 return 9 3315 3316 def _clevel_cl9(self) -> int: 3317 """Explicit CLEVEL 9 checks""" 3318 # 1 3319 if self["FileHeader"]["FL"].value >= 10 * (1 << 30): 3320 return 9 3321 3322 total_num_bands = 0 3323 for imseg in self["ImageSegments"]: 3324 # 2 3325 if ( 3326 imseg["subheader"]["NPPBH"].value == 0 3327 or imseg["subheader"]["NPPBV"].value == 0 3328 ): 3329 return 9 3330 total_num_bands += imseg.get("XBANDS", imseg["subheader"]["NBANDS"]).value 3331 3332 # 3 3333 if total_num_bands > 999: 3334 return 9 3335 3336 # 4 3337 if len(self["ImageSegments"]) > 100: 3338 return 9 3339 3340 # 5 3341 if len(self["GraphicSegments"]) > 100: 3342 return 9 3343 3344 # 6 3345 size = 0 3346 for field in self["FileHeader"].find_all("LS\\d+"): 3347 size += field.value 3348 if size > 2 * (1 << 20): 3349 return 9 3350 3351 # 7 3352 if len(self["TextSegments"]) > 32: 3353 return 9 3354 3355 # 8 3356 if len(self["DataExtensionSegments"]) > 100: 3357 return 9 3358 3359 return 0 3360 3361 def update_clevel(self) -> None: 3362 """Compute and update the CLEVEL field. See JBP-2025.1 Table G-1""" 3363 clevel = 3 3364 helpers = [attrib for attrib in dir(self) if attrib.startswith("_clevel_")] 3365 for helper in helpers: 3366 clevel = max(clevel, getattr(self, helper)()) 3367 3368 self["FileHeader"]["CLEVEL"].value = clevel
Class representing an entire NITF/NSIF
Contains the following keys:
- FileHeader
- ImageSegments
- GraphicSegments
- TextSegments
- DataExtensionSegments
- ReservedExtensionSegments
3069 def update_lengths(self) -> None: 3070 """Compute and set the segment lengths""" 3071 self["FileHeader"]["FL"]._set_value(self.get_size()) 3072 self["FileHeader"]["HL"]._set_value(self["FileHeader"].get_size()) 3073 3074 for idx, seg in enumerate(self["ImageSegments"]): 3075 self["FileHeader"][f"LISH{idx + 1:03d}"]._set_value( 3076 seg["subheader"].get_size() 3077 ) 3078 self["FileHeader"][f"LI{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3079 3080 for idx, seg in enumerate(self["GraphicSegments"]): 3081 self["FileHeader"][f"LSSH{idx + 1:03d}"]._set_value( 3082 seg["subheader"].get_size() 3083 ) 3084 self["FileHeader"][f"LS{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3085 3086 for idx, seg in enumerate(self["TextSegments"]): 3087 self["FileHeader"][f"LTSH{idx + 1:03d}"]._set_value( 3088 seg["subheader"].get_size() 3089 ) 3090 self["FileHeader"][f"LT{idx + 1:03d}"]._set_value(seg["Data"].get_size()) 3091 3092 for idx, seg in enumerate(self["DataExtensionSegments"]): 3093 self["FileHeader"][f"LDSH{idx + 1:03d}"]._set_value( 3094 seg["subheader"].get_size() 3095 ) 3096 self["FileHeader"][f"LD{idx + 1:03d}"]._set_value(seg["DESDATA"].get_size()) 3097 3098 for idx, seg in enumerate(self["ReservedExtensionSegments"]): 3099 self["FileHeader"][f"LRESH{idx + 1:03d}"]._set_value( 3100 seg["subheader"].get_size() 3101 ) 3102 self["FileHeader"][f"LRE{idx + 1:03d}"]._set_value( 3103 seg["RESDATA"].get_size() 3104 )
Compute and set the segment lengths
3106 def update_fdt(self) -> None: 3107 """Set the FDT field to the current time""" 3108 now = datetime.datetime.now(datetime.timezone.utc) 3109 self["FileHeader"]["FDT"].value = now.strftime("%Y%m%d%H%M%S")
Set the FDT field to the current time
3111 def finalize(self) -> None: 3112 """Compute derived values such as lengths, and CLEVEL""" 3113 super().finalize() 3114 self.update_lengths() 3115 self.update_fdt() 3116 self.update_clevel() # must be after lengths
Compute derived values such as lengths, and CLEVEL
3361 def update_clevel(self) -> None: 3362 """Compute and update the CLEVEL field. See JBP-2025.1 Table G-1""" 3363 clevel = 3 3364 helpers = [attrib for attrib in dir(self) if attrib.startswith("_clevel_")] 3365 for helper in helpers: 3366 clevel = max(clevel, getattr(self, helper)()) 3367 3368 self["FileHeader"]["CLEVEL"].value = clevel
Compute and update the CLEVEL field. See JBP-2025.1 Table G-1
Inherited Members
3371class TreSequence(ComponentCollection, collections.abc.MutableSequence): 3372 """ 3373 TREs which appear one after the other with no intervening bytes 3374 3375 Intended for use as the user defined and/or extended data fields. See Section 5.9.3. 3376 3377 Parameters 3378 ---------- 3379 name : str 3380 Name to give the field 3381 length : int 3382 Initial length in bytes 3383 """ 3384 3385 def __init__(self, name, length): 3386 super().__init__(name) 3387 self._length = length 3388 3389 def _load_impl(self, fd): 3390 if self._children: 3391 return super()._load_impl(fd) 3392 3393 # else need to discover which TREs are in the file 3394 bytes_read = 0 3395 while bytes_read < self._length: 3396 tretag = fd.read(6).decode() 3397 fd.seek(-6, os.SEEK_CUR) 3398 tre = tre_factory(tretag) 3399 self._append(tre) 3400 tre.load(fd) 3401 bytes_read += tre.get_size() 3402 3403 if bytes_read != self._length: 3404 logger.warning( 3405 f"Length of TREs ({bytes_read}) in {self.name} does not match expected length ({self._length})" 3406 ) 3407 3408 def __getitem__(self, key): 3409 return self._children[key] 3410 3411 def __setitem__(self, key, value): 3412 value._parent = self 3413 self._children[key] = value 3414 3415 def __delitem__(self, key): 3416 del self._children[key] 3417 3418 def __len__(self): 3419 return len(self._children) 3420 3421 def insert(self, index, element): 3422 element._parent = self 3423 self._children.insert(index, element)
TREs which appear one after the other with no intervening bytes
Intended for use as the user defined and/or extended data fields. See Section 5.9.3.
Parameters
- name (str): Name to give the field
- length (int): Initial length in bytes
3421 def insert(self, index, element): 3422 element._parent = self 3423 self._children.insert(index, element)
S.insert(index, value) -- insert value before index
Inherited Members
3426class Tre(Group): 3427 """Base class for TREs 3428 3429 Includes the TRETAG and TREL tags. 3430 3431 Parameters 3432 ---------- 3433 identifier : str 3434 identifier of the TRE. Must be 1-6 characters. 3435 tretag_rename : str 3436 Alternative to give the 'TRETAG' field 3437 trel_rename : str 3438 Alternative to give the 'TREL' field 3439 length_constraint : RangeCheck or None 3440 Decoded range check for 'TREL' field. Defaults to MinMax(1, 99985) 3441 3442 Notes 3443 ----- 3444 BIIF and JBP define TREs as having 3 fields, TRETAG, TREL, and. TREDATA. 3445 However, TREs commonly rename TRETAG and TREL and define their own fields as replacing TREDATA. 3446 """ 3447 3448 def __init__( 3449 self, 3450 identifier: str, 3451 tretag_rename: str = "TRETAG", 3452 trel_rename: str = "TREL", 3453 length_constraint: RangeCheck | None = None, 3454 ): 3455 if not (1 <= len(identifier) <= 6): 3456 raise ValueError(f"TRE identifier '{identifier}' must be 1-6 characters") 3457 3458 ident_rstrip = identifier.rstrip(" ") 3459 super().__init__(ident_rstrip) 3460 self.tretag_rename = tretag_rename 3461 self.trel_rename = trel_rename 3462 3463 if length_constraint is None: 3464 length_constraint = MinMax(1, 99985) 3465 3466 self._append( 3467 Field( 3468 tretag_rename, 3469 "Unique Extension Type Identifier", 3470 6, 3471 charset=BCSA, 3472 decoded_range=Constant(ident_rstrip), 3473 converter=StringAscii(), 3474 default=ident_rstrip, 3475 ) 3476 ) 3477 3478 self._append( 3479 Field( 3480 trel_rename, 3481 "Length of the TREDATA", 3482 5, 3483 charset=BCSN_PI, 3484 decoded_range=length_constraint, 3485 converter=Integer(), 3486 default=0, 3487 ) 3488 ) 3489 3490 def finalize(self) -> None: 3491 """Set the TREL field""" 3492 length = 0 3493 for child in self._children: 3494 if child.name in (self.tretag_rename, self.trel_rename): 3495 continue 3496 length += child.get_size() 3497 self[self.trel_rename]._set_value(length)
Base class for TREs
Includes the TRETAG and TREL tags.
Parameters
- identifier (str): identifier of the TRE. Must be 1-6 characters.
- tretag_rename (str): Alternative to give the 'TRETAG' field
- trel_rename (str): Alternative to give the 'TREL' field
- length_constraint (RangeCheck or None): Decoded range check for 'TREL' field. Defaults to MinMax(1, 99985)
Notes
BIIF and JBP define TREs as having 3 fields, TRETAG, TREL, and. TREDATA. However, TREs commonly rename TRETAG and TREL and define their own fields as replacing TREDATA.
3448 def __init__( 3449 self, 3450 identifier: str, 3451 tretag_rename: str = "TRETAG", 3452 trel_rename: str = "TREL", 3453 length_constraint: RangeCheck | None = None, 3454 ): 3455 if not (1 <= len(identifier) <= 6): 3456 raise ValueError(f"TRE identifier '{identifier}' must be 1-6 characters") 3457 3458 ident_rstrip = identifier.rstrip(" ") 3459 super().__init__(ident_rstrip) 3460 self.tretag_rename = tretag_rename 3461 self.trel_rename = trel_rename 3462 3463 if length_constraint is None: 3464 length_constraint = MinMax(1, 99985) 3465 3466 self._append( 3467 Field( 3468 tretag_rename, 3469 "Unique Extension Type Identifier", 3470 6, 3471 charset=BCSA, 3472 decoded_range=Constant(ident_rstrip), 3473 converter=StringAscii(), 3474 default=ident_rstrip, 3475 ) 3476 ) 3477 3478 self._append( 3479 Field( 3480 trel_rename, 3481 "Length of the TREDATA", 3482 5, 3483 charset=BCSN_PI, 3484 decoded_range=length_constraint, 3485 converter=Integer(), 3486 default=0, 3487 ) 3488 )
3490 def finalize(self) -> None: 3491 """Set the TREL field""" 3492 length = 0 3493 for child in self._children: 3494 if child.name in (self.tretag_rename, self.trel_rename): 3495 continue 3496 length += child.get_size() 3497 self[self.trel_rename]._set_value(length)
Set the TREL field
Inherited Members
3500class UnknownTre(Tre): 3501 """TRE without known TREDATA definition. 3502 see: Table 5.9-1. Registered and Controlled Tagged Record Extension Format 3503 """ 3504 3505 def __init__(self, name): 3506 super().__init__(name) 3507 self["TREL"]._setter_callback = self._trel_handler 3508 3509 self._append( 3510 Field( 3511 "TREDATA", 3512 "User-Defined Data", 3513 0, 3514 converter=Bytes(), 3515 default=b"", 3516 ) 3517 ) 3518 3519 def _trel_handler(self, field): 3520 self["TREDATA"].size = field.value
TRE without known TREDATA definition. see: Table 5.9-1. Registered and Controlled Tagged Record Extension Format
3523def available_tres() -> dict[str, Callable[[], Tre]]: 3524 """All discovered and available Tagged Record Extensions (TREs) 3525 3526 Returns 3527 ------- 3528 dict of {str : callable} 3529 Mapping of TRETAG name to a function with no required arguments that 3530 instantiates the appropriate TRE 3531 """ 3532 d = {} 3533 for plugin in importlib.metadata.entry_points(group="jbpy.extensions.tre"): 3534 try: 3535 assert len(plugin.name) == 6 3536 tretag = plugin.name.rstrip() 3537 d[tretag] = plugin.load() 3538 except AssertionError: 3539 logger.warning(f"Skipping {plugin=}; unable to parse") 3540 return d
All discovered and available Tagged Record Extensions (TREs)
Returns
- dict of {str (callable}): Mapping of TRETAG name to a function with no required arguments that instantiates the appropriate TRE
3543def tre_factory(tretag: str) -> Tre: 3544 """Create a TRE instance 3545 3546 Parameters 3547 ---------- 3548 tretag : str 3549 The 1-6 character name of the TRE 3550 3551 Returns 3552 ------- 3553 Tre 3554 TRE object 3555 """ 3556 tres = available_tres() 3557 if tretag in tres: 3558 return tres[tretag]() 3559 3560 return UnknownTre(tretag)
Create a TRE instance
Parameters
- tretag (str): The 1-6 character name of the TRE
Returns
- Tre: TRE object