imprinted 0.7.0__py3-none-any.whl
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.
- imprinted/__init__.py +59 -0
- imprinted/core.py +1357 -0
- imprinted-0.7.0.dist-info/METADATA +171 -0
- imprinted-0.7.0.dist-info/RECORD +7 -0
- imprinted-0.7.0.dist-info/WHEEL +5 -0
- imprinted-0.7.0.dist-info/licenses/LICENSE +21 -0
- imprinted-0.7.0.dist-info/top_level.txt +1 -0
imprinted/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Imprinted: hide a short payload in a picture, read it back from the file or a photo of a print.
|
|
2
|
+
|
|
3
|
+
import imprinted
|
|
4
|
+
imprinted.encode("photo.png", "hello", "photo_imprinted.png")
|
|
5
|
+
imprinted.decode("phone_shot.jpg") # -> "hello", or None
|
|
6
|
+
|
|
7
|
+
`imprinted.core` is the full codec (encode stats, every Imprinted found in a photo, the strength ladder).
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
from PIL import Image, ImageOps
|
|
15
|
+
|
|
16
|
+
from . import core
|
|
17
|
+
|
|
18
|
+
__version__ = "0.7.0"
|
|
19
|
+
__all__ = ["encode", "decode", "core"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _load(image) -> np.ndarray:
|
|
23
|
+
"""Path, PIL image or uint8 array (RGB, RGBA or grayscale) -> uint8 array, EXIF rotation applied."""
|
|
24
|
+
if isinstance(image, np.ndarray):
|
|
25
|
+
return image
|
|
26
|
+
if isinstance(image, (str, os.PathLike)):
|
|
27
|
+
image = Image.open(image)
|
|
28
|
+
image = ImageOps.exif_transpose(image)
|
|
29
|
+
if image.mode not in ("L", "RGB", "RGBA"):
|
|
30
|
+
image = image.convert("RGBA" if "A" in image.getbands() or "transparency" in image.info else "RGB")
|
|
31
|
+
return np.asarray(image)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def encode(image, payload: str | bytes, out=None, *, print_width_cm: float = 15.0,
|
|
35
|
+
strength: float | None = None) -> Image.Image:
|
|
36
|
+
"""Hide payload (1..32 bytes; a str is UTF-8 encoded) in image (a path, PIL image or RGB uint8
|
|
37
|
+
array). Returns the encoded picture (same size and mode) and saves it to out when given; save
|
|
38
|
+
it lossless (PNG). print_width_cm: how wide it will be printed or shown, which sets the pattern
|
|
39
|
+
size. strength: None takes the strongest within the visibility budget (SSIM >= 0.955, mean
|
|
40
|
+
dL* <= 0.99). ValueError when the picture is too small or too clipped to carry the payload."""
|
|
41
|
+
rgb = _load(image)
|
|
42
|
+
if isinstance(payload, str):
|
|
43
|
+
payload = payload.encode()
|
|
44
|
+
b = core.subblock_for_print(rgb.shape[1], print_width_cm * 10)
|
|
45
|
+
enc, _ = core.encode(rgb, payload, delta=strength, subblock_px=b)
|
|
46
|
+
img = Image.fromarray(enc)
|
|
47
|
+
if out is not None:
|
|
48
|
+
img.save(out)
|
|
49
|
+
return img
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def decode(image, raw: bool = False) -> str | bytes | None:
|
|
53
|
+
"""First payload found in image (a file, screenshot or photo: path, PIL image or RGB uint8
|
|
54
|
+
array), or None. A str (UTF-8) unless raw, then bytes."""
|
|
55
|
+
found = core.decode(_load(image))["found"]
|
|
56
|
+
if not found:
|
|
57
|
+
return None
|
|
58
|
+
payload = found[0]["payload"]
|
|
59
|
+
return payload if raw else payload.decode("utf-8", "replace")
|