diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc224c..fd82c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## Added +- `print()` method now supports the `file` argument +- `as_json()` and `as_text()` methods + ## [0.5.1] - 2025-12-17 diff --git a/jbpy/_jbpinfo.py b/jbpy/_jbpinfo.py index ceaa7c5..ff861e6 100644 --- a/jbpy/_jbpinfo.py +++ b/jbpy/_jbpinfo.py @@ -1,6 +1,4 @@ import argparse -import collections.abc -import json import os import sys @@ -12,39 +10,6 @@ pass -class _Encoder(json.JSONEncoder): - def __init__(self, *args, full_details=False, **kwargs): - super().__init__(*args, **kwargs) - self.full_details = full_details - - def default(self, obj): - if isinstance(obj, collections.abc.Mapping): - return dict(obj) - if isinstance(obj, bytes): - return list(obj) - if isinstance(obj, jbpy.core.Field): - if self.full_details: - return { - "size": obj.size, - "offset": obj.get_offset(), - "value": obj.value, - } - return obj.value - if isinstance(obj, jbpy.core.BinaryPlaceholder): - if self.full_details: - return { - "size": obj.size, - "offset": obj.get_offset(), - "value": "__binary__", - } - return f"__binary__ ({obj.get_size()} bytes)" - if isinstance(obj, jbpy.core.SegmentList): - return list(obj) - if isinstance(obj, jbpy.core.TreSequence): - return list(obj) - return super().default(obj) - - def main(args=None): parser = argparse.ArgumentParser(description="Display JBP Header content") parser.add_argument("filename", help="Path to JBP file") @@ -63,7 +28,7 @@ def main(args=None): try: if "json" in config.format: full_details = config.format == "json-full" - print(json.dumps(jbp, indent=2, cls=_Encoder, full_details=full_details)) + print(jbp.as_json(full_details)) else: jbp.print() except BrokenPipeError: diff --git a/jbpy/core.py b/jbpy/core.py index ade652d..f696e2d 100644 --- a/jbpy/core.py +++ b/jbpy/core.py @@ -17,6 +17,8 @@ import copy import datetime import importlib.metadata +import io +import json import logging import os import re @@ -506,7 +508,22 @@ def get_size(self) -> int: """Size of this component in bytes""" raise NotImplementedError() - def print(self) -> None: + def as_json(self, full: bool = False) -> str: + """Return a JSON representation of the component + Args + ---- + full : bool + Include additional details such as offset and length + """ + return json.dumps(self, indent=2, cls=_JsonEncoder, full_details=full) + + def as_text(self) -> str: + """Return a text representation of the component""" + buf = io.StringIO() + self.print(file=buf) + return buf.getvalue() + + def print(self, *, file=None) -> None: """Print information about the component to stdout""" raise NotImplementedError() @@ -713,9 +730,10 @@ def _dump_impl(self, fd: BinaryFile_RW) -> int: def get_size(self) -> int: return self.size - def print(self) -> None: + def print(self, *, file=None) -> None: print( - f"{self.name:15}{self.size:11} @ {self.get_offset():11} {self.encoded_value!r}" + f"{self.name:15}{self.size:11} @ {self.get_offset():11} {self.encoded_value!r}", + file=file, ) @@ -755,8 +773,10 @@ def _dump_impl(self, fd: BinaryFile_RW) -> int: def get_size(self) -> int: return self.size - def print(self) -> None: - print(f"{self.name:15}{self.size:11} @ {self.get_offset():11} ") + def print(self, *, file=None) -> None: + print( + f"{self.name:15}{self.size:11} @ {self.get_offset():11} ", file=file + ) class ComponentCollection(JbpIOComponent): @@ -826,9 +846,9 @@ def get_offset_of(self, child_obj: JbpIOComponent) -> int: else: raise ValueError(f"Could not find {child_obj.name}") - def print(self) -> None: + def print(self, *, file=None) -> None: for child in self._children: - child.print() + child.print(file=file) def finalize(self): for child in self._children: @@ -894,10 +914,6 @@ def _remove_all(self, pattern: str) -> None: def _index(self, name: str) -> int: return self._child_names().index(name) - def print(self) -> None: - for child in self._children: - child.print() - class SegmentList(ComponentCollection, collections.abc.Sequence): """A sequence of JBP segments""" @@ -2320,9 +2336,9 @@ def __init__(self, name: str, data_size: int = 1): self._append(ImageSubheader("subheader")) self._append(BinaryPlaceholder("Data", data_size)) - def print(self) -> None: - print(f"# ImageSegment {self.name}") - super().print() + def print(self, *, file=None) -> None: + print(f"# ImageSegment {self.name}", file=file) + super().print(file=file) class GraphicSubheader(Group): @@ -2528,9 +2544,9 @@ def __init__(self, name: str, data_size: int = 1): self._append(GraphicSubheader("subheader")) self._append(BinaryPlaceholder("Data", data_size)) - def print(self) -> None: - print(f"# GraphicSegment {self.name}") - super().print() + def print(self, *, file=None) -> None: + print(f"# GraphicSegment {self.name}", file=file) + super().print(file=file) class TextSubheader(Group): @@ -2673,9 +2689,9 @@ def __init__(self, name: str, data_size: int = 1): self._append(TextSubheader("subheader")) self._append(BinaryPlaceholder("Data", data_size)) - def print(self) -> None: - print(f"# TextSegment {self.name}") - super().print() + def print(self, *, file=None) -> None: + print(f"# TextSegment {self.name}", file=file) + super().print(file=file) class ReservedExtensionSegment(Group): @@ -2692,9 +2708,9 @@ def __init__(self, name: str, subheader_size: int = LRESH_MIN, data_size: int = ) self._append(BinaryPlaceholder("RESDATA", data_size)) - def print(self) -> None: - print(f"# ReservedExtensionSegment {self.name}") - super().print() + def print(self, *, file=None) -> None: + print(f"# ReservedExtensionSegment {self.name}", file=file) + super().print(file=file) class DataExtensionSubheader(Group): @@ -2924,9 +2940,9 @@ def _load_impl(self, fd): fd.seek(self.get_offset()) super()._load_impl(fd) - def print(self) -> None: - print(f"# DESegment {self.name}") - super().print() + def print(self, *, file=None) -> None: + print(f"# DESegment {self.name}", file=file) + super().print(file=file) def _update_tre_lengths(header, hdl, ofl, hd): @@ -3538,3 +3554,36 @@ def tre_factory(tretag: str) -> Tre: return tres[tretag]() return UnknownTre(tretag) + + +class _JsonEncoder(json.JSONEncoder): + def __init__(self, *args, full_details=False, **kwargs): + super().__init__(*args, **kwargs) + self.full_details = full_details + + def default(self, obj): + if isinstance(obj, collections.abc.Mapping): + return dict(obj) + if isinstance(obj, bytes): + return list(obj) + if isinstance(obj, Field): + if self.full_details: + return { + "size": obj.size, + "offset": obj.get_offset(), + "value": obj.value, + } + return obj.value + if isinstance(obj, BinaryPlaceholder): + if self.full_details: + return { + "size": obj.size, + "offset": obj.get_offset(), + "value": "__binary__", + } + return f"__binary__ ({obj.get_size()} bytes)" + if isinstance(obj, SegmentList): + return list(obj) + if isinstance(obj, TreSequence): + return list(obj) + return super().default(obj) diff --git a/test/test_core.py b/test/test_core.py index 5f1619f..1c9b7a0 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -1,6 +1,7 @@ import datetime import filecmp import io +import json import logging import os import pathlib @@ -324,9 +325,20 @@ def test_fileheader(capsys): ntf.finalize() ntf.print() captured = capsys.readouterr() - assert "GraphicSegment" in captured.out - assert "TextSegment" in captured.out - assert "ReservedExtensionSegment" in captured.out + assert "GraphicSegment 1" in captured.out + assert "TextSegment 1" in captured.out + assert "ReservedExtensionSegment 1" in captured.out + + buf = io.StringIO() + ntf.print(file=buf) + assert captured.out == buf.getvalue() + + txt = ntf.as_text() + assert "GraphicSegment 1" in txt + assert "TextSegment 1" in txt + assert "ReservedExtensionSegment 1" in txt + + assert "GraphicSegments" in json.loads(ntf.as_json()) def test_imseg(): diff --git a/test/test_jbpy.py b/test/test_jbpy.py index 14b6265..e8ce2a5 100644 --- a/test/test_jbpy.py +++ b/test/test_jbpy.py @@ -1,4 +1,5 @@ import io +import json import os import pytest @@ -17,6 +18,12 @@ def test_roundtrip_jitc_quicklook(filename, tmp_path): with filename.open("rb") as file: ntf.load(file) + txt = ntf.as_text() + assert "FHDR" in txt + assert "@" in txt + + assert "FileHeader" in json.loads(ntf.as_json()) + copy_filename = tmp_path / "copy.nitf" with copy_filename.open("wb") as fd: ntf.dump(fd)