Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 15 additions & 16 deletions faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@
# Must set before importing PySide6
os.environ["QT_LOGGING_RULES"] = "qt.qpa.mime.warning=false"

# Type Aliases for readability
DeletePair = Tuple[Optional[Path], Optional[Path]] # (src_path, recycle_bin_path)
DeleteRecord = Tuple[DeletePair, DeletePair] # (jpg_pair, raw_pair)

import concurrent.futures
import threading
import subprocess
Expand All @@ -41,7 +37,7 @@
QMimeData,
Qt,
QPoint,
QCoreApplication,
QCoreApplication, # noqa: F401 — patched by tests
)
from PySide6.QtWidgets import QApplication, QFileDialog
from PySide6.QtQml import QQmlApplicationEngine
Expand All @@ -52,7 +48,10 @@
from faststack.config import config
from faststack.logging_setup import setup_logging
from faststack.models import ImageFile, DecodedImage
from faststack.io.indexer import find_images, find_images_with_variants
from faststack.io.indexer import (
find_images,
find_images_with_variants,
) # noqa: F401 — find_images patched by tests
from faststack.io.variants import (
VariantGroup,
build_badge_list,
Expand Down Expand Up @@ -6554,10 +6553,15 @@ def execute_crop(self):
)

# Coerce elements to int and clamp to [0, 1000]
l, t, r, b = [max(0, min(1000, int(x))) for x in crop_box_raw]
left, top, right, bottom = [max(0, min(1000, int(x))) for x in crop_box_raw]

# Ensure correct order (left <= right, top <= bottom)
crop_box_raw = (min(l, r), min(t, b), max(l, r), max(t, b))
crop_box_raw = (
min(left, right),
min(top, bottom),
max(left, right),
max(top, bottom),
)

except (ValueError, TypeError, AttributeError) as e:
log.warning("Invalid crop box format: %s", e)
Expand Down Expand Up @@ -7536,11 +7540,7 @@ def _collect_active_bins(self) -> set:
if p.exists() and p.is_dir() and p not in pending_bins
}
local_bin = self.image_dir / "image recycle bin"
if (
local_bin.exists()
and local_bin.is_dir()
and local_bin not in pending_bins
):
if local_bin.exists() and local_bin.is_dir() and local_bin not in pending_bins:
active.add(local_bin)
return active

Expand Down Expand Up @@ -7696,9 +7696,8 @@ def _record_stale(record: DeleteRecord) -> bool:
"""True if any recycled path in this record was restored."""
(_, jpg_bin), (_, raw_bin) = record
return (
(jpg_bin is not None and jpg_bin.resolve() in resolved_restored)
or (raw_bin is not None and raw_bin.resolve() in resolved_restored)
)
jpg_bin is not None and jpg_bin.resolve() in resolved_restored
) or (raw_bin is not None and raw_bin.resolve() in resolved_restored)

