image resize ok
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user