Spaces:
Sleeping
Sleeping
import os | |
import struct | |
import tempfile | |
from typing import BinaryIO, Iterator | |
import gradio as gr | |
PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ helpers ββ | |
def _read_chunks(fp: BinaryIO) -> Iterator[tuple[bytes, bytes]]: | |
""" | |
Yield (raw_chunk_bytes, chunk_type_bytes) for every chunk in *fp*. | |
`raw_chunk_bytes` includes length, type, data and CRC exactly as in file, | |
so we can copy them verbatim later. | |
The PNG signature must already be consumed by the caller. | |
""" | |
while True: | |
length_bytes = fp.read(4) | |
if not length_bytes: | |
break # EOF | |
if len(length_bytes) != 4: | |
raise ValueError("Corrupted PNG: unexpected EOF while reading chunk length.") | |
length = struct.unpack(">I", length_bytes)[0] # β fixed (no stray space) | |
chunk_type = fp.read(4) | |
data = fp.read(length) | |
crc = fp.read(4) | |
if len(chunk_type) != 4 or len(data) != length or len(crc) != 4: | |
raise ValueError("Corrupted PNG: truncated chunk.") | |
yield length_bytes + chunk_type + data + crc, chunk_type | |
def clone_metadata(src_path: str, tgt_path: str, out_path: str) -> str: | |
""" | |
Copy all ancillary chunks (anything except IHDR / IDAT / IEND) from *src_path* | |
into *tgt_path* immediately after its IHDR, then save to *out_path*. | |
Pixel data of the target stays untouched. | |
""" | |
with open(src_path, "rb") as fs, open(tgt_path, "rb") as ft: | |
if fs.read(8) != PNG_SIGNATURE or ft.read(8) != PNG_SIGNATURE: | |
raise ValueError("Both files must be valid PNG images.") | |
src_chunks = list(_read_chunks(fs)) | |
tgt_chunks = list(_read_chunks(ft)) | |
# take every non-critical chunk from source | |
ancillary = [ | |
raw for raw, tp in src_chunks | |
if tp not in (b"IHDR", b"IDAT", b"IEND") | |
] | |
out_chunks: list[bytes] = [] | |
for raw, tp in tgt_chunks: | |
out_chunks.append(raw) | |
if tp == b"IHDR": # inject right after IHDR | |
out_chunks.extend(ancillary) | |
with open(out_path, "wb") as fo: | |
fo.write(PNG_SIGNATURE) | |
for chunk in out_chunks: | |
fo.write(chunk) | |
return out_path | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ gradio ui ββ | |
def process(src_file, tgt_file): | |
"""Gradio wrapper β returns path to the merged PNG.""" | |
if not src_file or not tgt_file: | |
raise gr.Error("Please upload both images.") | |
tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False) | |
tmp.close() # we only need the path | |
try: | |
clone_metadata(src_file, tgt_file, tmp.name) | |
except Exception as e: | |
os.remove(tmp.name) | |
raise gr.Error(str(e)) | |
return tmp.name | |
with gr.Blocks(title="Kovaaks PNG Metadata Cloner") as demo: | |
gr.Markdown( | |
""" | |
## Kovaaks PNG Metadata Cloner πΉ | |
**Step 1.** Upload a *metadata source* PNG that already works in-game | |
**Step 2.** Upload your *target* PNG (new 50 Γ 50 crosshair) | |
**Step 3.** Click **Clone metadata** and download the result. | |
Put the output file into | |
`%LOCALAPPDATA%\\FPSAimTrainer\\Saved\\MyImport\\crosshairs` | |
""" | |
) | |
with gr.Row(): | |
src = gr.File(label="Image 1 β source (metadata)", type="filepath") | |
tgt = gr.File(label="Image 2 β target (pixels)", type="filepath") | |
out = gr.File(label="Output PNG", interactive=False) | |
btn = gr.Button("Clone metadata β") | |
btn.click(fn=process, inputs=[src, tgt], outputs=[out]) | |
gr.Markdown( | |
"βοΈ Ancillary chunks (`sRGB`, `gAMA`, `pHYs`, `eXIf`, `tEXt`, β¦) are copied verbatim. " | |
"Pixel data from the target image is left untouched." | |
) | |
if __name__ == "__main__": | |
demo.launch(show_api=False, share=False) | |