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)
logger = <Logger jbpy.core (WARNING)>
LRESH_MIN = 200
class BinaryFile_R:
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

@abc.abstractmethod
def seek( self, _BinaryFile_R__offset: int, _BinaryFile_R__whence: int = Ellipsis) -> int:
37    @abc.abstractmethod
38    def seek(self, __offset: int, __whence: int = ...) -> int: ...
@abc.abstractmethod
def read(self, _BinaryFile_R__length: int = Ellipsis) -> bytes:
39    @abc.abstractmethod
40    def read(self, __length: int = ...) -> bytes: ...
class BinaryFile_RW(BinaryFile_R):
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

@abc.abstractmethod
def write(self, _BinaryFile_RW__data: bytes) -> int:
46    @abc.abstractmethod
47    def write(self, __data: bytes) -> int: ...
Inherited Members
class SubFile:
 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
SubFile(file: Any, start: int, length: int)
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
def seek(self, offset: int, whence: int = 0) -> int:
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
def tell(self) -> int:
100    def tell(self) -> int:
101        """Return the current position within the subfile."""
102        return self._pos

Return the current position within the subfile.

def read(self, size: int = -1) -> bytes:
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
def readinto(self, b) -> int | None:
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
def readline(self, size=-1) -> bytes:
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
def readlines(self, hint=-1) -> list[bytes]:
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
def readable(self) -> bool:
162    def readable(self) -> bool:
163        return self._file.readable()
class PythonConverter(abc.ABC):
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

def to_bytes(self, decoded_value: Any, size: int) -> bytes:
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
@abc.abstractmethod
def to_bytes_impl(self, decoded_value: Any, size: int) -> bytes:
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

def from_bytes(self, encoded_value: bytes) -> Any:
190    def from_bytes(self, encoded_value: bytes) -> Any:
191        """Convert bytes to python type"""
192        return self.from_bytes_impl(encoded_value)

Convert bytes to python type

@abc.abstractmethod
def from_bytes_impl(self, encoded_value) -> Any:
194    @abc.abstractmethod
195    def from_bytes_impl(self, encoded_value) -> Any:
196        """Convert bytes to python type"""

Convert bytes to python type

class StringUtf8(PythonConverter):
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

def to_bytes_impl(self, decoded_value: str, size: int) -> bytes:
202    def to_bytes_impl(self, decoded_value: str, size: int) -> bytes:
203        return decoded_value.encode().ljust(size)

Convert python type to bytes

def from_bytes_impl(self, encoded_value: bytes) -> str:
205    def from_bytes_impl(self, encoded_value: bytes) -> str:
206        return encoded_value.decode().rstrip(" ")

Convert bytes to python type

Inherited Members
class StringAscii(PythonConverter):
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

def to_bytes_impl(self, decoded_value: str, size: int) -> bytes:
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

def from_bytes_impl(self, encoded_value: bytes) -> str:
215    def from_bytes_impl(self, encoded_value: bytes) -> str:
216        return encoded_value.decode("ascii").rstrip(" ")

Convert bytes to python type

Inherited Members
class StringISO8859_1(PythonConverter):
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.

def to_bytes_impl(self, decoded_value: str, size: int) -> bytes:
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

def from_bytes_impl(self, encoded_value: bytes) -> str:
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
class IntPair(PythonConverter):
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

def to_bytes_impl(self, decoded_value: tuple[int, int], size: int) -> bytes:
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

def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int]:
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
class Bytes(PythonConverter):
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

def to_bytes_impl(self, decoded_value: bytes, size: int) -> 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

def from_bytes_impl(self, encoded_value: bytes) -> bytes:
257    def from_bytes_impl(self, encoded_value: bytes) -> bytes:
258        return encoded_value

Convert bytes to python type

Inherited Members
class Integer(PythonConverter):
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 sign is 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
Integer(sign: Literal['+', '-', ' '] = '-')
275    def __init__(self, sign: Literal["+", "-", " "] = "-"):
276        self.sign = sign
sign
def to_bytes_impl(self, decoded_value: int, size: int) -> bytes:
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

