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,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
|
+

|
|
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
|
+

|
|
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"]
|