updates to transparency filtering, fixing mass tag edit autocomplete

This commit is contained in:
Bryan Van Deusen
2026-01-21 08:36:46 -05:00
parent ede1457abc
commit fa9b89eca2
6 changed files with 111 additions and 81 deletions
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(python:*)"
]
}
}
+32 -29
View File
@@ -822,46 +822,49 @@ def get_tag_image_count(tag_id):
@main.get("/api/import-settings")
def get_import_settings():
"""Get current import filter settings."""
# Load from file first, fall back to env defaults
import json
settings_path = "/import/settings.json"
"""Get current import filter settings from database."""
from app.utils.version_migration import (
get_setting_int, get_setting_float, get_setting_bool
)
settings = {
"min_width": int(os.environ.get("IMPORT_MIN_WIDTH", "0")),
"min_height": int(os.environ.get("IMPORT_MIN_HEIGHT", "0")),
"skip_transparent": os.environ.get("IMPORT_SKIP_TRANSPARENT", "false").lower() == "true",
"transparency_threshold": float(os.environ.get("IMPORT_TRANSPARENCY_THRESHOLD", "0.9")),
"phash_threshold": int(os.environ.get("IMPORT_PHASH_THRESHOLD", "2")),
"min_width": get_setting_int("import_min_width", int(os.environ.get("IMPORT_MIN_WIDTH", "0"))),
"min_height": get_setting_int("import_min_height", int(os.environ.get("IMPORT_MIN_HEIGHT", "0"))),
"skip_transparent": get_setting_bool("import_skip_transparent", os.environ.get("IMPORT_SKIP_TRANSPARENT", "false").lower() == "true"),
"transparency_threshold": get_setting_float("import_transparency_threshold", float(os.environ.get("IMPORT_TRANSPARENCY_THRESHOLD", "0.9"))),
"phash_threshold": get_setting_int("phash_threshold", int(os.environ.get("IMPORT_PHASH_THRESHOLD", "10"))),
}
try:
if os.path.exists(settings_path):
with open(settings_path, "r") as f:
file_settings = json.load(f)
settings.update(file_settings)
except Exception:
pass
return jsonify(ok=True, settings=settings)
@main.post("/api/import-settings")
def save_import_settings():
"""
Save import filter settings.
Settings are stored in /import/settings.json and loaded by the importer.
Save import filter settings to database.
Settings are loaded by the Celery workers via load_import_settings().
"""
settings = {
"min_width": request.form.get("min_width", 0, type=int),
"min_height": request.form.get("min_height", 0, type=int),
"skip_transparent": request.form.get("skip_transparent") == "true",
"transparency_threshold": request.form.get("transparency_threshold", 0.9, type=float),
"phash_threshold": request.form.get("phash_threshold", 2, type=int),
}
from app.utils.version_migration import set_setting
min_width = request.form.get("min_width", 0, type=int)
min_height = request.form.get("min_height", 0, type=int)
skip_transparent = request.form.get("skip_transparent") == "true"
transparency_threshold = request.form.get("transparency_threshold", 0.9, type=float)
phash_threshold = request.form.get("phash_threshold", 10, type=int)
import json
settings_path = "/import/settings.json"
try:
with open(settings_path, "w") as f:
json.dump(settings, f, indent=2)
set_setting("import_min_width", str(min_width))
set_setting("import_min_height", str(min_height))
set_setting("import_skip_transparent", str(skip_transparent).lower())
set_setting("import_transparency_threshold", str(transparency_threshold))
set_setting("phash_threshold", str(phash_threshold))
settings = {
"min_width": min_width,
"min_height": min_height,
"skip_transparent": skip_transparent,
"transparency_threshold": transparency_threshold,
"phash_threshold": phash_threshold,
}
return jsonify(ok=True, settings=settings)
except Exception as e:
return jsonify(ok=False, error=str(e)), 500
+9 -1
View File
@@ -262,7 +262,15 @@
// Autocomplete
// ---------------------------
function setupAutocomplete() {
if (!dom.tagInput || !dom.autocomplete) return;
if (!dom.tagInput) return;
// Move autocomplete dropdown to body to escape transform containing block
// (CSS transform on parent creates new stacking context that breaks fixed positioning)
if (dom.autocomplete && dom.autocomplete.parentNode !== document.body) {
document.body.appendChild(dom.autocomplete);
}
if (!dom.autocomplete) return;
// Show autocomplete on focus
dom.tagInput.addEventListener('focus', () => {
+13 -29
View File
@@ -1509,31 +1509,24 @@ select.form-input optgroup {
}
/*------------------------------------------------------------------------------
Gallery Header Layout
Floating Select Button (Gallery)
------------------------------------------------------------------------------*/
.gallery-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 1rem;
margin-bottom: 1rem;
.floating-select-btn {
position: fixed;
top: 0.75rem;
right: 1rem;
z-index: 1001;
}
.gallery-header-left {
display: flex;
flex-direction: column;
gap: 0.5rem;
.floating-select-btn .btn {
box-shadow: var(--shadow-2);
}
.gallery-header-left h1 {
margin: 0;
}
.gallery-header-actions {
display: flex;
gap: 0.5rem;
align-items: center;
@media (max-width: 640px) {
.floating-select-btn {
top: 0.5rem;
right: 0.5rem;
}
}
/*------------------------------------------------------------------------------
@@ -1723,13 +1716,4 @@ body.select-mode .gallery-item:hover {
.bulk-editor-panel.active {
transform: translateY(0);
}
.gallery-header {
flex-direction: column;
align-items: stretch;
}
.gallery-header-actions {
justify-content: flex-end;
}
}
+12 -12
View File
@@ -3,6 +3,11 @@
{% block title %}Gallery{% endblock %}
{% block content %}
<!-- Floating Select Button (in navbar area) -->
<div id="floatingSelectBtn" class="floating-select-btn">
<button id="selectModeBtn" class="btn secondary-btn">Select</button>
</div>
<div class="gallery-infinite-container">
<!-- Timeline Scrollbar (left sidebar) -->
<aside id="timelineSidebar" class="timeline-sidebar">
@@ -15,18 +20,13 @@
<!-- Main Gallery Content -->
<main class="gallery-main">
<div class="gallery-header">
<div class="gallery-header-left">
<h1>Gallery</h1>
{% if active_tag %}
<div class="active-filter">
Filtered by: <span class="filter-tag">{{ active_tag }}</span>
<a href="{{ url_for('main.gallery') }}" class="clear-filter" title="Clear filter">×</a>
</div>
{% endif %}
</div>
<div class="gallery-header-actions">
<button id="selectModeBtn" class="btn secondary-btn">Select</button>
</div>
<h1>Gallery</h1>
{% if active_tag %}
<div class="active-filter">
Filtered by: <span class="filter-tag">{{ active_tag }}</span>
<a href="{{ url_for('main.gallery') }}" class="clear-filter" title="Clear filter">×</a>
</div>
{% endif %}
</div>
<!-- Gallery Grid with Date Sections -->
+38 -10
View File
@@ -105,26 +105,54 @@ def is_mostly_transparent(file_path: str, threshold: float = 0.9) -> bool:
"""
Check if an image is mostly transparent (> threshold % of pixels are transparent).
Returns False for images without alpha channel.
Args:
file_path: Path to the image file
threshold: Percentage of pixels that must be transparent (0.0-1.0, default 0.9 = 90%)
Returns:
True if the image has > threshold% transparent pixels
"""
try:
with Image.open(file_path) as img:
if img.mode not in ("RGBA", "LA", "P"):
original_mode = img.mode
# Handle different image modes that may have transparency
if original_mode == "RGBA":
# Already has alpha channel
pass
elif original_mode == "LA":
# Grayscale with alpha
img = img.convert("RGBA")
elif original_mode == "P":
# Palette mode - check for transparency
if "transparency" in img.info:
img = img.convert("RGBA")
else:
return False
elif original_mode == "PA":
# Palette with alpha
img = img.convert("RGBA")
else:
# RGB, L, etc. - no alpha channel
return False
# Convert palette images with transparency
if img.mode == "P" and "transparency" in img.info:
img = img.convert("RGBA")
elif img.mode == "LA":
img = img.convert("RGBA")
elif img.mode != "RGBA":
# Get alpha channel
bands = img.split()
if len(bands) < 4:
return False
# Get alpha channel and count transparent pixels
alpha = img.split()[-1]
alpha = bands[3] # Alpha is the 4th channel (index 3)
alpha_data = alpha.getdata()
total_pixels = alpha.size[0] * alpha.size[1]
if total_pixels == 0:
return False
# Count pixels where alpha < 128 (more than 50% transparent)
transparent_count = sum(1 for p in alpha.getdata() if p < 128)
transparent_count = sum(1 for p in alpha_data if p < 128)
transparency_ratio = transparent_count / total_pixels
return transparency_ratio > threshold
except Exception as e:
print(f"[WARN] Failed to check transparency for {file_path}: {e}")