Documentation Index
Fetch the complete documentation index at: https://mintlify.com/xcoder-es/media-cleaner-pro/llms.txt
Use this file to discover all available pages before exploring further.
Every image processed by MediaCleaner Pro passes through the same 10 stages in the same order. All 10 stages run for every image — stages do not short-circuit. After all stages have evaluated the image, the routing logic selects the first stage whose condition was met (Stages 1–8) as the output destination. If no rejection stage fired, the AI Classification result (Stage 9) is used instead. Stage 10 (Quality Ranking) always runs and records a score, but its quality/ destination is used only when neither a rejection stage nor AI Classification has claimed the file. All stage logic lives in StageProcessor inside mc-core/src/services.rs.
Stage 1: Exact Duplicate Removal
What it detects: Byte-for-byte identical image files, regardless of filename or location.
Detection algorithm: A SHA-256 hash is computed for every image during the hash-worker phase. Stage 1 checks whether that hash already exists in a shared HashSet<String>. If it does, the file is a duplicate. If it does not, the hash is inserted so subsequent images can match against it.
let is_duplicate = seen_hashes.contains(&meta.sha256);
Because SHA-256 produces a 256-bit digest, false positives are cryptographically impossible. Two files match only when their raw bytes are identical.
Configuration parameters: None. Exact duplicate detection cannot be disabled or adjusted.
Output destination: duplicates/exact/
What gets caught: The same photo copied to multiple folders, camera bursts that produced identical output frames, or re-downloaded assets with different filenames but identical content.
Stage 2: Perceptual Duplicate Removal
What it detects: Images that are visually near-identical even when they differ at the byte level — different compression settings, minor crops, colour adjustments, or format conversions.
Detection algorithm: A dHash (difference hash) is computed for each image: the image is downsampled to 9×8 grayscale and each pixel is compared to its right neighbour, producing a 64-bit binary fingerprint. Stage 2 then measures the Hamming distance (number of differing bits) between the new image’s dHash and the dHashes of all previously seen images.
To avoid an O(n²) comparison, hashes are stored in a bucket map keyed by the top 16 bits of the hash (the prefix). When checking for near-neighbours, the detector queries three adjacent buckets: prefix - 1, prefix, and prefix + 1. This covers the vast majority of near-matches while keeping lookup sub-linear.
let prefix = (dhash >> 48) as u16;
// Checks buckets: prefix.wrapping_sub(1), prefix, prefix.wrapping_add(1)
for (existing_path, existing_hash) in &candidates {
let distance = hamming_distance(dhash, *existing_hash);
if distance <= self.threshold {
duplicates.push(existing_path.clone());
}
}
Configuration parameters:
| Variable | Default | Range | Effect |
|---|
HAMMING_THRESHOLD | 4 | 0–64 | Maximum number of differing bits before two images are considered distinct. Lower values require a closer visual match; 0 requires bit-for-bit identical dHashes. |
Output destination: duplicates/perceptual/
What gets caught: Resaved JPEGs at different quality levels, thumbnails that were re-exported at a slightly different size, watermarked versions of the same photo, or screenshots taken moments apart with minimal UI change.
Stage 3: Tiny Image Detection
What it detects: Images too small to be useful — decorators, spacers, tiny icons that slipped past Stage 4, or aggressively downsampled previews.
Detection algorithm: A simple dimension check against configurable minimum thresholds. The condition uses OR so that an image fails if either dimension is below threshold — a tall narrow image that is only 50 px wide is flagged even if its height is 2000 px.
let is_tiny = meta.width < min_width || meta.height < min_height;
If flagged, the rejection reason includes the actual dimensions and the threshold, for example: 128x64 below threshold 100x100.
Configuration parameters:
| Variable | Default | Description |
|---|
MIN_WIDTH | 100 | Minimum acceptable width in pixels |
MIN_HEIGHT | 100 | Minimum acceptable height in pixels |
Output destination: rejected/tiny/
What gets caught: Single-pixel tracking images, 32×32 favicons, CSS sprite fragments, low-resolution preview thumbnails auto-saved by older software.
Stage 4: Icon Detection
What it detects: Application icons, UI glyphs, and favicon-style images — square, small assets that are not useful photographs.
Detection algorithm: Two conditions must both be true:
- Square aspect ratio — the width-to-height ratio falls between 0.9 and 1.1 (inclusive).
- Small size — both dimensions are at or below 256 px.
let aspect = meta.width as f32 / meta.height.max(1) as f32;
let is_square = (0.9..=1.1).contains(&aspect);
let is_small = meta.width <= 256 && meta.height <= 256;
let is_icon = is_square && is_small;
The 10 % aspect tolerance handles icons that are stored at non-power-of-two dimensions (e.g. 48×52).
Configuration parameters: None. Icon detection is toggled via detect_icons in PipelineConfig (default true).
Output destination: categories/icons/
What gets caught: App bundle icons (icon.png, favicon.ico extracted frames), toolbar button assets, emoji image files, and small UI element exports.
Stage 5: Thumbnail Detection
What it detects: Reduced-size preview images generated by cameras, content management systems, image editors, or web applications.
Detection algorithm: A thumbnail is flagged if either of these conditions holds:
- Name-based match — the filename (lowercased) contains any of:
thumb, thumbnail, small, mini, preview, tn_
- Size-based match — width ≤ 320 px or height ≤ 240 px, and the image is not taller-than-wide by a 2:1 margin (to avoid catching valid portrait photos at small sizes)
let has_thumb_name = thumb_patterns.iter().any(|p| name_lower.contains(p));
let is_small = meta.width <= 320 || meta.height <= 240;
let is_thumbnail = has_thumb_name || (is_small && meta.width < meta.height * 2);
Configuration parameters: None. Thumbnail detection is toggled via detect_thumbnails in PipelineConfig (default true).
Output destination: categories/thumbnails/
What gets caught: WordPress-generated image-300x200.jpg variants, Lightroom smart preview exports, CMS-auto-resized thumbnail.jpg files, and camera-embedded preview frames extracted to disk.
Stage 6: Screenshot Detection
What it detects: Screen captures from desktops, laptops, phones, and tablets — images whose dimensions exactly match a common display resolution or whose filename indicates a screenshot.
Detection algorithm: A match on either of two checks:
-
Resolution match — the image’s width × height (or height × width for portrait captures) matches any of 16 preset screen resolutions:
| Resolution | Common device/use |
|---|
| 1920 × 1080 | Full HD desktop/laptop |
| 1366 × 768 | Common laptop |
| 1440 × 900 | MacBook Air (older) |
| 1536 × 864 | Windows scaled HD |
| 1280 × 720 | HD / older display |
| 1600 × 900 | HD+ laptop |
| 2560 × 1440 | QHD / 27” display |
| 3840 × 2160 | 4K UHD |
| 1280 × 1024 | Legacy 5:4 display |
| 1024 × 768 | Legacy XGA / tablet |
| 1680 × 1050 | WSXGA+ display |
| 1920 × 1200 | WUXGA display |
| 1440 × 960 | — |
| 2560 × 1600 | MacBook Pro (older) |
| 2880 × 1800 | MacBook Pro Retina |
| 3024 × 1964 | MacBook Pro 14-inch |
-
Name-based match — the filename (lowercased) contains
screenshot, screen shot, or screencapture.
let is_common_res = common_resolutions.iter().any(|(w, h)| {
(meta.width == *w && meta.height == *h) || (meta.width == *h && meta.height == *w)
});
let has_screenshot_name = name_lower.contains("screenshot")
|| name_lower.contains("screen shot")
|| name_lower.contains("screencapture");
Configuration parameters: None. Screenshot detection is toggled via detect_screenshots in PipelineConfig (default true).
Output destination: categories/screenshots/
What gets caught: macOS Screenshot 2024-03-01 at 10.00.00.png files, Windows screenshot.png Snipping Tool saves, Android Screenshot_20240301.jpg captures, and any image that was saved at a known-display resolution.
Stage 7: Wallpaper Detection
What it detects: Desktop wallpapers and large landscape images optimised for display backgrounds — either ultra-wide format or high-resolution 4K+ images.
Detection algorithm: Two independent conditions, either of which triggers the stage:
- Ultrawide aspect ratio — width ÷ height falls between 1.8 and 3.5 (inclusive). This covers 16:9 (1.78), 21:9 (2.33), and panoramic images.
- 4K+ with wide aspect — width ≥ 3840 px or height ≥ 2160 px, and the aspect ratio is ≥ 1.5 (landscape orientation at 3:2 or wider).
let aspect = meta.width as f32 / meta.height.max(1) as f32;
let is_ultrawide = (1.8..=3.5).contains(&aspect);
let is_4k = meta.width >= 3840 || meta.height >= 2160;
let is_wallpaper = is_ultrawide || (is_4k && aspect >= 1.5);
Configuration parameters: None. Wallpaper detection is toggled via detect_wallpapers in PipelineConfig (default true).
Output destination: categories/wallpapers/
What gets caught: Desktop background packs (3840×2160 landscape photos), dual-monitor panoramic wallpapers (5120×1440), downloaded “HD wallpaper” site images, and ultra-wide game screenshots.
Stage 8: Document Detection
What it detects: Scanned documents, PDF-to-image exports, and photographed pages whose proportions match standard paper sizes.
Detection algorithm: Two conditions must both be true:
-
Paper aspect ratio — the image’s aspect ratio (or its reciprocal, to handle portrait orientation) falls within ±0.15 of any of four standard paper ratios:
| Paper standard | Ratio |
|---|
| A4 (ISO 216) | 1.414 (√2) |
| US Letter | 1.294 |
| US Legal | 1.545 |
| Square / CD | 1.0 |
-
High resolution — width ≥ 1200 px, ensuring low-resolution letter-shaped images (small icons, avatars) are not mistakenly flagged.
let paper_ratios = [1.414, 1.294, 1.545, 1.0];
let is_paper_ratio = paper_ratios
.iter()
.any(|r| (aspect - *r).abs() < 0.15 || ((1.0 / aspect) - *r).abs() < 0.15);
let is_high_res = meta.width >= 1200;
let is_document = is_paper_ratio && is_high_res;
Configuration parameters: None. Document detection is toggled via detect_documents in PipelineConfig (default true).
Output destination: categories/documents/
What gets caught: Scanned invoices and contracts (A4/Letter portrait), photographed whiteboards, PDF slide exports, and book page scans.
Stage 9: AI Classification
What it detects: The dominant content category of images that have not been claimed by any earlier stage. This stage always passes — every image receives a classification, but none are rejected.
Detection algorithm: A heuristic scoring function evaluates the image’s aspect ratio and megapixel count, assigning confidence scores to each candidate category. The category with the highest score becomes the primary classification.
| Category | Condition | Score |
|---|
portrait | aspect < 0.8 | 0.7 |
landscape | aspect > 1.3 and megapixels > 2.0 | 0.6 |
macro | megapixels > 5.0 and 1.0 < aspect < 1.5 | 0.5 |
low_quality | megapixels < 0.5 | 0.8 |
panorama | aspect > 2.5 | 0.9 |
social | 0.95 < aspect < 1.05 | 0.6 |
if aspect < 0.8 { scores.push(("portrait", 0.7)); }
if aspect > 1.3 && mp > 2.0 { scores.push(("landscape", 0.6)); }
if mp > 5.0 && aspect > 1.0 && aspect < 1.5 { scores.push(("macro", 0.5)); }
if mp < 0.5 { scores.push(("low_quality", 0.8)); }
if aspect > 2.5 { scores.push(("panorama", 0.9)); }
if aspect > 0.95 && aspect < 1.05 { scores.push(("social", 0.6)); }
Scores are sorted descending; the top category is used as the destination subdirectory. Images that match no rule receive the label uncategorized.
Configuration parameters: Toggled via classification_enabled in PipelineConfig (default true).
Output destination: categories/{category}/ — for example, categories/portrait/, categories/landscape/, categories/panorama/.
What gets caught (examples):
- A 2000×3000 px phone photo →
portrait (aspect 0.67)
- A 4000×2500 px DSLR landscape →
landscape (aspect 1.6, 10 MP)
- A 5000×3500 px detailed close-up →
macro (17.5 MP, aspect 1.43)
- A 8000×2000 px stitched cityscape →
panorama (aspect 4.0)
- A 1080×1080 px Instagram export →
social (aspect 1.0)
Stage 10: Quality Ranking
What it detects: Nothing is rejected — every image that reaches this stage receives a composite quality score (0–100) and is placed in the corresponding quality tier directory.
Detection algorithm: The score is built from four additive components:
| Component | Calculation | Max points |
|---|
| Size score | (file_size_bytes ÷ (megapixels × 500,000)).min(1.0) × 30 | 30 |
| Resolution score | (megapixels ÷ 20).min(1.0) × 40 | 40 |
| Aspect score | 15 if landscape (width > height), otherwise 10 | 15 |
| Base | Fixed | 15 |
let mp = (meta.width * meta.height) as f32 / 1_000_000.0;
let size_score = (meta.size_bytes as f32 / (mp * 500_000.0)).min(1.0) * 30.0;
let res_score = mp.min(20.0) / 20.0 * 40.0;
let aspect_score = if meta.width > meta.height { 15.0 } else { 10.0 };
let total = (size_score + res_score + aspect_score + 15.0).min(100.0);
The final score maps to a quality bucket:
| Bucket | Score range |
|---|
excellent | ≥ 90 |
good | 75–89 |
average | 50–74 |
below_average | 25–49 |
poor | < 25 |
Configuration parameters: Toggled via quality_ranking_enabled in PipelineConfig (default true).
Output destination: quality/{bucket}/ — for example, quality/excellent/, quality/poor/.
What gets caught (examples):
- A 20 MP RAW-exported JPEG with large file size → likely
excellent or good
- A 2 MP photo saved at high compression → likely
average
- A 0.3 MP image (below 0.5 MP threshold) → low resolution score, likely
below_average or poor
Stages 9 and 10 both always pass and both always run for every image. The file routing logic selects the first failed stage (Stages 1–8) as the destination, then falls back to Stage 9’s categories/ result. Stage 10’s quality/ destination is used only when neither a rejection stage nor Stage 9 has claimed the file — in practice this means quality/ directories are populated only when the image matches no rejection condition, no AI classification category can be assigned, and the fallback reaches Stage 10.