swarmauri_xmp_jpeg 0.1.1.dev31__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ This project is licensed under the Apache License 2.0.
2
+ See the root LICENSE file of the swarmauri-sdk repository.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: swarmauri_xmp_jpeg
3
+ Version: 0.1.1.dev31
4
+ Summary: JPEG handler for embedding and extracting XMP packets in Swarmauri runtimes.
5
+ License-Expression: Apache-2.0
6
+ License-File: LICENSE
7
+ Keywords: swarmauri,sdk,standards,xmp,jpeg
8
+ Author: Jacob Stewart
9
+ Author-email: jacob@swarmauri.com
10
+ Requires-Python: >=3.10,<3.13
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Development Status :: 3 - Alpha
18
+ Classifier: Programming Language :: Python
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Requires-Dist: swarmauri_base
22
+ Requires-Dist: swarmauri_core
23
+ Project-URL: Homepage, https://github.com/swarmauri/swarmauri-sdk
24
+ Description-Content-Type: text/markdown
25
+
26
+ ![Swarmauri Logo](https://github.com/swarmauri/swarmauri-sdk/blob/3d4d1cfa949399d7019ae9d8f296afba773dfb7f/assets/swarmauri.brand.theme.svg)
27
+
28
+
29
+ <p align="center">
30
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
31
+ <img src="https://img.shields.io/pypi/dm/swarmauri_xmp_jpeg" alt="PyPI - Downloads"/></a>
32
+ <a href="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_xmp_jpeg/">
33
+ <img alt="Hits" src="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_xmp_jpeg.svg"/></a>
34
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
35
+ <img src="https://img.shields.io/pypi/pyversions/swarmauri_xmp_jpeg" alt="PyPI - Python Version"/></a>
36
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
37
+ <img src="https://img.shields.io/pypi/l/swarmauri_xmp_jpeg" alt="PyPI - License"/></a>
38
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
39
+ <img src="https://img.shields.io/pypi/v/swarmauri_xmp_jpeg?label=swarmauri_xmp_jpeg&color=green" alt="PyPI - swarmauri_xmp_jpeg"/></a>
40
+ </p>
41
+
42
+ ---
43
+
44
+ # swarmauri_xmp_jpeg
45
+
46
+ `swarmauri_xmp_jpeg` ships the `JPEGXMP` handler for embedding, reading, and removing XMP packets from JPEGs via APP1 segments that follow Adobe's namespace header.
47
+
48
+ ## Features
49
+
50
+ - **Immediate discovery** – derives from `EmbedXmpBase` so dynamic registry consumers can import it automatically.
51
+ - **Header accuracy** – produces APP1 payloads prefixed with `http://ns.adobe.com/xap/1.0/\x00` as required by the specification.
52
+ - **Defensive parsing** – iterates markers safely and halts at SOS to avoid corrupting scan data.
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ # pip
58
+ pip install swarmauri_xmp_jpeg
59
+
60
+ # uv
61
+ uv add swarmauri_xmp_jpeg
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ```python
67
+ from pathlib import Path
68
+
69
+ from swarmauri_xmp_jpeg import JPEGXMP
70
+
71
+ handler = JPEGXMP()
72
+ photo_path = Path("example.jpg")
73
+ xmp_packet = """<x:xmpmeta xmlns:x='adobe:ns:meta/'><rdf:RDF>...</rdf:RDF></x:xmpmeta>"""
74
+
75
+ # Insert the packet right after the SOI marker
76
+ updated_bytes = handler.write_xmp(photo_path.read_bytes(), xmp_packet)
77
+ photo_path.write_bytes(updated_bytes)
78
+
79
+ # Confirm it can be recovered later
80
+ restored_xml = handler.read_xmp(updated_bytes)
81
+ print(restored_xml)
82
+
83
+ # Strip the packet if needed
84
+ clean_bytes = handler.remove_xmp(updated_bytes)
85
+ ```
86
+
87
+ ### Why it works
88
+
89
+ - **APP1 discipline** – the implementation builds a compliant APP1 segment with the canonical XMP namespace string.
90
+ - **Dynamic registration** – inheriting from `EmbedXmpBase` means the handler is registered automatically for plugin discovery.
91
+ - **Robust parsing** – helper iterators validate segment structure, stopping safely at SOS or malformed markers.
92
+
93
+ ## Project Resources
94
+
95
+ - Source: <https://github.com/swarmauri/swarmauri-sdk>
96
+ - License: Apache 2.0
97
+
@@ -0,0 +1,71 @@
1
+ ![Swarmauri Logo](https://github.com/swarmauri/swarmauri-sdk/blob/3d4d1cfa949399d7019ae9d8f296afba773dfb7f/assets/swarmauri.brand.theme.svg)
2
+
3
+
4
+ <p align="center">
5
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
6
+ <img src="https://img.shields.io/pypi/dm/swarmauri_xmp_jpeg" alt="PyPI - Downloads"/></a>
7
+ <a href="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_xmp_jpeg/">
8
+ <img alt="Hits" src="https://hits.sh/github.com/swarmauri/swarmauri-sdk/tree/master/pkgs/standards/swarmauri_xmp_jpeg.svg"/></a>
9
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
10
+ <img src="https://img.shields.io/pypi/pyversions/swarmauri_xmp_jpeg" alt="PyPI - Python Version"/></a>
11
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
12
+ <img src="https://img.shields.io/pypi/l/swarmauri_xmp_jpeg" alt="PyPI - License"/></a>
13
+ <a href="https://pypi.org/project/swarmauri_xmp_jpeg/">
14
+ <img src="https://img.shields.io/pypi/v/swarmauri_xmp_jpeg?label=swarmauri_xmp_jpeg&color=green" alt="PyPI - swarmauri_xmp_jpeg"/></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ # swarmauri_xmp_jpeg
20
+
21
+ `swarmauri_xmp_jpeg` ships the `JPEGXMP` handler for embedding, reading, and removing XMP packets from JPEGs via APP1 segments that follow Adobe's namespace header.
22
+
23
+ ## Features
24
+
25
+ - **Immediate discovery** – derives from `EmbedXmpBase` so dynamic registry consumers can import it automatically.
26
+ - **Header accuracy** – produces APP1 payloads prefixed with `http://ns.adobe.com/xap/1.0/\x00` as required by the specification.
27
+ - **Defensive parsing** – iterates markers safely and halts at SOS to avoid corrupting scan data.
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ # pip
33
+ pip install swarmauri_xmp_jpeg
34
+
35
+ # uv
36
+ uv add swarmauri_xmp_jpeg
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```python
42
+ from pathlib import Path
43
+
44
+ from swarmauri_xmp_jpeg import JPEGXMP
45
+
46
+ handler = JPEGXMP()
47
+ photo_path = Path("example.jpg")
48
+ xmp_packet = """<x:xmpmeta xmlns:x='adobe:ns:meta/'><rdf:RDF>...</rdf:RDF></x:xmpmeta>"""
49
+
50
+ # Insert the packet right after the SOI marker
51
+ updated_bytes = handler.write_xmp(photo_path.read_bytes(), xmp_packet)
52
+ photo_path.write_bytes(updated_bytes)
53
+
54
+ # Confirm it can be recovered later
55
+ restored_xml = handler.read_xmp(updated_bytes)
56
+ print(restored_xml)
57
+
58
+ # Strip the packet if needed
59
+ clean_bytes = handler.remove_xmp(updated_bytes)
60
+ ```
61
+
62
+ ### Why it works
63
+
64
+ - **APP1 discipline** – the implementation builds a compliant APP1 segment with the canonical XMP namespace string.
65
+ - **Dynamic registration** – inheriting from `EmbedXmpBase` means the handler is registered automatically for plugin discovery.
66
+ - **Robust parsing** – helper iterators validate segment structure, stopping safely at SOS or malformed markers.
67
+
68
+ ## Project Resources
69
+
70
+ - Source: <https://github.com/swarmauri/swarmauri-sdk>
71
+ - License: Apache 2.0
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "swarmauri_xmp_jpeg"
3
+ version = "0.1.1.dev31"
4
+ description = "JPEG handler for embedding and extracting XMP packets in Swarmauri runtimes."
5
+ license = "Apache-2.0"
6
+ readme = "README.md"
7
+ requires-python = ">=3.10,<3.13"
8
+ classifiers = [
9
+ "License :: OSI Approved :: Apache Software License",
10
+ "Natural Language :: English",
11
+ "Programming Language :: Python :: 3.10",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Development Status :: 3 - Alpha",
16
+ "Programming Language :: Python",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ ]
20
+ authors = [{ name = "Jacob Stewart", email = "jacob@swarmauri.com" }]
21
+ dependencies = [
22
+ "swarmauri_core",
23
+ "swarmauri_base",
24
+ ]
25
+ keywords = [
26
+ 'swarmauri',
27
+ 'sdk',
28
+ 'standards',
29
+ 'xmp',
30
+ 'jpeg',
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/swarmauri/swarmauri-sdk"
35
+
36
+ [project.entry-points.'swarmauri.xmp_handlers']
37
+ JPEGXMP = "swarmauri_xmp_jpeg:JPEGXMP"
38
+
39
+ [tool.uv.sources]
40
+ swarmauri_core = { workspace = true }
41
+ swarmauri_base = { workspace = true }
42
+
43
+ [build-system]
44
+ requires = ["poetry-core>=1.0.0"]
45
+ build-backend = "poetry.core.masonry.api"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "pytest>=8.0",
50
+ "ruff>=0.9",
51
+ ]
@@ -0,0 +1,108 @@
1
+ """JPEG APP1 XMP handler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ from typing import ClassVar, Iterator, Tuple
7
+
8
+ from swarmauri_base import register_type
9
+ from swarmauri_base.xmp import EmbedXmpBase
10
+
11
+
12
+ @register_type(resource_type=EmbedXmpBase)
13
+ class JPEGXMP(EmbedXmpBase):
14
+ """Embed XMP packets inside JPEG APP1 segments."""
15
+
16
+ SOI: ClassVar[bytes] = b"\xff\xd8"
17
+ EOI: ClassVar[bytes] = b"\xff\xd9"
18
+ SOS: ClassVar[int] = 0xDA
19
+ APP1: ClassVar[int] = 0xE1
20
+ XMP_HDR: ClassVar[bytes] = b"http://ns.adobe.com/xap/1.0/\x00"
21
+
22
+ def supports(self, header: bytes, path: str) -> bool:
23
+ return header.startswith(self.SOI) or path.lower().endswith((".jpg", ".jpeg"))
24
+
25
+ def _iter_segments(self, data: bytes) -> Iterator[Tuple[int, int, int, int]]:
26
+ if not data.startswith(self.SOI):
27
+ raise ValueError("Not a JPEG payload")
28
+ n = len(data)
29
+ i = 2
30
+ yield 0, 0xD8, 0, 0
31
+ while i < n:
32
+ if data[i] != 0xFF:
33
+ return
34
+ j = i
35
+ while j < n and data[j] == 0xFF:
36
+ j += 1
37
+ if j >= n:
38
+ return
39
+ marker = data[j]
40
+ i = j + 1
41
+ if marker == self.SOS:
42
+ if i + 2 > n:
43
+ raise ValueError("Truncated SOS segment")
44
+ seglen = int.from_bytes(data[i : i + 2], "big")
45
+ yield j - 1, marker, i, seglen - 2
46
+ return
47
+ if i + 2 > n:
48
+ return
49
+ seglen = int.from_bytes(data[i : i + 2], "big")
50
+ if seglen < 2 or i + seglen > n:
51
+ raise ValueError("Invalid JPEG segment length")
52
+ yield j - 1, marker, i, seglen - 2
53
+ i += seglen
54
+
55
+ def read_xmp(self, data: bytes) -> str | None:
56
+ for _m_off, marker, p_off, p_len in self._iter_segments(data):
57
+ if marker != self.APP1:
58
+ continue
59
+ payload = data[p_off + 2 : p_off + 2 + p_len]
60
+ if payload.startswith(self.XMP_HDR):
61
+ return payload[len(self.XMP_HDR) :].decode("utf-8", errors="ignore")
62
+ return None
63
+
64
+ def _build_app1(self, xmp_utf8: bytes) -> bytes:
65
+ payload = self.XMP_HDR + xmp_utf8
66
+ seglen = 2 + len(payload)
67
+ return b"\xff" + bytes([self.APP1]) + seglen.to_bytes(2, "big") + payload
68
+
69
+ def write_xmp(self, data: bytes, xmp_xml: str) -> bytes:
70
+ xmp_bytes = self._ensure_xml(xmp_xml)
71
+ if not data.startswith(self.SOI):
72
+ raise ValueError("Not a JPEG payload")
73
+ out = io.BytesIO()
74
+ out.write(self.SOI)
75
+ inserted = False
76
+ for _m_off, marker, p_off, p_len in self._iter_segments(data):
77
+ if marker == 0xD8:
78
+ continue
79
+ if not inserted:
80
+ out.write(self._build_app1(xmp_bytes))
81
+ inserted = True
82
+ if marker == self.APP1:
83
+ payload = data[p_off + 2 : p_off + 2 + p_len]
84
+ if payload.startswith(self.XMP_HDR):
85
+ continue
86
+ out.write(b"\xff" + bytes([marker]) + data[p_off : p_off + 2 + p_len])
87
+ if not inserted:
88
+ out.write(self._build_app1(xmp_bytes))
89
+ out.write(data[2:])
90
+ return out.getvalue()
91
+
92
+ def remove_xmp(self, data: bytes) -> bytes:
93
+ if not data.startswith(self.SOI):
94
+ raise ValueError("Not a JPEG payload")
95
+ out = io.BytesIO()
96
+ out.write(self.SOI)
97
+ for _m_off, marker, p_off, p_len in self._iter_segments(data):
98
+ if marker == 0xD8:
99
+ continue
100
+ if marker == self.APP1:
101
+ payload = data[p_off + 2 : p_off + 2 + p_len]
102
+ if payload.startswith(self.XMP_HDR):
103
+ continue
104
+ out.write(b"\xff" + bytes([marker]) + data[p_off : p_off + 2 + p_len])
105
+ return out.getvalue()
106
+
107
+
108
+ __all__ = ["JPEGXMP"]