def from_bytes_impl(self, encoded_value: bytes) -> int:
282    def from_bytes_impl(self, encoded_value: bytes) -> int:
283        return int(encoded_value)

Convert bytes to python type

Inherited Members
class RGB(PythonConverter):
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

def to_bytes_impl(self, decoded_value: tuple[int, int, int], size: int) -> bytes:
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

def from_bytes_impl(self, encoded_value: bytes) -> tuple[int, int, int]:
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
ECS = ' -~\xa0-ÿ\n\x0c\r'
ECSA = ' -~\xa0-ÿ'
BCS = ' -~\n\x0c\r'
BCSA = ' -~'
BCSN = '0-9+-./'
BCSN_I = '0-9+-'
BCSN_PI = '0-9'
U8 = '\x00-ÿ'
class RangeCheck(abc.ABC):
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

@abc.abstractmethod
def isvalid(self, value: Any) -> bool:
326    @abc.abstractmethod
327    def isvalid(self, value: Any) -> bool:
328        """Returns ``True`` if field satisfies range check."""

Returns True if field satisfies range check.

class AnyRange(RangeCheck):
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

def isvalid(self, value: Any) -> bool:
334    def isvalid(self, value: Any) -> bool:
335        return True

Returns True if field satisfies range check.

class MinMax(RangeCheck):
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.
MinMax(minimum: int | float | None, maximum: int | float | None)
349    def __init__(self, minimum: int | float | None, maximum: int | float | None):
350        self.minimum = minimum
351        self.maximum = maximum
minimum
maximum
def isvalid(self, value: int | float) -> bool:
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.

class Regex(RangeCheck):
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

Regex(pattern: str)
365    def __init__(self, pattern: str):
366        self.pattern = pattern
pattern
def isvalid(self, value: str) -> bool:
368    def isvalid(self, value: str) -> bool:
369        return bool(re.fullmatch(self.pattern, value))

Returns True if field satisfies range check.

class Constant(RangeCheck):
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

Constant(const: Any)
375    def __init__(self, const: Any):
376        self.const = const
const
def isvalid(self, value: Any) -> bool:
378    def isvalid(self, value: Any) -> bool:
379        return value == self.const

Returns True if field satisfies range check.

class Enum(RangeCheck):
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

Enum(enumeration: Iterable)
385    def __init__(self, enumeration: Iterable):
386        self.enumeration = set(enumeration)
enumeration
def isvalid(self, value: Any) -> bool:
388    def isvalid(self, value: Any) -> bool:
389        return value in self.enumeration

Returns True if field satisfies range check.

class AnyOf(RangeCheck):
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
AnyOf(*ranges: RangeCheck)
401    def __init__(self, *ranges: RangeCheck):
402        self.ranges = ranges
ranges
def isvalid(self, value: Any) -> bool:
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)

Returns True if field satisfies range check.

class AllOf(RangeCheck):
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
AllOf(*ranges: RangeCheck)
418    def __init__(self, *ranges: RangeCheck):
419        self.ranges = ranges
ranges
def isvalid(self, value: Any) -> bool:
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)

Returns True if field satisfies range check.

class Not(RangeCheck):
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

Not(range_check: RangeCheck)
429    def __init__(self, range_check: RangeCheck):
430        self.range_check = range_check
range_check
def isvalid(self, value: Any) -> bool:
432    def isvalid(self, value: Any) -> bool:
433        return not self.range_check.isvalid(value)

Returns True if field satisfies range check.

PATTERN_CC = '[0-9]{2}'
PATTERN_YY = '[0-9]{2}'
PATTERN_MM = '(0[1-9]|1[0-2])'
PATTERN_DD = '(0[1-9]|[12][0-9]|3[0-1])'
PATTERN_HH = '([0-1][0-9]|2[0-3])'
PATTERN_mm = '([0-5][0-9])'
PATTERN_SS = '([0-5][0-9])'
DATETIME_REGEX = <Regex object>
DATE_REGEX = <Regex object>
class JbpIOComponent:
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

