wuhp commited on
Commit
b9339ab
Β·
verified Β·
1 Parent(s): 8384724

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -80
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
- # Kovaaks ancillary order we preserve
24
- ANCILLARY_ORDER = (
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
- Caller must have consumed the 8-byte PNG signature already.
 
 
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: bad length field")
46
 
47
- length = struct.unpack(">I", length_bytes)[0]
48
  chunk_type = fp.read(4)
49
- data = fp.read(length)
50
- crc = fp.read(4)
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 _clean_ancillary(src_chunks: list[tuple[bytes, bytes]]) -> list[bytes]:
59
- """Return at most one instance of each ancillary tag in canonical order."""
60
- seen: dict[bytes, bytes] = {}
61
- for raw, tp in src_chunks:
62
- if tp in ANCILLARY_ORDER and tp not in seen:
63
- seen[tp] = raw
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 inputs must be valid PNG files.")
87
 
88
  src_chunks = list(_read_chunks(fs))
89
  tgt_chunks = list(_read_chunks(ft))
90
 
91
- ancillary = _clean_ancillary(src_chunks) # list[bytes]
92
- replace_tags = {raw[4:8] for raw in ancillary} # slice gives chunk-type
 
 
 
93
 
94
- rebuilt: list[bytes] = []
95
- inserted = False
96
  for raw, tp in tgt_chunks:
97
- if tp == b"IHDR":
98
- rebuilt.append(raw)
99
- if not inserted:
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 rebuilt:
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
- # ────────────────────────────────────────────────── Gradio UI ──
120
- def process(src_file: Path, tgt_file: Path, auto_fix: bool):
121
- if not src
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)