self.delete_history = [
r for r in self.delete_history if not _record_stale(r)
Expand Down
4 changes: 2 additions & 2 deletions faststack/config.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
"""Manages application configuration via an INI file."""

import configparser
import logging
import sys
import glob
import logging
import os
import re
import sys
from pathlib import PureWindowsPath

from faststack.logging_setup import get_app_data_dir
Expand Down
4 changes: 1 addition & 3 deletions faststack/deletion_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@

import threading
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Any, List, Optional, Tuple


from enum import Enum


class DeletionErrorCodes(str, Enum):
"""Standardized error codes for deletion failures."""

Expand Down
5 changes: 3 additions & 2 deletions faststack/imaging/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

import inspect
import logging
import threading
import time
from pathlib import Path
from typing import Any, Callable, Optional, Union
import time
import threading

from cachetools import Cache, LRUCache

log = logging.getLogger(__name__)
Expand Down
21 changes: 10 additions & 11 deletions faststack/imaging/editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@
import time
import uuid
from pathlib import Path
from typing import Optional, Dict, Any, List, Tuple
from typing import Any, Dict, List, Optional, Tuple

import numpy as np
from PIL import Image, ImageFilter, ImageOps, ExifTags
from PIL import ExifTags, Image, ImageFilter, ImageOps

from faststack.models import DecodedImage
from faststack.imaging.math_utils import (
_srgb_to_linear,
_linear_to_srgb,
_smoothstep01,
_apply_headroom_shoulder,
_analyze_highlight_state,
_lerp,
_highlight_recover_linear,
_apply_headroom_shoulder,
_highlight_boost_linear,
_highlight_recover_linear,
_lerp,
_linear_to_srgb,
_smoothstep01,
_srgb_to_linear,
)
from faststack.imaging.orientation import get_exif_orientation, apply_orientation_to_np
from faststack.imaging.orientation import apply_orientation_to_np, get_exif_orientation
from faststack.models import DecodedImage

try:
from PySide6.QtGui import QImage
Expand All @@ -34,7 +34,6 @@

from faststack.imaging.optional_deps import cv2


log = logging.getLogger(__name__)

# Aspect Ratios for cropping
Expand Down
7 changes: 2 additions & 5 deletions faststack/imaging/math_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import numpy as np
from typing import Optional

import numpy as np

# ----------------------------
# sRGB ↔ Linear Conversion Helpers
# ----------------------------
Expand Down Expand Up @@ -200,10 +201,6 @@ def _highlight_recover_linear(
# Use max-channel as brightness metric - handles saturated highlights better than luminance
brightness = rgb_linear.max(axis=2)

# Build smooth highlight mask: 0 below pivot, 1 in highlights
# Use headroom_ceiling instead of 1.0 for the normalization range
mask = _smoothstep01((brightness - pivot) / (headroom_ceiling - pivot + eps))

# Highlights recovery: we want to pull down highlights to reveal detail.
# Rational compression formula: y = x / (1 + kx).
# We apply this relative to the pivot.
Expand Down
3 changes: 2 additions & 1 deletion faststack/imaging/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from fractions import Fraction
from pathlib import Path
from typing import Any, Dict, Optional, Union
from PIL import Image, ExifTags

from PIL import ExifTags, Image

log = logging.getLogger(__name__)

Expand Down
1 change: 1 addition & 0 deletions faststack/imaging/orientation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
from pathlib import Path
from typing import Optional

import numpy as np
from PIL import Image

Expand Down
23 changes: 12 additions & 11 deletions faststack/imaging/prefetch.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
"""Handles prefetching and decoding of adjacent images in a background thread pool."""

import logging
import os
import io
import hashlib
import io
import logging
import mmap
from pathlib import Path
from concurrent.futures import Future
from typing import List, Dict, Optional, Callable
import os
import threading
import time

from concurrent.futures import Future
from pathlib import Path
from typing import Callable, Dict, List, Optional

import numpy as np
from PIL import Image as PILImage, ImageCms
from PIL import Image as PILImage
from PIL import ImageCms

try:
from PySide6.QtCore import QTimer
Expand All @@ -22,11 +22,11 @@
QTimer = None
QImage = None

from faststack.models import ImageFile, DecodedImage
from faststack.imaging.jpeg import decode_jpeg_rgb, decode_jpeg_resized
from faststack.config import config
from faststack.imaging.cache import build_cache_key
from faststack.imaging.jpeg import decode_jpeg_resized, decode_jpeg_rgb
from faststack.imaging.orientation import apply_orientation_to_np
from faststack.config import config
from faststack.models import DecodedImage, ImageFile
from faststack.util.executors import create_daemon_threadpool_executor

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -103,6 +103,7 @@ def _make_raw_placeholder(width: int, height: int) -> np.ndarray:

return np.array(img)


# ---- Option C: ICC Color Management Setup ----
SRGB_PROFILE = ImageCms.createProfile("sRGB")

Expand Down
2 changes: 1 addition & 1 deletion faststack/imaging/turbo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
log = logging.getLogger(__name__)

try:
from turbojpeg import TurboJPEG, TJPF_RGB
from turbojpeg import TJPF_RGB, TurboJPEG
except ImportError: # pragma: no cover - exercised via create_turbojpeg
TurboJPEG = None
TJPF_RGB = None
Expand Down
2 changes: 1 addition & 1 deletion faststack/io/deletion.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import logging
from pathlib import Path
from PySide6.QtWidgets import QMessageBox

from PySide6.QtWidgets import QMessageBox

log = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions faststack/io/indexer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
import re
import time
from pathlib import Path
from typing import List, Dict, Tuple
from typing import Dict, List, Tuple

from faststack.models import ImageFile
from faststack.io.variants import (
VariantGroup,
build_variant_map,
norm_path,
parse_variant_stem,
)
from faststack.models import ImageFile

log = logging.getLogger(__name__)

Expand Down
6 changes: 4 additions & 2 deletions faststack/io/sidecar.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
from typing import Literal, Optional, Union, overload

from faststack.io.indexer import JPG_EXTENSIONS, RAW_EXTENSIONS
from faststack.models import Sidecar, EntryMetadata
from faststack.models import EntryMetadata, Sidecar

log = logging.getLogger(__name__)
KNOWN_IMAGE_EXTENSIONS = frozenset(ext.lower() for ext in JPG_EXTENSIONS | RAW_EXTENSIONS)
KNOWN_IMAGE_EXTENSIONS = frozenset(
ext.lower() for ext in JPG_EXTENSIONS | RAW_EXTENSIONS
)


def _entrymetadata_from_json(meta: dict) -> EntryMetadata:
Expand Down
1 change: 1 addition & 0 deletions faststack/io/variants.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple

from faststack.io.utils import normalize_path_key as norm_path

# Token-boundary regex: match `-developed` as a real dash-delimited token.
Expand Down
2 changes: 1 addition & 1 deletion faststack/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import dataclasses
from pathlib import Path
from typing import Any, Optional, Dict, List
from typing import Any, Dict, List, Optional


@dataclasses.dataclass
Expand Down
2 changes: 1 addition & 1 deletion faststack/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ ApplicationWindow {
if (uiState && uiState.isGridViewActive) return ""
var ratio = zs / fs
if (Math.abs(ratio - 1.0) < 0.03) return "Zoom: Fit to window (" + Math.round(zs * 100) + "%)"
return "Zoom: " + Math.round(ratio * 100) + "%"
return "Zoom: " + Math.round(zs * 100) + "%"
}
}

Expand Down
6 changes: 4 additions & 2 deletions faststack/tests/benchmark_decode.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import time
import io
import time

import numpy as np
from PIL import Image
from faststack.imaging.jpeg import decode_jpeg_resized, TURBO_AVAILABLE

from faststack.imaging.jpeg import TURBO_AVAILABLE, decode_jpeg_resized


def create_test_jpeg(width=6000, height=4000):
Expand Down
10 changes: 6 additions & 4 deletions faststack/tests/benchmark_decode_bilinear.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import time
import io
import time

import numpy as np
from PIL import Image

from faststack.imaging.jpeg import (
decode_jpeg_rgb,
_get_turbojpeg_scaling_factor,
TURBO_AVAILABLE,
JPEG_DECODER,
TJPF_RGB,
TURBO_AVAILABLE,
_get_turbojpeg_scaling_factor,
decode_jpeg_rgb,
)


Expand Down
Loading
Loading