Merge remote-tracking branch 'origin/main' into test/increase-coverage-to-50-percent

This commit is contained in:
github-actions[bot]
2026-03-04 23:04:02 +00:00
2 changed files with 142 additions and 7 deletions
+96 -1
View File
@@ -1627,11 +1627,106 @@ def _get_image_path(img_type: str, subfolder: str, filename: str) -> str | None:
return full_path
def _strip_png_metadata_fast(data: bytes) -> bytes | None:
import io
import struct
if len(data) < 8 or data[:8] != b"\x89PNG\r\n\x1a\n":
return None
out = io.BytesIO()
out.write(data[:8])
offset = 8
saw_any_chunk = False
saw_iend = False
while offset + 8 <= len(data):
chunk_length = struct.unpack(">I", data[offset : offset + 4])[0]
chunk_type = data[offset + 4 : offset + 8]
chunk_full_size = 12 + chunk_length
if offset + chunk_full_size > len(data):
return None
saw_any_chunk = True
if chunk_type == b"IEND":
saw_iend = True
if chunk_type not in (b"tEXt", b"zTXt", b"iTXt"):
out.write(data[offset : offset + chunk_full_size])
offset += chunk_full_size
if not saw_any_chunk or not saw_iend:
return None
return out.getvalue()
def _strip_webp_metadata_fast(data: bytes) -> bytes | None:
import io
import struct
if len(data) < 12 or data[:4] != b"RIFF" or data[8:12] != b"WEBP":
return None
out = io.BytesIO()
out.write(data[:12])
offset = 12
vp8x_pos = -1
vp8x_data = bytearray()
saw_any_chunk = False
while offset + 8 <= len(data):
chunk_id = data[offset : offset + 4]
chunk_size = struct.unpack("<I", data[offset + 4 : offset + 8])[0]
padding = chunk_size % 2
chunk_full_size = 8 + chunk_size + padding
if offset + chunk_full_size > len(data):
return None
saw_any_chunk = True
chunk_data = data[offset : offset + chunk_full_size]
if chunk_id in (b"EXIF", b"XMP "):
pass # skip metadata
elif chunk_id == b"VP8X":
vp8x_pos = out.tell()
vp8x_data = bytearray(chunk_data)
out.write(vp8x_data)
else:
out.write(chunk_data)
offset += chunk_full_size
total_size = out.tell()
if not saw_any_chunk or total_size <= 12:
return None
if vp8x_pos != -1 and len(vp8x_data) >= 12:
flags = struct.unpack("<I", vp8x_data[8:12])[0]
flags &= ~8 # Unset EXIF
flags &= ~4 # Unset XMP
vp8x_data[8:12] = struct.pack("<I", flags)
current_pos = out.tell()
out.seek(vp8x_pos)
out.write(vp8x_data)
out.seek(current_pos)
out.seek(4)
out.write(struct.pack("<I", total_size - 8))
return out.getvalue()
def _strip_metadata(image_path: str) -> bytes:
import io
try:
with Image.open(image_path) as img:
with open(image_path, "rb") as f:
data = f.read()
if data.startswith(b"\x89PNG\r\n\x1a\n"):
try:
res = _strip_png_metadata_fast(data)
if res is not None:
return res
except Exception as e:
logging.warning(f"[Meld] Fast PNG strip failed for {image_path}: {e}")
if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
try:
res = _strip_webp_metadata_fast(data)
if res is not None:
return res
except Exception as e:
logging.warning(f"[Meld] Fast WebP strip failed for {image_path}: {e}")
# Fallback for JPEG or if fast paths fail
with Image.open(io.BytesIO(data)) as img:
# Paste into a new image to strip metadata
clean_img = Image.new(img.mode, img.size)
clean_img.paste(img)
@@ -1,11 +1,47 @@
import { Download, X } from "lucide-react";
import type React from "react";
import { useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useEscapeToClose } from "../../../hooks/useEscapeToClose";
import { logger } from "../../../logger";
import * as imagesApi from "../../images/api/imagesApi";
const STORAGE_KEY = "meld-download-options";
interface StoredDownloadOptions {
format: "zip" | "raw";
removeMetadata: boolean;
}
function loadStoredOptions(): StoredDownloadOptions {
try {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
const parsed = JSON.parse(saved) as Partial<StoredDownloadOptions>;
const raw = parsed.removeMetadata as unknown;
// Legacy clients/saved states may store removeMetadata as boolean true, string "true",
// or numeric 1; preserve these exact values for backward compatibility.
const removeMetadata =
raw === true || raw === "true" || (typeof raw === "number" && raw === 1);
return {
format: parsed.format === "raw" ? "raw" : "zip",
removeMetadata,
};
}
} catch (_e) {
// Ignore parse errors, use defaults
}
return { format: "zip", removeMetadata: false };
}
function saveStoredOptions(options: StoredDownloadOptions): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(options));
} catch (_e) {
// Ignore quota errors
}
}
interface DownloadModalProps {
imageIds: number[];
onSuccess?: () => void;
@@ -13,12 +49,16 @@ interface DownloadModalProps {
}
export const DownloadModal: React.FC<DownloadModalProps> = ({ imageIds, onSuccess, onClose }) => {
const [format, setFormat] = useState<"zip" | "raw">("zip");
const [removeMetadata, setRemoveMetadata] = useState(false);
const [options, setOptions] = useState<StoredDownloadOptions>(() => loadStoredOptions());
const { format, removeMetadata } = options;
const [isDownloading, setIsDownloading] = useState(false);
const overlayMouseDownRef = useRef(false);
useEffect(() => {
saveStoredOptions(options);
}, [options]);
const handleOverlayMouseDown = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
overlayMouseDownRef.current = true;
@@ -110,7 +150,7 @@ export const DownloadModal: React.FC<DownloadModalProps> = ({ imageIds, onSucces
name="format"
value="zip"
checked={format === "zip"}
onChange={() => setFormat("zip")}
onChange={() => setOptions((o) => ({ ...o, format: "zip" }))}
disabled={isDownloading}
/>
ZIP (Single File)
@@ -128,7 +168,7 @@ export const DownloadModal: React.FC<DownloadModalProps> = ({ imageIds, onSucces
name="format"
value="raw"
checked={format === "raw"}
onChange={() => setFormat("raw")}
onChange={() => setOptions((o) => ({ ...o, format: "raw" }))}
disabled={isDownloading}
/>
Raw (Multiple Files)
@@ -158,7 +198,7 @@ export const DownloadModal: React.FC<DownloadModalProps> = ({ imageIds, onSucces
<input
type="checkbox"
checked={removeMetadata}
onChange={(e) => setRemoveMetadata(e.target.checked)}
onChange={(e) => setOptions((o) => ({ ...o, removeMetadata: e.target.checked }))}
disabled={isDownloading}
/>
Remove metadata (ComfyUI workflow data) from downloaded images