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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +28 -88
app.py CHANGED
@@ -1,11 +1,10 @@
1
  #!/usr/bin/env python
2
  """
3
- Kovaaks PNG Metadata Cloner β€” v2
4
- ↓ Improvements
5
- β€’ enforces PNG chunk order (IHDR β†’ sRGB β†’ gAMA β†’ pHYs β†’ eXIf β†’ (…) β†’ IDAT β†’ IEND)
6
- β€’ keeps only a single instance of each ancillary chunk (no duplicates)
7
- β€’ validates / auto-converts target to 50 Γ— 50 RGBA if the user ticks a checkbox
8
- β€’ warns when resulting file > 256 B (Kovaaks soft limit)
9
  """
10
 
11
  from __future__ import annotations
@@ -20,12 +19,13 @@ import gradio as gr
20
  from PIL import Image
21
 
22
  PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
23
- # ancillary chunks Kovaaks cares about, in desired order
 
24
  ANCILLARY_ORDER = (
25
  b"sRGB",
26
  b"gAMA",
27
  b"pHYs",
28
- b"eXIf", # empty container in template crosshair
29
  b"iTXt",
30
  b"tEXt",
31
  b"zTXt",
@@ -40,9 +40,9 @@ def _read_chunks(fp: BinaryIO) -> Iterator[tuple[bytes, bytes]]:
40
  while True:
41
  length_bytes = fp.read(4)
42
  if not length_bytes:
43
- break # EOF
44
  if len(length_bytes) != 4:
45
- raise ValueError("Corrupted PNG: could not read length")
46
 
47
  length = struct.unpack(">I", length_bytes)[0]
48
  chunk_type = fp.read(4)
@@ -56,61 +56,52 @@ def _read_chunks(fp: BinaryIO) -> Iterator[tuple[bytes, bytes]]:
56
 
57
 
58
  def _clean_ancillary(src_chunks: list[tuple[bytes, bytes]]) -> list[bytes]:
59
- """Return at most one instance of each ordered ancillary chunk."""
60
- kept: dict[bytes, bytes] = {}
61
  for raw, tp in src_chunks:
62
- if tp in ANCILLARY_ORDER and tp not in kept:
63
- kept[tp] = raw
64
- # return in canonical order
65
- return [kept[tag] for tag in ANCILLARY_ORDER if tag in kept]
66
 
67
 
68
  def _validate_or_resize(path: str, auto_fix: bool) -> None:
69
- """Ensure 50Γ—50 RGBA; optionally fix in-place (overwrites file)."""
70
  img = Image.open(path)
71
  if img.size == (50, 50) and img.mode == "RGBA":
72
- return # already good
73
 
74
  if not auto_fix:
75
- raise ValueError("Target must be 50Γ—50 px, RGBA (enable auto-fix to convert).")
76
 
77
- # auto-convert
78
- img = img.convert("RGBA")
79
- img = img.resize((50, 50), Image.LANCZOS)
80
  img.save(path, optimize=True)
81
 
82
 
83
  def clone_metadata(
84
- src_path: str, tgt_path: str, out_path: str, auto_fix: bool = True
85
  ) -> str:
86
- """Clone cleaned ancillary chunks from src β†’ tgt. Save to out_path."""
87
  with open(src_path, "rb") as fs, open(tgt_path, "rb") as ft:
88
  if fs.read(8) != PNG_SIGNATURE or ft.read(8) != PNG_SIGNATURE:
89
- raise ValueError("Both uploads must be valid PNG images.")
90
 
91
  src_chunks = list(_read_chunks(fs))
92
  tgt_chunks = list(_read_chunks(ft))
93
 
94
- ancillary = _clean_ancillary(src_chunks)
 
95
 
96
- # Remove from target any ancillary tags we will replace
97
- replace_tags = {tp for _, tp in ancillary}
98
  rebuilt: list[bytes] = []
99
-
100
  inserted = False
101
  for raw, tp in tgt_chunks:
102
  if tp == b"IHDR":
103
  rebuilt.append(raw)
104
- # insert ordered ancillary exactly once
105
  if not inserted:
106
  rebuilt.extend(ancillary)
107
  inserted = True
108
  continue
109
-
110
  if tp in replace_tags:
111
- # skip existing duplicate in target
112
- continue
113
-
114
  rebuilt.append(raw)
115
 
116
  with open(out_path, "wb") as fo:
@@ -118,64 +109,13 @@ def clone_metadata(
118
  for chunk in rebuilt:
119
  fo.write(chunk)
120
 
121
- # sanity - size warning
122
  if os.path.getsize(out_path) > 256:
123
- print("⚠️ Output > 256 bytes; Kovaaks may still load it but smaller is safer.")
124
-
125
- # optional validation / resize after writing (pixel data unchanged)
126
- _validate_or_resize(out_path, auto_fix=False) # ensure still 50Γ—50 RGBA
127
 
 
128
  return out_path
129
 
130
 
131
  # ────────────────────────────────────────────────── Gradio UI ──
132
  def process(src_file: Path, tgt_file: Path, auto_fix: bool):
133
- if not src_file or not tgt_file:
134
- raise gr.Error("Upload both images first.")
135
-
136
- # optional in-place auto-fix for target
137
- try:
138
- _validate_or_resize(str(tgt_file), auto_fix)
139
- except Exception as e:
140
- raise gr.Error(str(e))
141
-
142
- tmp = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
143
- tmp.close()
144
- try:
145
- clone_metadata(str(src_file), str(tgt_file), tmp.name, auto_fix=False)
146
- except Exception as e:
147
- os.remove(tmp.name)
148
- raise gr.Error(str(e))
149
-
150
- return tmp.name
151
-
152
-
153
- with gr.Blocks(title="Kovaaks PNG Metadata Cloner v2") as demo:
154
- gr.Markdown(
155
- """
156
- # 🏹 Kovaaks PNG Metadata Cloner v2
157
- 1. **Image 1 (source)** – a PNG that already works in-game
158
- 2. **Image 2 (target)** – your new crosshair (any size/format)
159
- 3. Tick **auto-fix** to force 50 Γ— 50 RGBA conversion
160
- 4. Click **Clone** β†’ download the ready file
161
-
162
- Put the result into
163
- `%LOCALAPPDATA%\\FPSAimTrainer\\Saved\\MyImport\\crosshairs`
164
- """
165
- )
166
- with gr.Row():
167
- src = gr.File(label="Image 1 β€” metadata source (PNG)", type="filepath")
168
- tgt = gr.File(label="Image 2 β€” target crosshair", type="filepath")
169
- auto = gr.Checkbox(True, label="Auto-fix target to 50Γ—50 RGBA")
170
- out = gr.File(label="Output PNG", interactive=False)
171
- btn = gr.Button("Clone metadata β†’")
172
-
173
- btn.click(fn=process, inputs=[src, tgt, auto], outputs=[out])
174
-
175
- gr.Markdown(
176
- "Ancillary chunks (sRGB, gAMA, pHYs, eXIf, etc.) are copied verbatim, "
177
- "duplicates removed, order normalised. Pixel data comes solely from Image 2."
178
- )
179
-
180
- if __name__ == "__main__":
181
- demo.launch(show_api=False)
 
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
 
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",
 
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)
 
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:
 
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