JbpIOComponent(name: str)
459    def __init__(self, name: str):
460        self.name = name
461        self._parent: ComponentCollection | None = None
name
def load(self, fd: BinaryFile_R) -> Self:
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
def dump(self, fd: BinaryFile_RW, seek_first: bool = False) -> int:
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
def get_offset(self) -> int:
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

def get_size(self) -> int:
519    def get_size(self) -> int:
520        """Size of this component in bytes"""
521        raise NotImplementedError()

Size of this component in bytes

def as_json(self, full: bool = False) -> str:
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
def as_text(self) -> str:
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

def print(self, *, file=None) -> None:
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

def finalize(self):
543    def finalize(self):
544        """Perform any necessary final updates"""

Perform any necessary final updates

def as_filelike(self, file: Any) -> SubFile:
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
class Field(JbpIOComponent):
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): True if BCS-A spaces are allowed for entire field (often denoted with "<>" in JBP Field Type). When True, charset, range checks, conversion, etc. are bypassed when the python-typed value is None.
Attributes
  • description (str): Text description of the field. For informational purposes only.
  • size (int): Field size in bytes
  • nullable (bool): True if BCS-A spaces are allowed for entire field
  • encoded_value (bytes): Field value as bytes
  • value: Field value as python type
Field( name: str, description: str, size: int, *, charset: str | None = None, encoded_range: RangeCheck | None = None, decoded_range: RangeCheck | None = None, converter: PythonConverter, default: Any, setter_callback: Callable | None = None, nullable: bool = False)
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
description
nullable
def isnull(self) -> bool:
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

def isvalid(self) -> bool:
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

encoded_value: bytes
681    @property
682    def encoded_value(self) -> bytes:
683        return self._encoded_value
size: int
706    @property
707    def size(self) -> int:
708        return self._size
value: Any
718    @property
719    def value(self) -> Any:
720        if self.isnull():
721            return None
722        return self._converter.from_bytes(self.encoded_value)
def get_size(self) -> int:
743    def get_size(self) -> int:
744        return self.size

Size of this component in bytes

def print(self, *, file=None) -> None:
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

class BinaryPlaceholder(JbpIOComponent):
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.

BinaryPlaceholder(name: str, size: int)
759    def __init__(self, name: str, size: int):
760        super().__init__(name)
761        self._size = size
size: int
769    @property
770    def size(self) -> int:
771        return self._size
def get_size(self) -> int:
785    def get_size(self) -> int:
786        return self.size

Size of this component in bytes

def print(self, *, file=None) -> None:
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

class ComponentCollection(JbpIOComponent):
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

ComponentCollection(name: str)
797    def __init__(self, name: str):
798        super().__init__(name)
799        self._children: Final[list[JbpIOComponent]] = []
def get_size(self) -> int:
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

def get_offset_of(self, child_obj: JbpIOComponent) -> int:
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}")
def print(self, *, file=None) -> None:
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

def finalize(self):
865    def finalize(self):
866        for child in self._children:
867            child.finalize()

Perform any necessary final updates

class Group(ComponentCollection, collections.abc.Mapping):
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
Group(name)
880    def __init__(self, name):
881        super().__init__(name)
def find_all(self, pattern: str) -> Iterator[JbpIOComponent]:
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
class SegmentList(ComponentCollection, collections.abc.Sequence):
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

SegmentList( name: str, field_creator: Callable[[str], Group], minimum: int = 0, maximum: int = 1)
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)
field_creator
minimum
maximum
def set_count(self, size: int) -> None:
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()
class SecurityFields(Group):
 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

SecurityFields(name: str, x: str)
 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        )
class FileHeader(Group):
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

