From 5c2da5e187a44abf78dbd82c59249b0c7d01d337 Mon Sep 17 00:00:00 2001
From: Zeecka <24873615+Zeecka@users.noreply.github.com>
Date: Wed, 10 Dec 2025 23:46:11 +0100
Subject: [PATCH 01/19] Revise README for clarity and structure
Updated README.md to enhance formatting, improve descriptions, and add sections.
---
README.md | 224 ++++++++++++++++++++++++++++++++++++++----------------
1 file changed, 159 insertions(+), 65 deletions(-)
diff --git a/README.md b/README.md
index 1628e41..6f7f2eb 100644
--- a/README.md
+++ b/README.md
@@ -6,101 +6,195 @@
[](https://aperisolve.com/)
[](https://inventory.raw.pm/tools.html#Aperi'Solve)
-
+
+
+
+
+
-Try it now: https://www.aperisolve.com
+Try it now: **https://www.aperisolve.com**
- Support Aperi'Solve:
+Support Aperi'Solve:
-## What is Aperi'Solve?
-Aperi'Solve is a platform which performs steganalysis on images.
+---
-The platform uses *layer analysis*, "*zsteg*", "*steghide*", "*outguess*", "*exiftool*", "*binwalk*", "*foremost*" and "*strings*" for deeper steganography analysis.
+Aperi'Solve is an open-source steganalysis web platform that performs automated analysis on images to detect and extract hidden data using common steganography tools and techniques.
-
-
-### Why Aperi'Solve?
+Table of contents
+- [Key features](#key-features)
+- [Quick start (Docker)](#quick-start-docker)
+- [Development](#development)
+- [Adding a new analyzer](#adding-a-new-analyzer)
+- [Configuration & environment variables](#configuration--environment-variables)
+- [Architecture](#architecture)
+- [Troubleshooting & tips](#troubleshooting--tips)
+- [Roadmap](#roadmap)
+- [Contributing](#contributing)
-Aperi'Solve has been created in order to have an "easy to use" platform which performs common steganalysis tests such as LSB or `steghide`. The platform is also a quick alternative for people who were note able to install tools like `zsteg` (ruby gem) properly.
+## Key features
-### Features
+- Visualize each bit layer (LSB and other layers) per image channel (R/G/B/Alpha).
+- Browse and download each bit-layer image.
+- Integrates and displays outputs from:
+ - zsteg (LSB text/data extraction)
+ - steghide (extraction with password)
+ - outguess (extraction with password)
+ - exiftool (metadata and geolocation)
+ - binwalk (embedded archives)
+ - foremost (carved files)
+ - pngcheck
+ - strings
+- Worker queue architecture for offloading heavy/slow analyzers (Redis + background workers).
+- Results stored for later browsing and download.
-Aperi'Solve is based on Python3 with Flask and PIL module, the platform supports the most common image formats like`.png`, `.jpg`, `.gif`, `.bmp`, `.jpeg`, `.jfif`, `.jpe`, `.tiff`...
+## Quick start (Docker)
-The platform allows you to:
-- **Visualize each bit layer** of each channel for a given image (ie. LSB of Red channel).
-- **Browse** and **Download each bit layer image**.
-- **Visualise `zsteg` information** such as text encoded on LSB
-- **Download `steghide` files** using a defined password
-- **Download `outguess` files** using a defined password
-- **Visualise `exiftool` information** such as geolocation or author
-- **Visualise `binwalk` information**
-- **Download `binwalk` files** such as ZIP file in PNG headers
-- **Download `foremost` files** such as ZIP file in PNG headers
-- **Visualise `pngcheck` output**
-- **Visualise `strings` output**
+In case you want to host your own version of https://www.aperisolve.com/.
-## Installation
-
-Even if Aperi'Solve is available at the URL https://www.aperisolve.com/, you can set up your own instance with the following command:
+Recommended: Docker + Docker Compose.
+Clone and start the full stack (production-like):
```bash
git clone https://github.com/Zeecka/AperiSolve.git
-cd aperisolve
+cd AperiSolve
docker compose up -d
```
-> Default will run on http://localhost:5000/
+Default: http://localhost:5000/
## Development
-
-Aperi'Solve platform is a *Flask* web service (python 3.11+) with python workers performing analysis.
-Jobs are stacked in a redis-queue, and image information is stored in a postgresql database.
-All services run inside Docker containers.
-
-### Adding new analyzer
-
-Adding new analyzer is easy.
-
-1. Copy and adapt the [template_analyzer.py](aperisolve/analyzers/template_analyzer.py) file with your name, ie. "myanalyzer.py".
-2. Add this analyzer to [workers.py](aperisolve/workers.py).
- - Import your function `from .analyzers.analyzer import analyze_myanalyzer`
- - Add the function to the `analyzers` variable, like other analyzers.
-3. Add your analyzer name to `TOOL_ORDER` in [aperisolve.js](aperisolve/static/js/aperisolve.js).
-
-### Tips
-
-Here is a batch of useful commands.
-
+Development compose:
```bash
-docker compose down -v # Remove the volume containings results
-docker compose -f compose.dev.yml up # Start application in developpement mode
-rm -rf aperisolve/results # Remove results from development mode
-docker exec -it postgres psql -U aperiuser -d aperisolve # Get postgresql shell
-docker exec -it aperisolve-web bash # Get a shell inside the running web app
+# development environment (hot reload and local volumes)
+docker compose -f compose.dev.yml up --build
```
-> Note, if you used [compose.dev.yml](compose.dev.yml), you must remove "results" when switching to [production Docker](compose.yml) setup.
+Useful commands:
+```bash
+# Stop and remove containers, networks and volumes (results are stored in a volume)
+docker compose down -v
-### Code quality
+# Enter web container shell
+docker exec -it aperisolve-web bash
-Python code follows the following linters:
+# Enter Postgres shell (from host)
+docker exec -it postgres psql -U aperiuser -d aperisolve
+```
-- Black
-- Flake8 (ignoring E203,E501,W503)
-- Pylint (ignoring W0718,R0903,R0801)
-- Mypy (ignoring unused-awaitable)
+Note: If switching between dev and production compose files, remove the `results` directory or mounted volume to avoid conflicts:
+```bash
+rm -rf aperisolve/results
+```
+### Adding a new analyzer
-GitHub CI will alert in case of failure.
+Adding a custom analyzer is straightforward:
+
+1. Create your analyzer file:
+ - Copy `aperisolve/analyzers/template_analyzer.py` -> `aperisolve/analyzers/myanalyzer.py`
+ - Implement function signature similar to:
+ ```python
+ # aperisolve/analyzers/myanalyzer.py
+ def analyze_myanalyzer(image_path: str, results_dir: str) -> dict:
+ """
+ Perform analysis on image located at image_path.
+ Produce outputs in results_dir and return a result dict (json-serializable).
+ """
+ # your analysis logic here
+ return {"name": "myanalyzer", "status": "ok", "outputs": [...]}
+ ```
+
+2. Register the analyzer in the worker pipeline:
+ - Edit `aperisolve/workers.py`:
+ ```python
+ # import
+ from .analyzers.myanalyzer import analyze_myanalyzer
+
+ # add to analyzers list (preserve the order)
+ analyzers = [
+ analyze_zsteg,
+ analyze_steghide,
+ # ...
+ analyze_myanalyzer,
+ ]
+ ```
+
+3. Add your analyzer to the UI order:
+ - Edit `aperisolve/static/js/aperisolve.js` and append `myanalyzer` to `TOOL_ORDER` so it appears in the frontend.
+
+4. Test locally: run the worker and submit jobs to ensure outputs are produced and displayed.
+
+Tips:
+- Keep analyzers idempotent and write outputs to the provided `results_dir`.
+- Return structured JSON so the frontend can render links/downloads automatically.
+
+## Configuration & environment variables
+
+Typical services:
+- Web app (Flask)
+- Workers (Python)
+- Redis (RQ)
+- PostgreSQL
+
+Main environment variables (examples):
+- DATABASE_URL=postgresql://aperiuser:password@postgres:5432/aperisolve
+- REDIS_URL=redis://redis:6379/0
+- SECRET_KEY=change_me
+- FLASK_ENV=production/development
+
+If using Docker Compose, defaults are set in the compose files. For production deployments, set secure secrets via your orchestrator or environment.
+
+## Architecture
+
+- Flask web framework
+- Background workers that run analyzers (queue via Redis)
+- PostgreSQL stores image metadata and job statuses
+- Docker-based deployment for isolation of analyzers and services
+
+This separation keeps heavy tools (binwalk, foremost, zsteg, etc.) isolated and avoids blocking the web worker.
+
+## Troubleshooting & tips
+
+- If analyzers don't produce output, check worker logs:
+ ```bash
+ docker compose logs -f aperisolve-worker
+ ```
+- To force re-analysis, remove results for the image (both file and in database) and re-submit the job.
+- Ensure system packages needed by native tools (binwalk, foremost) are available in the analyzer containers or host image.
## Roadmap
-- [ ] Zsteg extract / --all --> **Download `zsteg` files** such as mp3 encoded on LSB
-- [ ] Test on mobile
-- [ ] i18n
-- [ ] Rootless analyzers ?
+- [ ] zsteg: full extraction (--all) and download of discovered files (mp3, etc.)
+- [ ] Mobile-friendly UI / test on mobile
+- [ ] i18n (internationalization)
+- [ ] Rootless / unprivileged analyzers
+- [ ] Improve analyzer sandboxing (e.g., per-analyzer containers)
+
+## Contributing
+
+Contributions are welcome! Please:
+1. Fork the repository
+2. Create a feature branch
+3. Open a pull request describing your change
+
+Please follow the code style and run linters before submitting.
+The project adheres to:
+- Black
+- Flake8 (ignoring E203, E501, W503)
+- Pylint (ignoring W0718, R0903, R0801)
+- Mypy (ignoring unused-awaitable)
+
+CI will run these checks on each PR.
+
+## Security
+
+- If you discover a security issue, please report it privately to the repository owner instead of opening a public issue.
+
+## Credits
+
+Acknowledgements:
+- Thanks to contributors and the open-source community for the tools integrated (zsteg, steghide, binwalk, foremost, exiftool).
From a0ed892b60b7f71bb4cf9f053b97330b359bb55e Mon Sep 17 00:00:00 2001
From: Zeecka
Date: Wed, 10 Dec 2025 23:47:10 +0100
Subject: [PATCH 02/19] Bump JS, add coffee link
---
aperisolve/static/css/aperisolve.css | 10 ++++++++--
aperisolve/templates/base.html | 4 ++--
aperisolve/templates/footer.html | 5 +++++
3 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/aperisolve/static/css/aperisolve.css b/aperisolve/static/css/aperisolve.css
index 1d4c7be..4279412 100644
--- a/aperisolve/static/css/aperisolve.css
+++ b/aperisolve/static/css/aperisolve.css
@@ -330,7 +330,7 @@ td {
#cheatsheet li::before {
content: "\f054";
- font-family: "Font Awesome 6 Free";
+ font-family: "Font Awesome 7 Free";
font-weight: 900;
position: absolute;
left: 0;
@@ -397,11 +397,17 @@ footer {
}
}
-
footer a i {
font-size: 32px;
}
+#coffee {
+ height: 32px;
+ display: inline-block;
+ vertical-align: middle;
+ width: auto;
+}
+
/* ======== Utility ======== */
.glow-border {
diff --git a/aperisolve/templates/base.html b/aperisolve/templates/base.html
index 5ec42eb..292879c 100644
--- a/aperisolve/templates/base.html
+++ b/aperisolve/templates/base.html
@@ -6,8 +6,8 @@
Aperi'Solve
-
-
+
+
diff --git a/aperisolve/templates/footer.html b/aperisolve/templates/footer.html
index 0dad7a3..27bd7b4 100644
--- a/aperisolve/templates/footer.html
+++ b/aperisolve/templates/footer.html
@@ -1,4 +1,9 @@
+
From c889f6d229c506b4480d2f6dae30a73c3a326830 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 14:45:47 +0100
Subject: [PATCH 03/19] Added crc database creation
---
aperisolve/analyzers/utils.py | 85 +++++++++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
diff --git a/aperisolve/analyzers/utils.py b/aperisolve/analyzers/utils.py
index a20036a..693ea94 100644
--- a/aperisolve/analyzers/utils.py
+++ b/aperisolve/analyzers/utils.py
@@ -6,6 +6,9 @@ import os
import threading
from pathlib import Path
from typing import Any
+import itertools
+import zlib
+import sqlite3
_thread_lock = threading.Lock()
@@ -46,3 +49,85 @@ def update_data(
os.replace(tmp_file, json_file) # ensures file write is atomic
finally:
fcntl.flock(lock, fcntl.LOCK_UN)
+
+
+def create_crc_db():
+ db_filename = "ihdr_crcs.db"
+
+ # Connect to database
+ conn = sqlite3.connect(db_filename)
+ cursor = conn.cursor()
+
+ # Optimization: Turn off safety checks for bulk insertion speed
+ cursor.execute("PRAGMA synchronous = OFF")
+ cursor.execute("PRAGMA journal_mode = MEMORY")
+
+ # Create table
+ # We store the CRC as an integer for faster indexing/storage
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS ihdr (
+ crc INTEGER,
+ width INTEGER,
+ height INTEGER
+ )
+ """)
+
+ # Drop index if exists (rebuilding it at the end is faster)
+ cursor.execute("DROP INDEX IF EXISTS idx_crc")
+
+ max_width = 2000
+ max_height = 2000
+ W = range(1, max_width + 1)
+ H = range(1, max_height + 1)
+
+ # Standard PNG IHDR parameters
+ bit_depth = [1, 2, 4, 8, 16]
+ color_type = [0, 2, 3, 4, 6]
+ compression_method = [0] # This is always 0 according to the PNG spec
+ filter_method = [0] # This is always 0 according to the PNG spec
+ interlace_method = [0, 1]
+
+ # Create the generator
+ all_combinations = itertools.product(
+ W, H, bit_depth, color_type, compression_method, filter_method, interlace_method
+ )
+
+ batch_size = 100000
+ batch = []
+ count = 0
+
+ for w, h, bd, ct, comp, filt, inter in all_combinations:
+ # Construct IHDR chunk data
+ ihdr_data = (
+ w.to_bytes(4, "big") +
+ h.to_bytes(4, "big") +
+ bytes([bd, ct, comp, filt, inter])
+ )
+
+ # Calculate CRC
+ crc = zlib.crc32(b"IHDR" + ihdr_data) & 0xffffffff
+
+ # Add to batch
+ batch.append((crc, w, h))
+
+ if len(batch) >= batch_size:
+ cursor.executemany("INSERT INTO ihdr VALUES (?,?,?)", batch)
+ conn.commit()
+ batch = []
+ count += batch_size
+ if count % 1000000 == 0:
+ print(f"Inserted {count:,} rows...")
+
+ # Insert remaining records
+ if batch:
+ cursor.executemany("INSERT INTO ihdr VALUES (?,?,?)", batch)
+ conn.commit()
+
+ print("Insertion complete. Creating Index for faster lookups...")
+ # Creating the index allows for faster lookups
+ cursor.execute("CREATE INDEX idx_crc ON ihdr(crc)")
+ conn.commit()
+
+ conn.close()
+ print(f"Database ready.")
+ return
\ No newline at end of file
From d517a31504af8a3b7b6d5cb8887ec03ed3eb776c Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:11:19 +0100
Subject: [PATCH 04/19] Added CRC database lookup functionality
---
aperisolve/analyzers/image_resize.py | 124 +++++++++++++++++++++------
1 file changed, 96 insertions(+), 28 deletions(-)
diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py
index c9ed704..549d588 100644
--- a/aperisolve/analyzers/image_resize.py
+++ b/aperisolve/analyzers/image_resize.py
@@ -1,6 +1,7 @@
"""Image size bruteforcer for common PNG sizes."""
import zlib
+import sqlite3
from pathlib import Path
from .utils import update_data
@@ -142,6 +143,31 @@ EXPECTED_SIZES = [
]
+def write_recovered_png(
+ png_bytes: bytearray,
+ ihdr_offset: int,
+ 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")
+
+ # Splicing: [Start...IHDR+4] + [W] + [H] + [IHDR+12...End]
+ full_png_data = (
+ png_bytes[: ihdr_offset + 4]
+ + width_bytes
+ + height_bytes
+ + png_bytes[ihdr_offset + 12 :]
+ )
+
+ with output_path.open("wb") as out_f:
+ out_f.write(full_png_data)
+
+
def calc_checksum(
header_chunk: bytearray, width_bytes: bytearray, height_bytes: bytearray
) -> bytearray:
@@ -156,10 +182,43 @@ def calc_checksum(
return bytearray((zlib.crc32(new_header) & 0xFFFFFFFF).to_bytes(4, byteorder="big"))
+def lookup_crc(crc_bytes: bytes, logs: list) -> list:
+ """
+ Queries the SQLite DB for the CRC and returns a list of (width, height) tuples.
+ """
+ # Locate DB relative to this script file
+ db_path = Path(__file__).parent / "ihdr_crcs.db"
+
+ if not db_path.exists():
+ logs.append(f"Error: Database not found at {db_path}")
+ return []
+
+ try:
+ target_crc = int.from_bytes(crc_bytes, byteorder="big")
+ except ValueError:
+ logs.append(f"Invalid CRC bytes: {crc_bytes}")
+ return []
+
+ conn = sqlite3.connect(db_path)
+ cursor = conn.cursor()
+
+ # Query the database
+ cursor.execute("SELECT width, height FROM ihdr WHERE crc = ?", (target_crc,))
+ results = cursor.fetchall() # Returns list of (width, height)
+ conn.close()
+
+ if results:
+ logs.append(f"Database: Found {len(results)} match(es) for CRC {target_crc}")
+ return results
+ else:
+ logs.append(f"Database: No match found for CRC {target_crc}")
+ return []
+
+
def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
"""
Analyze an image submission using python-based resize bruteforce.
- Attempts to recover PNGs with modified resolutions but valid CRCs.
+ Strategy: Check common sizes first, then fall back to DB lookup.
"""
# 1. Setup Paths
@@ -168,7 +227,6 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
# 3. Initialize Logs
logs = []
- recovered_image = None
try:
with input_img.open("rb") as image:
@@ -198,8 +256,9 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
logs.append(f"Target CRC found: 0x{target_crc.hex()}")
match_found = False
+ candidates = []
- # Brute-force Loop
+ # --- STRATEGY 1: Check Common Sizes ---
for size in EXPECTED_SIZES:
width, height = size
@@ -210,40 +269,49 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
# Check if this size matches the file's CRC
if target_crc == calc_checksum(header_chunk, width_bytes, height_bytes):
match_found = True
- logs.append(f"Success: Match found! Dimensions: {width}x{height}")
-
- # Create Output Filename
- img_name = f"recovered_{width}x{height}.png"
-
- # Create parent directories if they don't exist
- output_dir.mkdir(parents=True, exist_ok=True)
- output_path = output_dir / img_name
-
- # Splicing: [Start...IHDR+4] + [W] + [H] + [IHDR+12...End]
- full_png_data = (
- b[: ihdr + 4] + width_bytes + height_bytes + b[ihdr + 12 :]
- )
-
- # Write the recovered file to disk
- with output_path.open("wb") as out_f:
- out_f.write(full_png_data)
-
- recovered_image = Path(output_dir.name) / img_name
- break # Stop looking after finding the match
+ logs.append(f"Success (Common List): Match found! Dimensions: {width}x{height}")
+ candidates.append((width, height))
+ break # Stop after first match
+ # --- STRATEGY 2: Check SQLite DB ---
if not match_found:
- logs.append("Failure: No matching dimensions found.")
+ logs.append("No common size matched. Checking database...")
+ db_matches = lookup_crc(target_crc, logs)
+ if db_matches:
+ candidates.extend(db_matches)
+ match_found = True
+
+ # --- Generate Output Images ---
+ saved_img_urls = []
+
+ if match_found and candidates:
+ # Create parent directories if they don't exist
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Iterate through ALL candidates found
+ for w, h in candidates:
+ img_name = f"recovered_{w}x{h}.png"
+ output_path = output_dir / img_name
+ write_recovered_png(b, ihdr, w, h, output_path)
+
+ logs.append(f"Image saved: {img_name}")
+
+ # Append the formatted URL to our list
+ saved_img_urls.append("/image/" + str(Path(output_dir.name) / img_name))
+
+ else:
+ logs.append("Failure: No matching dimensions found in List or DB.")
# 4. Final Data Update
output_data = {
- "image_resize": { # Key name must match your module name in AperiSolve
+ "image_resize": {
"status": "ok",
"output": logs,
+ "png_images": saved_img_urls,
}
}
- if recovered_image:
- output_data["image_resize"]["image"] = "/image/" + str(recovered_image)
+
update_data(output_dir, output_data)
except Exception as e:
- update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}})
+ update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}})
\ No newline at end of file
From 623361b7bc78dc8707d247f18ffac591498b8e15 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:11:41 +0100
Subject: [PATCH 05/19] Added handling of multiple png candidates
---
aperisolve/static/js/aperisolve.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js
index 6e98b57..2867666 100644
--- a/aperisolve/static/js/aperisolve.js
+++ b/aperisolve/static/js/aperisolve.js
@@ -279,6 +279,12 @@ function parseResult(result) {
)}'/> `;
}
+ if ("png_images" in result[tool]) {
+ for (const image of result[tool]["png_images"]) {
+ analyzer.innerHTML += ` `;
+ }
+ }
+
// Parse text output
From e6e086d4795ecef4283efdb2f0baa560a26451f3 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:12:23 +0100
Subject: [PATCH 06/19] added sqlite3 to requirements
---
aperisolve/requirements.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/aperisolve/requirements.txt b/aperisolve/requirements.txt
index 14446ca..6de4dbd 100644
--- a/aperisolve/requirements.txt
+++ b/aperisolve/requirements.txt
@@ -7,4 +7,5 @@ psycopg2-binary
types-Flask-SQLAlchemy
sqlalchemy-stubs
Pillow
-gunicorn
\ No newline at end of file
+gunicorn
+sqlite3
\ No newline at end of file
From 2dba82d04fb7ac09a05f0bf30753830dc12f7120 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:13:09 +0100
Subject: [PATCH 07/19] added crc database to .gitignore
---
.gitignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.gitignore b/.gitignore
index aa5b26a..12546d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
# Python
__pycache__/
.mypy_cache/
-results/
\ No newline at end of file
+results/
+ihdr_crcs.db
\ No newline at end of file
From d99651d3919305ed81885b2b7438b04b986d1515 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:16:22 +0100
Subject: [PATCH 08/19] Added database creation in Dockerfile
---
Dockerfile | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Dockerfile b/Dockerfile
index 67235ca..5487996 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,4 +21,6 @@ COPY aperisolve/ /aperisolve/
RUN pip install --no-cache-dir -r /aperisolve/requirements.txt
+RUN PYTHONPATH=/ python -c "from aperisolve.utils import create_crc_db; create_crc_db()"
+
CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "aperisolve.wsgi:application"]
\ No newline at end of file
From 9cacbf7d139883b7a187729c85303a6084a972a8 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 15:22:12 +0100
Subject: [PATCH 09/19] sqlite3 is builtin and thus not needed in requirements
---
aperisolve/requirements.txt | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/aperisolve/requirements.txt b/aperisolve/requirements.txt
index 6de4dbd..14446ca 100644
--- a/aperisolve/requirements.txt
+++ b/aperisolve/requirements.txt
@@ -7,5 +7,4 @@ psycopg2-binary
types-Flask-SQLAlchemy
sqlalchemy-stubs
Pillow
-gunicorn
-sqlite3
\ No newline at end of file
+gunicorn
\ No newline at end of file
From 705e19701fb56a0ab4c36d69af2cc383ed691b95 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 16:37:46 +0100
Subject: [PATCH 10/19] Linter
---
aperisolve/analyzers/image_resize.py | 17 ++++++-----
aperisolve/analyzers/utils.py | 45 ++++++++++++++++++----------
2 files changed, 39 insertions(+), 23 deletions(-)
diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py
index 549d588..45a6194 100644
--- a/aperisolve/analyzers/image_resize.py
+++ b/aperisolve/analyzers/image_resize.py
@@ -3,6 +3,7 @@
import zlib
import sqlite3
from pathlib import Path
+from typing import List
from .utils import update_data
# Full list of expected sizes
@@ -182,7 +183,7 @@ def calc_checksum(
return bytearray((zlib.crc32(new_header) & 0xFFFFFFFF).to_bytes(4, byteorder="big"))
-def lookup_crc(crc_bytes: bytes, logs: list) -> list:
+def lookup_crc(crc_bytes: bytes, logs: List[str]) -> List[tuple[int, int]]:
"""
Queries the SQLite DB for the CRC and returns a list of (width, height) tuples.
"""
@@ -196,7 +197,7 @@ def lookup_crc(crc_bytes: bytes, logs: list) -> list:
try:
target_crc = int.from_bytes(crc_bytes, byteorder="big")
except ValueError:
- logs.append(f"Invalid CRC bytes: {crc_bytes}")
+ logs.append(f"Invalid CRC bytes: {crc_bytes!r}")
return []
conn = sqlite3.connect(db_path)
@@ -210,9 +211,9 @@ def lookup_crc(crc_bytes: bytes, logs: list) -> list:
if results:
logs.append(f"Database: Found {len(results)} match(es) for CRC {target_crc}")
return results
- else:
- logs.append(f"Database: No match found for CRC {target_crc}")
- return []
+
+ logs.append(f"Database: No match found for CRC {target_crc}")
+ return []
def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
@@ -269,7 +270,9 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
# Check if this size matches the file's CRC
if target_crc == calc_checksum(header_chunk, width_bytes, height_bytes):
match_found = True
- logs.append(f"Success (Common List): Match found! Dimensions: {width}x{height}")
+ logs.append(
+ f"Success (Common List): Match found! Dimensions: {width}x{height}"
+ )
candidates.append((width, height))
break # Stop after first match
@@ -314,4 +317,4 @@ def analyze_image_resize(input_img: Path, output_dir: Path) -> None:
update_data(output_dir, output_data)
except Exception as e:
- update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}})
\ No newline at end of file
+ update_data(output_dir, {"image_resize": {"status": "error", "error": str(e)}})
diff --git a/aperisolve/analyzers/utils.py b/aperisolve/analyzers/utils.py
index 693ea94..55fdbaa 100644
--- a/aperisolve/analyzers/utils.py
+++ b/aperisolve/analyzers/utils.py
@@ -14,6 +14,14 @@ _thread_lock = threading.Lock()
MAX_PENDING_TIME = int(os.getenv("MAX_PENDING_TIME", "600")) # 10 minutes by default
+# PATH CONFIGURATION
+# Prioritize /data for Docker persistence, fallback to local directory for bare metal
+DB_NAME = "ihdr_crcs.db"
+if os.path.exists("/data"):
+ DB_PATH = os.path.join("/data", DB_NAME)
+else:
+ DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), DB_NAME)
+
def update_data(
output_dir: Path, new_data: dict[Any, Any], json_filename: str = "results.json"
@@ -51,7 +59,11 @@ def update_data(
fcntl.flock(lock, fcntl.LOCK_UN)
-def create_crc_db():
+def create_crc_db() -> None:
+ """Creates and populates CRC lookup database with CRC, Width and Heigth tuples"""
+ if os.path.exists(DB_PATH):
+ print(f"Database already exists at {DB_PATH}. Skipping generation.")
+ return
db_filename = "ihdr_crcs.db"
# Connect to database
@@ -64,51 +76,53 @@ def create_crc_db():
# Create table
# We store the CRC as an integer for faster indexing/storage
- cursor.execute("""
+ cursor.execute(
+ """
CREATE TABLE IF NOT EXISTS ihdr (
crc INTEGER,
width INTEGER,
height INTEGER
)
- """)
+ """
+ )
# Drop index if exists (rebuilding it at the end is faster)
cursor.execute("DROP INDEX IF EXISTS idx_crc")
max_width = 2000
max_height = 2000
- W = range(1, max_width + 1)
- H = range(1, max_height + 1)
+ w = range(1, max_width + 1)
+ h = range(1, max_height + 1)
# Standard PNG IHDR parameters
bit_depth = [1, 2, 4, 8, 16]
color_type = [0, 2, 3, 4, 6]
- compression_method = [0] # This is always 0 according to the PNG spec
- filter_method = [0] # This is always 0 according to the PNG spec
+ compression_method = [0] # This is always 0 according to the PNG spec
+ filter_method = [0] # This is always 0 according to the PNG spec
interlace_method = [0, 1]
# Create the generator
all_combinations = itertools.product(
- W, H, bit_depth, color_type, compression_method, filter_method, interlace_method
+ w, h, bit_depth, color_type, compression_method, filter_method, interlace_method
)
batch_size = 100000
batch = []
count = 0
- for w, h, bd, ct, comp, filt, inter in all_combinations:
+ for w_val, h_val, bd, ct, comp, filt, inter in all_combinations:
# Construct IHDR chunk data
ihdr_data = (
- w.to_bytes(4, "big") +
- h.to_bytes(4, "big") +
- bytes([bd, ct, comp, filt, inter])
+ w_val.to_bytes(4, "big")
+ + h_val.to_bytes(4, "big")
+ + bytes([bd, ct, comp, filt, inter])
)
# Calculate CRC
- crc = zlib.crc32(b"IHDR" + ihdr_data) & 0xffffffff
+ crc = zlib.crc32(b"IHDR" + ihdr_data) & 0xFFFFFFFF
# Add to batch
- batch.append((crc, w, h))
+ batch.append((crc, w_val, h_val))
if len(batch) >= batch_size:
cursor.executemany("INSERT INTO ihdr VALUES (?,?,?)", batch)
@@ -129,5 +143,4 @@ def create_crc_db():
conn.commit()
conn.close()
- print(f"Database ready.")
- return
\ No newline at end of file
+ print("Database ready.")
From e94962ccbb910be55fa398802f0e28b945fd7c57 Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 18:26:56 +0100
Subject: [PATCH 11/19] Fixed dev environment database setup
---
Dockerfile | 6 ++++--
aperisolve/analyzers/image_resize.py | 5 +++--
aperisolve/analyzers/utils.py | 8 +++++---
3 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 5487996..fc2198a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,8 +19,10 @@ RUN apt-get update && apt-get install -y \
COPY aperisolve/ /aperisolve/
+RUN mkdir -p /data
+
RUN pip install --no-cache-dir -r /aperisolve/requirements.txt
-RUN PYTHONPATH=/ python -c "from aperisolve.utils import create_crc_db; create_crc_db()"
+RUN PYTHONPATH=/ python -c "from aperisolve.analyzers.utils import create_crc_db; create_crc_db()"
-CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "aperisolve.wsgi:application"]
\ No newline at end of file
+CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "aperisolve.wsgi:application"]
diff --git a/aperisolve/analyzers/image_resize.py b/aperisolve/analyzers/image_resize.py
index 45a6194..66a62c7 100644
--- a/aperisolve/analyzers/image_resize.py
+++ b/aperisolve/analyzers/image_resize.py
@@ -4,7 +4,8 @@ import zlib
import sqlite3
from pathlib import Path
from typing import List
-from .utils import update_data
+from .utils import update_data, DB_PATH
+
# Full list of expected sizes
EXPECTED_SIZES = [
@@ -188,7 +189,7 @@ def lookup_crc(crc_bytes: bytes, logs: List[str]) -> List[tuple[int, int]]:
Queries the SQLite DB for the CRC and returns a list of (width, height) tuples.
"""
# Locate DB relative to this script file
- db_path = Path(__file__).parent / "ihdr_crcs.db"
+ db_path = Path(DB_PATH)
if not db_path.exists():
logs.append(f"Error: Database not found at {db_path}")
diff --git a/aperisolve/analyzers/utils.py b/aperisolve/analyzers/utils.py
index 55fdbaa..c539abd 100644
--- a/aperisolve/analyzers/utils.py
+++ b/aperisolve/analyzers/utils.py
@@ -64,10 +64,12 @@ def create_crc_db() -> None:
if os.path.exists(DB_PATH):
print(f"Database already exists at {DB_PATH}. Skipping generation.")
return
- db_filename = "ihdr_crcs.db"
- # Connect to database
- conn = sqlite3.connect(db_filename)
+ # Use the global DB_PATH variable so it writes to /data/ in Docker
+ # and /analyzers/ in local dev
+ print(f"Generating database at: {DB_PATH}")
+ conn = sqlite3.connect(DB_PATH)
+
cursor = conn.cursor()
# Optimization: Turn off safety checks for bulk insertion speed
From 50578eca1266d4e6472ce7053adb3bfd3e62080b Mon Sep 17 00:00:00 2001
From: Philip Zimmermann
Date: Sat, 13 Dec 2025 18:32:21 +0100
Subject: [PATCH 12/19] Added test png for png size change analyzer
---
examples/size_change_png.png | Bin 0 -> 63291 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 examples/size_change_png.png
diff --git a/examples/size_change_png.png b/examples/size_change_png.png
new file mode 100644
index 0000000000000000000000000000000000000000..064c96a7c64cf3cff4d39e779b5c273664b52496
GIT binary patch
literal 63291
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