Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 1 addition & 36 deletions jbpy/_jbpinfo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import argparse
import collections.abc
import json
import os
import sys

Expand All @@ -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")
Expand All @@ -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:
Expand Down
101 changes: 75 additions & 26 deletions jbpy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import copy
import datetime
import importlib.metadata
import io
import json
import logging
import os
import re
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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,
)


Expand Down Expand Up @@ -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} <Binary>")
def print(self, *, file=None) -> None:
print(
f"{self.name:15}{self.size:11} @ {self.get_offset():11} <Binary>", file=file
)


class ComponentCollection(JbpIOComponent):
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"""
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
18 changes: 15 additions & 3 deletions test/test_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import filecmp
import io
import json
import logging
import os
import pathlib
Expand Down Expand Up @@ -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():
Expand Down
7 changes: 7 additions & 0 deletions test/test_jbpy.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import json
import os

import pytest
Expand All @@ -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)
Expand Down