FileHeader( name: str, numi_callback: Callable | None = None, lin_callback: Callable | None = None, nums_callback: Callable | None = None, lsn_callback: Callable | None = None, numt_callback: Callable | None = None, ltn_callback: Callable | None = None, numdes_callback: Callable | None = None, ldn_callback: Callable | None = None, numres_callback: Callable | None = None, lreshn_callback: Callable | None = None, lren_callback: Callable | None = None)
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        )
numi_callback
lin_callback
nums_callback
lsn_callback
numt_callback
ltn_callback
numdes_callback
ldn_callback
numres_callback
lreshn_callback
lren_callback
def finalize(self) -> None:
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

class ImageSubheader(Group):
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

ImageSubheader(name: str)
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        )
def finalize(self) -> None:
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

class ImageSegment(Group):
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
ImageSegment(name: str, data_size: int = 1)
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))
def print(self, *, file=None) -> None:
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

class GraphicSubheader(Group):
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

GraphicSubheader(name: str)
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        )
def finalize(self) -> None:
2544    def finalize(self) -> None:
2545        super().finalize()
2546        _update_tre_lengths(self, "SXSHDL", "SXSOFL", "SXSHD")

Perform any necessary final updates

class GraphicSegment(Group):
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
GraphicSegment(name: str, data_size: int = 1)
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))
def print(self, *, file=None) -> None:
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

class TextSubheader(Group):
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

TextSubheader(name: str)
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        )
def finalize(self) -> None:
2688    def finalize(self) -> None:
2689        super().finalize()
2690        _update_tre_lengths(self, "TXSHDL", "TXSOFL", "TXSHD")

Perform any necessary final updates

class TextSegment(Group):
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
TextSegment(name: str, data_size: int = 1)
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))
def print(self, *, file=None) -> None:
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

class ReservedExtensionSegment(Group):
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
ReservedExtensionSegment(name: str, subheader_size: int = 200, data_size: int = 1)
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))
def print(self, *, file=None) -> None:
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

class DataExtensionSubheader(Group):
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

DataExtensionSubheader( name: str, *, desid_constraint: RangeCheck | None = None, desver_constraint: RangeCheck | None = None, desshl_constraint: RangeCheck | None = None)
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
class TreOverflowDesSubheader(DataExtensionSubheader):
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

TreOverflowDesSubheader(name)
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        )
DesSubheaderDefs = dict[tuple[str, int], collections.abc.Callable[[str], DataExtensionSubheader]]
def available_des_subheaders() -> dict[tuple[str, int], Callable[[str], DataExtensionSubheader]]:
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
def des_subheader_factory( desid: str, desver: int, name: str = 'subheader') -> DataExtensionSubheader:
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.
class DataExtensionSegment(Group):
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
DataExtensionSegment(name: str, data_size: int = 1)
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))
def set_subheader(self, subhdr: DataExtensionSubheader) -> None:
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

def print(self, *, file=None) -> None:
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

class Jbp(Group):
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
def update_lengths(self) -> None:
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

def update_fdt(self) -> None:
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

def finalize(self) -> None:
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

def update_clevel(self) -> None:
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

class TreSequence(ComponentCollection, collections.abc.MutableSequence):
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
TreSequence(name, length)
3385    def __init__(self, name, length):
3386        super().__init__(name)
3387        self._length = length
def insert(self, index, element):
3421    def insert(self, index, element):
3422        element._parent = self
3423        self._children.insert(index, element)

S.insert(index, value) -- insert value before index

class Tre(Group):
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.

Tre( identifier: str, tretag_rename: str = 'TRETAG', trel_rename: str = 'TREL', length_constraint: RangeCheck | None = None)
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        )
tretag_rename
trel_rename
def finalize(self) -> None:
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

class UnknownTre(Tre):
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

UnknownTre(name)
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        )
def available_tres() -> dict[str, Callable[[], Tre]]:
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
def tre_factory(tretag: str) -> 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