File size: 4,135 Bytes
a120b1e
 
 
054b532
a120b1e
 
 
 
8384724
b9339ab
 
054b532
 
 
b9339ab
 
 
054b532
a120b1e
 
 
b9339ab
054b532
b9339ab
054b532
b9339ab
a120b1e
b9339ab
 
054b532
 
b9339ab
054b532
 
a120b1e
 
b9339ab
 
 
 
 
 
a120b1e
054b532
b9339ab
a120b1e
 
 
 
b9339ab
 
 
 
 
4a5831f
b9339ab
a120b1e
b9339ab
 
 
a120b1e
 
 
b9339ab
a120b1e
054b532
a120b1e
 
 
b9339ab
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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)