image resize ok

This commit is contained in:
Zeecka
2026-01-02 20:00:49 +01:00
parent 40421f51bf
commit 36b8b1cca0
2 changed files with 139 additions and 167 deletions
+74 -102
View File
@@ -8,53 +8,35 @@ from typing import Any, List
from ..app import app from ..app import app
from ..models import IHDR from ..models import IHDR
from .utils import PNG, update_data from .base_analyzer import SubprocessAnalyzer
from .utils import PNG
def write_recovered_png( class ResizeAnalyzer(SubprocessAnalyzer):
png_bytes: bytearray, """Analyzer for image resize."""
width: int,
height: int,
output_path: Path,
) -> None:
"""
Rebuilds a PNG by patching IHDR width/height and writes it to disk.
"""
height_bytes = height.to_bytes(4, byteorder="big")
width_bytes = width.to_bytes(4, byteorder="big")
full_png_data = ( png: PNG
png_bytes[:16] # Splicing: [Start...IHDR+4] + [W] + [H] + [IHDR+12...End]
+ width_bytes
+ height_bytes
+ png_bytes[24:]
)
with output_path.open("wb") as out_f: def __init__(self, *args):
out_f.write(full_png_data) super().__init__("image_resize", *args, has_archive=True)
def _lookup_crc(self) -> List[PNG]:
def lookup_crc(png: PNG) -> List[PNG]:
""" """
Look up PNG images in the database by their CRC value. Look up PNG images in the database by their CRC value.
Args:
png (PNG): A PNG object containing the CRC value to search for.
Returns: Returns:
List[PNG]: A list of PNG objects with matching CRC values, reconstructed from their packed List[PNG]: A list of PNG objects with matching CRC values, reconstructed from their
representations. packed representations.
""" """
with app.app_context(): with app.app_context():
imgs = IHDR.query.filter_by(crc=png.crc).all() imgs = IHDR.query.filter_by(crc=self.png.crc).all()
results = [] results = []
for img in imgs: for img in imgs:
packed = img.packed packed = img.packed
results.append(PNG.from_packed(packed, crc=png.crc)) results.append(PNG.from_packed(packed, crc=self.png.crc))
return results return results
def _search_height_crc(self) -> List[PNG]:
def search_height_crc(png: PNG) -> List[PNG]:
""" """
Search for PNG images with matching CRC by iterating through possible heights. Search for PNG images with matching CRC by iterating through possible heights.
@@ -62,9 +44,6 @@ def search_height_crc(png: PNG) -> List[PNG]:
CRC checksum as the input PNG by systematically varying the height parameter while CRC checksum as the input PNG by systematically varying the height parameter while
keeping all other properties constant. keeping all other properties constant.
Args:
png (PNG): The reference PNG object whose CRC checksum will be matched.
Returns: Returns:
List[PNG]: A list of PNG objects with matching CRC values. Returns an empty list List[PNG]: A list of PNG objects with matching CRC values. Returns an empty list
if no matches are found. if no matches are found.
@@ -76,17 +55,39 @@ def search_height_crc(png: PNG) -> List[PNG]:
results = [] results = []
for height in range(100, 3501): for height in range(100, 3501):
candidate = PNG( candidate = PNG(
size=(png.width, height), size=(self.png.width, height),
bit_depth=png.bit_depth, bit_depth=self.png.bit_depth,
color_type=png.color_type, color_type=self.png.color_type,
interlace=png.interlace, interlace=self.png.interlace,
) )
if candidate.crc == png.crc: if candidate.crc == self.png.crc:
results.append(candidate) results.append(candidate)
return results return results
def _write_recovered_png(self, png_bytes: bytearray, png_match: PNG) -> str:
"""
Rebuilds a PNG by patching IHDR width/height and writes it to disk.
"""
def analyze_image_resize(input_img: Path, output_dir: Path) -> None: img_name = f"recovered_{png_match.width}x{png_match.height}.png"
output_path = self.output_dir / img_name
height_bytes = png_match.height.to_bytes(4, byteorder="big")
width_bytes = png_match.width.to_bytes(4, byteorder="big")
full_png_data = (
png_bytes[:16] # Splicing: [Start...IHDR+4] + [W] + [H] + [IHDR+12...End]
+ width_bytes
+ height_bytes
+ png_bytes[24:]
)
with output_path.open("wb") as out_f:
out_f.write(full_png_data)
return img_name
def get_results(self, _: Optional[str] = None) -> dict[str, Any]:
""" """
Analyze a PNG image and recover resized versions based on CRC matching. Analyze a PNG image and recover resized versions based on CRC matching.
@@ -94,10 +95,6 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
by looking up its CRC (Cyclic Redundancy Check) in a database. If matches are by looking up its CRC (Cyclic Redundancy Check) in a database. If matches are
found, it recovers and saves PNG images with the matched dimensions. found, it recovers and saves PNG images with the matched dimensions.
Args:
input_img (Path): Path to the input PNG image file to analyze.
output_dir (Path): Path to the directory where recovered PNG images will be saved.
Returns: Returns:
None None
@@ -111,88 +108,63 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
Notes: Notes:
- If the PNG has invalid structure or IHDR chunk is not first, an error is logged. - If the PNG has invalid structure or IHDR chunk is not first, an error is logged.
- The function performs a two-step lookup: first by CRC, then by height/CRC if no matches - The function performs a two-step lookup: first by CRC, then by height/CRC if no
found. matches found.
- Recovered images are named as "recovered_{width}x{height}.png". - Recovered images are named as "recovered_{width}x{height}.png".
- Analysis results (logs, status, and image URLs) are persisted via update_data(). - Analysis results (logs, status, and image URLs) are persisted via update_data().
""" """
# 1. Setup Paths
input_img = Path(input_img)
output_dir = Path(output_dir)
# 3. Initialize Logs
logs = [] logs = []
try: try:
with input_img.open("rb") as image: with self.input_img.open("rb") as image:
img_bytes = bytearray(image.read()) img_bytes = bytearray(image.read())
try: try:
png = PNG.from_bytearray(img_bytes) self.png = PNG.from_bytearray(img_bytes)
except ValueError: except ValueError:
png = None return {
if png is None:
logs.append(
"Failure: IHDR chunk is not the first chunk, or PNG has invalid structure."
)
update_data(
output_dir,
{
"image_resize": {
"status": "error", "status": "error",
"error": "IHDR chunk is not the first chunk, or PNG has invalid structure.", "error": "IHDR chunk is not the first chunk, or PNG has invalid structure.",
} }
},
)
return
logs.append(f"Target CRC found: 0x{png.crc:08x}") # Already correct
if self.png.crc == self.png.compute_crc():
if png.crc == png.compute_crc(): return {
note = ( "status": "ok",
f"PNG is already valid with dimensions {png.width}x{png.height} " "note": f"PNG is already valid with dimensions "
f"and crc 0x{png.crc:08x}." f"{self.png.width}x{self.png.height} and crc 0x{self.png.crc:08x}.",
)
output_no_compute: dict[str, Any] = {
"image_resize": {"status": "ok", "note": note}
} }
update_data(output_dir, output_no_compute)
return
saved_img_urls = [] saved_img_urls = []
db_matches = lookup_crc(png) # No match found
if not db_matches: if not (db_matches := self._lookup_crc()) and not (
db_matches = search_height_crc(png) db_matches := self._search_height_crc()
if not db_matches: ):
logs.append("Failure: No matching dimensions found.") return {
else: "status": "error",
# Create parent directories if they don't exist "error": "Failure: No matching dimensions found.",
output_dir.mkdir(parents=True, exist_ok=True) }
# Iterate through ALL candidates found
for im_match in db_matches:
img_name = f"recovered_{im_match.width}x{im_match.height}.png"
output_path = output_dir / img_name
write_recovered_png(
img_bytes, im_match.width, im_match.height, output_path
)
# Match found, saving image(s)
logs.append(f"Target CRC found: 0x{self.png.crc:08x}")
self.output_dir.mkdir(parents=True, exist_ok=True)
for png_match in db_matches:
img_name = self._write_recovered_png(img_bytes, png_match)
logs.append(f"Image saved: {img_name}") logs.append(f"Image saved: {img_name}")
# Append the formatted URL to our list # Append the formatted URL to our list
saved_img_urls.append("/image/" + str(Path(output_dir.name) / img_name)) saved_img_urls.append("/image/" + str(Path(self.output_dir.name) / img_name))
# 4. Final Data Update return {
output_data: dict[str, Any] = {
"image_resize": {
"status": "ok", "status": "ok",
"output": logs, "output": logs,
"png_images": saved_img_urls, "png_images": saved_img_urls,
} }
}
update_data(output_dir, output_data)
except Exception as e: except Exception as e:
update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}}) return {"status": "error", "error": str(e)}
def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
"""Analyze an image submission using outguess."""
analyzer = ResizeAnalyzer(input_img, output_dir)
analyzer.analyze()
+5 -5
View File
@@ -89,12 +89,12 @@ def analyze_image(submission_hash: str) -> None:
(analyze_decomposer, img_path, result_path), (analyze_decomposer, img_path, result_path),
(analyze_exiftool, img_path, result_path), (analyze_exiftool, img_path, result_path),
(analyze_foremost, img_path, result_path), (analyze_foremost, img_path, result_path),
(analyze_strings, img_path, result_path),
(analyze_pngcheck, img_path, result_path),
(analyze_steghide, img_path, result_path, submission.password),
(analyze_openstego, img_path, result_path, submission.password),
(analyze_zsteg, img_path, result_path),
(analyze_image_resize, img_path, result_path), (analyze_image_resize, img_path, result_path),
(analyze_openstego, img_path, result_path, submission.password),
(analyze_pngcheck, img_path, result_path),
(analyze_strings, img_path, result_path),
(analyze_steghide, img_path, result_path, submission.password),
(analyze_zsteg, img_path, result_path),
] ]
# Deep analysis only # Deep analysis only