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