Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,121 +1,116 @@
|
|
1 |
-
#!/usr/bin/env python
|
2 |
-
"""
|
3 |
-
Kovaaks PNG Metadata Cloner β v2.1
|
4 |
-
β’ strict ancillary-chunk order
|
5 |
-
β’ duplicates removed
|
6 |
-
β’ optional auto-resize / RGBA convert to 50 Γ 50
|
7 |
-
β’ file-size warning
|
8 |
-
"""
|
9 |
-
|
10 |
-
from __future__ import annotations
|
11 |
-
|
12 |
import os
|
13 |
import struct
|
14 |
import tempfile
|
15 |
-
from pathlib import Path
|
16 |
from typing import BinaryIO, Iterator
|
17 |
|
18 |
import gradio as gr
|
19 |
-
from PIL import Image
|
20 |
|
21 |
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
22 |
|
23 |
-
|
24 |
-
|
25 |
-
b"sRGB",
|
26 |
-
b"gAMA",
|
27 |
-
b"pHYs",
|
28 |
-
b"eXIf", # usually empty
|
29 |
-
b"iTXt",
|
30 |
-
b"tEXt",
|
31 |
-
b"zTXt",
|
32 |
-
)
|
33 |
-
|
34 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββ helpers ββ
|
35 |
def _read_chunks(fp: BinaryIO) -> Iterator[tuple[bytes, bytes]]:
|
36 |
"""
|
37 |
Yield (raw_chunk_bytes, chunk_type_bytes) for every chunk in *fp*.
|
38 |
-
|
|
|
|
|
39 |
"""
|
40 |
while True:
|
41 |
length_bytes = fp.read(4)
|
42 |
if not length_bytes:
|
43 |
-
break
|
44 |
if len(length_bytes) != 4:
|
45 |
-
raise ValueError("Corrupted PNG:
|
46 |
|
47 |
-
length = struct.unpack(">I", length_bytes)[0]
|
48 |
chunk_type = fp.read(4)
|
49 |
-
data
|
50 |
-
crc
|
51 |
|
52 |
if len(chunk_type) != 4 or len(data) != length or len(crc) != 4:
|
53 |
-
raise ValueError("Corrupted PNG: truncated chunk")
|
54 |
|
55 |
yield length_bytes + chunk_type + data + crc, chunk_type
|
56 |
|
57 |
|
58 |
-
def
|
59 |
-
"""
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
return [seen[tag] for tag in ANCILLARY_ORDER if tag in seen]
|
65 |
-
|
66 |
-
|
67 |
-
def _validate_or_resize(path: str, auto_fix: bool) -> None:
|
68 |
-
"""Ensure 50 Γ 50 RGBA; optionally fix in-place."""
|
69 |
-
img = Image.open(path)
|
70 |
-
if img.size == (50, 50) and img.mode == "RGBA":
|
71 |
-
return
|
72 |
-
|
73 |
-
if not auto_fix:
|
74 |
-
raise ValueError("Target must be 50Γ50 px, RGBA (enable auto-fix).")
|
75 |
-
|
76 |
-
img = img.convert("RGBA").resize((50, 50), Image.LANCZOS)
|
77 |
-
img.save(path, optimize=True)
|
78 |
-
|
79 |
-
|
80 |
-
def clone_metadata(
|
81 |
-
src_path: str, tgt_path: str, out_path: str, *, auto_fix: bool = True
|
82 |
-
) -> str:
|
83 |
-
"""Clone cleaned ancillary chunks from src β tgt and save out_path."""
|
84 |
with open(src_path, "rb") as fs, open(tgt_path, "rb") as ft:
|
85 |
if fs.read(8) != PNG_SIGNATURE or ft.read(8) != PNG_SIGNATURE:
|
86 |
-
raise ValueError("Both
|
87 |
|
88 |
src_chunks = list(_read_chunks(fs))
|
89 |
tgt_chunks = list(_read_chunks(ft))
|
90 |
|
91 |
-
|
92 |
-
|
|
|
|
|
|
|
93 |
|
94 |
-
|
95 |
-
inserted = False
|
96 |
for raw, tp in tgt_chunks:
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
rebuilt.extend(ancillary)
|
101 |
-
inserted = True
|
102 |
-
continue
|
103 |
-
if tp in replace_tags:
|
104 |
-
continue # skip duplicate already provided
|
105 |
-
rebuilt.append(raw)
|
106 |
|
107 |
with open(out_path, "wb") as fo:
|
108 |
fo.write(PNG_SIGNATURE)
|
109 |
-
for chunk in
|
110 |
fo.write(chunk)
|
111 |
|
112 |
-
if os.path.getsize(out_path) > 256:
|
113 |
-
print("β οΈ Output larger than 256 B β Kovaaks usually still loads it.")
|
114 |
-
|
115 |
-
_validate_or_resize(out_path, auto_fix=False) # ensure still valid
|
116 |
return out_path
|
117 |
|
118 |
|
119 |
-
#
|
120 |
-
def process(src_file
|
121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
import struct
|
3 |
import tempfile
|
|
|
4 |
from typing import BinaryIO, Iterator
|
5 |
|
6 |
import gradio as gr
|
|
|
7 |
|
8 |
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
|
9 |
|
10 |
+
|
11 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ helpers ββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
def _read_chunks(fp: BinaryIO) -> Iterator[tuple[bytes, bytes]]:
|
13 |
"""
|
14 |
Yield (raw_chunk_bytes, chunk_type_bytes) for every chunk in *fp*.
|
15 |
+
`raw_chunk_bytes` includes length, type, data and CRC exactly as in file,
|
16 |
+
so we can copy them verbatim later.
|
17 |
+
The PNG signature must already be consumed by the caller.
|
18 |
"""
|
19 |
while True:
|
20 |
length_bytes = fp.read(4)
|
21 |
if not length_bytes:
|
22 |
+
break # EOF
|
23 |
if len(length_bytes) != 4:
|
24 |
+
raise ValueError("Corrupted PNG: unexpected EOF while reading chunk length.")
|
25 |
|
26 |
+
length = struct.unpack(">I", length_bytes)[0] # β fixed (no stray space)
|
27 |
chunk_type = fp.read(4)
|
28 |
+
data = fp.read(length)
|
29 |
+
crc = fp.read(4)
|
30 |
|
31 |
if len(chunk_type) != 4 or len(data) != length or len(crc) != 4:
|
32 |
+
raise ValueError("Corrupted PNG: truncated chunk.")
|
33 |
|
34 |
yield length_bytes + chunk_type + data + crc, chunk_type
|
35 |
|
36 |
|
37 |
+
def clone_metadata(src_path: str, tgt_path: str, out_path: str) -> str:
|
38 |
+
"""
|
39 |
+
Copy all ancillary chunks (anything except IHDR / IDAT / IEND) from *src_path*
|
40 |
+
into *tgt_path* immediately after its IHDR, then save to *out_path*.
|
41 |
+
Pixel data of the target stays untouched.
|
42 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
with open(src_path, "rb") as fs, open(tgt_path, "rb") as ft:
|
44 |
if fs.read(8) != PNG_SIGNATURE or ft.read(8) != PNG_SIGNATURE:
|
45 |
+
raise ValueError("Both files must be valid PNG images.")
|
46 |
|
47 |
src_chunks = list(_read_chunks(fs))
|
48 |
tgt_chunks = list(_read_chunks(ft))
|
49 |
|
50 |
+
# take every non-critical chunk from source
|
51 |
+
ancillary = [
|
52 |
+
raw for raw, tp in src_chunks
|
53 |
+
if tp not in (b"IHDR", b"IDAT", b"IEND")
|
54 |
+
]
|
55 |
|
56 |
+
out_chunks: list[bytes] = []
|
|
|
57 |
for raw, tp in tgt_chunks:
|
58 |
+
out_chunks.append(raw)
|
59 |
+
if tp == b"IHDR": # inject right after IHDR
|
60 |
+
out_chunks.extend(ancillary)
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
|
62 |
with open(out_path, "wb") as fo:
|
63 |
fo.write(PNG_SIGNATURE)
|
64 |
+
for chunk in out_chunks:
|
65 |
fo.write(chunk)
|
66 |
|
|
|
|
|
|
|
|
|
67 |
return out_path
|
68 |
|
69 |
|
70 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ gradio ui ββ
|
71 |
+
def process(src_file, tgt_file):
|
72 |
+
"""Gradio wrapper β returns path to the merged PNG."""
|
73 |
+
if not src_file or not tgt_file:
|
74 |
+
raise gr.Error("Please upload both images.")
|
75 |
+
|
76 |
+
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
|
77 |
+
tmp.close() # we only need the path
|
78 |
+
try:
|
79 |
+
clone_metadata(src_file, tgt_file, tmp.name)
|
80 |
+
except Exception as e:
|
81 |
+
os.remove(tmp.name)
|
82 |
+
raise gr.Error(str(e))
|
83 |
+
|
84 |
+
return tmp.name
|
85 |
+
|
86 |
+
|
87 |
+
with gr.Blocks(title="Kovaaks PNG Metadata Cloner") as demo:
|
88 |
+
gr.Markdown(
|
89 |
+
"""
|
90 |
+
## Kovaaks PNG Metadata Cloner πΉ
|
91 |
+
|
92 |
+
**Step 1.** Upload a *metadata source* PNG that already works in-game
|
93 |
+
**Step 2.** Upload your *target* PNG (new 50 Γ 50 crosshair)
|
94 |
+
**Step 3.** Click **Clone metadata** and download the result.
|
95 |
+
|
96 |
+
Put the output file into
|
97 |
+
`%LOCALAPPDATA%\\FPSAimTrainer\\Saved\\MyImport\\crosshairs`
|
98 |
+
"""
|
99 |
+
)
|
100 |
+
|
101 |
+
with gr.Row():
|
102 |
+
src = gr.File(label="Image 1 β source (metadata)", type="filepath")
|
103 |
+
tgt = gr.File(label="Image 2 β target (pixels)", type="filepath")
|
104 |
+
|
105 |
+
out = gr.File(label="Output PNG", interactive=False)
|
106 |
+
btn = gr.Button("Clone metadata β")
|
107 |
+
|
108 |
+
btn.click(fn=process, inputs=[src, tgt], outputs=[out])
|
109 |
+
|
110 |
+
gr.Markdown(
|
111 |
+
"βοΈ Ancillary chunks (`sRGB`, `gAMA`, `pHYs`, `eXIf`, `tEXt`, β¦) are copied verbatim. "
|
112 |
+
"Pixel data from the target image is left untouched."
|
113 |
+
)
|
114 |
+
|
115 |
+
if __name__ == "__main__":
|
116 |
+
demo.launch(show_api=False, share=False)
|