diff --git a/CHANGELOG.md b/CHANGELOG.md index 6525de0..480b5e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). ## [Unreleased] +## [1.0.7] - 2026-09-26 + +### Behoben / Fixed (Store-Icon weißer Rand — T-20260820-729932431) +- **Store-Kachel-Icons mit sichtbarem weißen Rand (`store_assets/Square44x44Logo.png`, `Square150x150Logo.png`, `Square310x310Logo.png`, `StoreLogo.png`, `Wide310x150Logo.png`)**: Diese Dateien hatten trotz RGBA-Modus einen opaken (nicht transparenten) Canvas-Hintergrund gebacken (Alpha 255 statt 0 rund um das Motiv, bei `Wide310x150Logo.png` im eingebetteten 150px-Mittelfeld) — sichtbar als weißer Kasten/Halo um das Icon-Motiv in Kachel, Startmenü, Taskleiste und Store-Listing. + - **Nachreview (merge-reviewer, claude-opus, Commit `aee6401`) fand einen Mangel** in einem ersten, per Direkt-Push auf `master` eingespielten Versuch (in 1.0.6 nicht mehr zurückgenommen, siehe unten): Die dort verwendeten Ersatzdateien (`icon_44x44.png` u. a.) zeigen eine ANDERE Gestaltung (gerahmte Variante mit cyanfarbenem Rahmen und weißer Sichel am Rand, kleineres Motiv) statt des ursprünglichen Vollflächen-Motivs — und `Wide310x150Logo.png` blieb dabei unentdeckt defekt. + - **Erster Korrekturversuch (PR #3, `edcb8fa`) noch nicht sauber:** Ursprüngliches Vollflächen-Motiv per Flood-Fill-Matting freigestellt, aber mit zu kleinem Farbdistanz-Schwellwert (60) — die eigentliche Antialiasing-Übergangszone am Rand der abgerundeten Ecke war nur 1 Pixel breit, hatte aber eine Farbdistanz von ~97 zum Hintergrund und blieb dadurch unverarbeitet: ein 1px heller Saum auf dunklem Grund (gemessen: bis zu 204 Randpixel mit Alpha ≥ 192 und Luminanz auf Schwarz bis 216, je nach Kachelgröße). + - **Korrigierter Fix (dieser Release):** Farbdistanz-Schwellwert auf 150 angehoben (deutlich über der gemessenen Übergangsdistanz von ~97, deutlich unter der Distanz zur soliden Motivfarbe von >300) — damit erfasst das Un-Premultiply die komplette Antialiasing-Übergangszone, kein Fransensaum mehr. Motiv unverändert (keine Neugestaltung). `icon_310x150.png` ist ein separates Marketing-Banner (nicht vom AppxManifest referenziert) und bleibt unverändert. Regressionstest `test_store_tiles_have_transparent_corners` um eine Luminanz-Prüfung auf Schwarz-Komposit erweitert, damit dieser Fehler künftig auffällt. Verifiziert per Kontaktbogen (5 Größen × Weiß/Schwarz/Grau/Akzentgrün) und Vergrößerung der abgerundeten Ecke: kein Halo, kein Fransensaum, keine hellen Randflächen. Per Pull Request gegen `master` (kein Direkt-Push). + ## [1.0.6] - 2026-09-26 ### Behoben / Fixed (Kontextmenü, Editor-Dirty-Flag, Qt-Übersetzung — T-20260926-912169808) @@ -14,9 +22,7 @@ Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.1.0/). - **Speichern/Verwerfen-Dialog auf Englisch in der deutschen Version (`src/main.py`)**: Kein Qt-eigener Übersetzer installiert. `QTranslator` mit `qtbase_.qm` ergänzt (unabhängig von der App-eigenen `TranslationSystem`, kein Overwrite). - **Mehrfach-Umbenennen/Diff/Einstellungen stürzen im Quellstart ab (`src/main.py`, `ExplorerPro.spec`)**: `translator.py` (Projektwurzel) fehlte auf `sys.path`; Spec um `pathex`/`datas` für Projektwurzel und `locales/` ergänzt. - **9 neue automatisierte Regressionstests** (`tests/test_context_menu_actions.py`), Vollsuite 359 passed / 2 skipped. Review/Merge als zweites Modell: Repo `file-bricks/ExplorerPro#1`, Squash `264aa6eae60055f1ffb4ff041025920738c6ead2`. - -### Behoben / Fixed (Store-Icon weißer Rand — T-20260820-729932431) -- **Store-Kachel-Icons mit sichtbarem weißen Rand (`store_assets/Square44x44Logo.png`, `Square150x150Logo.png`, `Square310x310Logo.png`, `StoreLogo.png`)**: Diese vier Dateien hatten trotz RGBA-Modus einen opaken (nicht transparenten) Canvas-Hintergrund gebacken (Alpha 255 an den Ecken statt 0) — sichtbar als weißer Kasten/Halo um das Icon-Motiv in Kachel, Startmenü und Store-Listing. Die im selben Ordner bereits korrekt transparenten Gegenstücke gleicher Größe und gleichen Motivs (`icon_44x44.png`, `icon_150x150.png`, `icon_310x310.png`, `icon_50x50.png`) haben die vier defekten Dateien ersetzt — Motiv unverändert (kein Redesign), nur echte Transparenz statt gebackenem Hintergrund. `Wide310x150Logo.png` war bereits korrekt; `icon_310x150.png` ist ein separates Marketing-Banner (nicht vom AppxManifest referenziert) und unverändert. Verifiziert per Kontaktbogen auf Weiß/Schwarz/Grau/Akzentgrün (kein Halo, keine hellen Ränder auf allen vier Hintergründen). +- **Bekannter Mangel dieses Releases:** Der ebenfalls in 1.0.6 enthaltene Icon-Fix-Versuch (direkt gepusht, `aee6401`) hatte das falsche Motiv und einen unentdeckt gebliebenen Defekt in `Wide310x150Logo.png` — die Store-Submission dieser Version läuft dennoch durch, weil die Kontextmenü-Fixes ankommen sollen; der korrigierte Icon-Fix folgt in 1.0.7. ### Behoben / Fixed (Bugsearch 2026-09-26) - **Batch-Renamer, Kaskadierende Kollisionserkennung, Windows-Gerätenamens-Schutz und Nummerierungs-Affix-Integrität (`src/core/batch_rename_service.py`)**: diff --git a/CLAUDE.md b/CLAUDE.md index b96c036..15379e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,7 @@ name: ExplorerPro type: project-docs profile: STANDARD -version: 1.0.6 +version: 1.0.7 created: 2026-07-22 updated: 2026-09-26 reason_last_change: "T-20260926-912169808: Kontextmenü-Tags/Metadaten, Editor-Dirty-Flag (modificationChanged), qtbase-Übersetzung, 359 Tests" diff --git a/README.md b/README.md index 33396bd..e593f23 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ [![Umbrella: open--bricks](https://img.shields.io/badge/umbrella-open--bricks-blue.svg)](https://github.com/open-bricks) [![Microsoft Store](https://img.shields.io/badge/Microsoft%20Store-Live-0078D7.svg?logo=windows)](https://apps.microsoft.com/detail/9P0X52WSHZ3Q) [![LLM-Ready: llms.txt](https://img.shields.io/badge/LLM--Ready-llms.txt-success.svg)](llms.txt) -[![Version: 1.0.6](https://img.shields.io/badge/version-1.0.6-orange.svg)](CHANGELOG.md) +[![Version: 1.0.7](https://img.shields.io/badge/version-1.0.7-orange.svg)](CHANGELOG.md) [![Last Checked](https://img.shields.io/badge/Last--Checked-2026--09--26-blue?style=flat-square)](CHANGELOG.md) > [!NOTE] diff --git a/README_de.md b/README_de.md index 353d997..a010a63 100644 --- a/README_de.md +++ b/README_de.md @@ -21,7 +21,7 @@ [![Dachverband: open--bricks](https://img.shields.io/badge/Dachverband-open--bricks-blue.svg)](https://github.com/open-bricks) [![Microsoft Store](https://img.shields.io/badge/Microsoft%20Store-Live-0078D7.svg?logo=windows)](https://apps.microsoft.com/detail/9P0X52WSHZ3Q) [![LLM-Ready: llms.txt](https://img.shields.io/badge/LLM--Ready-llms.txt-success.svg)](llms.txt) -[![Version: 1.0.6](https://img.shields.io/badge/Version-1.0.6-orange.svg)](CHANGELOG.md) +[![Version: 1.0.7](https://img.shields.io/badge/Version-1.0.7-orange.svg)](CHANGELOG.md) [![Stand](https://img.shields.io/badge/Stand-2026--09--26-blue?style=flat-square)](CHANGELOG.md) > [!NOTE] diff --git a/llms.txt b/llms.txt index f701d08..2fbaedd 100644 --- a/llms.txt +++ b/llms.txt @@ -8,7 +8,7 @@ License: AGPL-3.0 Microsoft Store Identity: Geiger.ExplorerPro (Store-ID: 9P0X52WSHZ3Q) Last-checked: 2026-09-26 -Version: 1.0.6 +Version: 1.0.7 ## Search Phrases diff --git a/pyproject.toml b/pyproject.toml index 3415b04..474115e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "explorerpro" -version = "1.0.6" +version = "1.0.7" description = "Datei-Explorer-Suite mit Editor, PDF-Viewer und Datenanalyse" authors = [{ name = "Lukas Geiger", email = "support@lukasgeiger.com" }] requires-python = ">=3.10" diff --git a/scripts/defringe_store_tile.py b/scripts/defringe_store_tile.py new file mode 100644 index 0000000..6690e94 --- /dev/null +++ b/scripts/defringe_store_tile.py @@ -0,0 +1,108 @@ +"""Freistellen: entfernt einen naeher-am-weissen Canvas-Hintergrund liegenden, +mit dem Bild verbundenen Bereich per Flood-Fill und entfernt dabei per +Un-Premultiply auch den weissen Fransen-/Halo-Saum an den Kanten. +Motiv bleibt unveraendert; nur Hintergrund/Rand wird transparent. + +bg_thresh muss die GESAMTE Antialiasing-Uebergangszone zwischen Hintergrund +und Motiv abdecken, nicht nur "nah am Hintergrund". Bei T-20260820-729932431 +war der Uebergang am Rand der abgerundeten Ecke nur 1 Pixel breit, aber mit +einer Farbdistanz von ~97 zum Hintergrund -- ein zu kleiner bg_thresh (60) +liess genau dieses Pixel unverarbeitet (voll opak, urspruengliche helle +Mischfarbe) zurueck: ein 1px heller Saum auf dunklem Grund. 150 hat sich +als sicherer Wert erwiesen (deutlich unter der Distanz zur soliden +Motivfarbe von >300, deutlich ueber der gemessenen Uebergangsdistanz).""" +import sys +import math +from collections import deque +from PIL import Image + + +def defringe(path, out_path, bg=None, bg_thresh=150): + im = Image.open(path).convert("RGBA") + w, h = im.size + px = im.load() + + if bg is None: + # Referenz-Hintergrund aus den vier Ecken (falls dort opak) oder Default-Weiss. + samples = [] + for cx, cy in ((0, 0), (w - 1, 0), (0, h - 1), (w - 1, h - 1)): + r, g, b, a = px[cx, cy] + if a > 0: + samples.append((r, g, b)) + if samples: + bg = tuple(sum(c[i] for c in samples) / len(samples) for i in range(3)) + else: + bg = (248.0, 247.0, 247.0) + + def dist(c): + return math.sqrt(sum((c[i] - bg[i]) ** 2 for i in range(3))) + + def passable(x, y): + r, g, b, a = px[x, y] + if a == 0: + return True, 0.0 + d = dist((r, g, b)) + if d < bg_thresh: + return True, d + return False, None + + visited = [[False] * w for _ in range(h)] + dist_map = [[0.0] * w for _ in range(h)] + q = deque() + + def seed(x, y): + if not visited[y][x]: + ok, d = passable(x, y) + if ok: + visited[y][x] = True + dist_map[y][x] = d + q.append((x, y)) + + for x in range(w): + seed(x, 0) + seed(x, h - 1) + for y in range(h): + seed(0, y) + seed(w - 1, y) + + while q: + x, y = q.popleft() + for nx, ny in ((x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)): + if 0 <= nx < w and 0 <= ny < h and not visited[ny][nx]: + ok, d = passable(nx, ny) + if ok: + visited[ny][nx] = True + dist_map[ny][nx] = d + q.append((nx, ny)) + + out = Image.new("RGBA", (w, h)) + outpx = out.load() + feather = bg_thresh + for y in range(h): + for x in range(w): + r, g, b, a = px[x, y] + if visited[y][x]: + d = dist_map[y][x] + alpha = int(round(255 * min(1.0, d / feather))) + if alpha <= 0: + outpx[x, y] = (0, 0, 0, 0) + else: + af = alpha / 255.0 + nr = (r - (1 - af) * bg[0]) / af + ng = (g - (1 - af) * bg[1]) / af + nb = (b - (1 - af) * bg[2]) / af + nr = max(0, min(255, round(nr))) + ng = max(0, min(255, round(ng))) + nb = max(0, min(255, round(nb))) + outpx[x, y] = (nr, ng, nb, alpha) + else: + outpx[x, y] = (r, g, b, 255) + out.save(out_path, "PNG") + return bg + + +if __name__ == "__main__": + src, dst = sys.argv[1], sys.argv[2] + thresh = float(sys.argv[3]) if len(sys.argv) > 3 else 60 + used_bg = defringe(src, dst, bg_thresh=thresh) + print("bg used:", used_bg, "->", dst) diff --git a/store_assets/Square150x150Logo.png b/store_assets/Square150x150Logo.png index 430cd9d..482e810 100644 Binary files a/store_assets/Square150x150Logo.png and b/store_assets/Square150x150Logo.png differ diff --git a/store_assets/Square310x310Logo.png b/store_assets/Square310x310Logo.png index 36df412..8660925 100644 Binary files a/store_assets/Square310x310Logo.png and b/store_assets/Square310x310Logo.png differ diff --git a/store_assets/Square44x44Logo.png b/store_assets/Square44x44Logo.png index 2400307..e18c37c 100644 Binary files a/store_assets/Square44x44Logo.png and b/store_assets/Square44x44Logo.png differ diff --git a/store_assets/StoreLogo.png b/store_assets/StoreLogo.png index a7e8b57..c6ca00a 100644 Binary files a/store_assets/StoreLogo.png and b/store_assets/StoreLogo.png differ diff --git a/store_assets/Wide310x150Logo.png b/store_assets/Wide310x150Logo.png index dcc16a5..dfa42dc 100644 Binary files a/store_assets/Wide310x150Logo.png and b/store_assets/Wide310x150Logo.png differ diff --git a/store_package.json b/store_package.json index c9bf073..a8564e1 100644 --- a/store_package.json +++ b/store_package.json @@ -3,7 +3,7 @@ "publisher": "CN=52596601-BAB4-4F3F-B182-E8F3F273B202", "publisher_display": "Geiger", "identity_name": "Geiger.ExplorerPro", - "version": "1.0.6.0", + "version": "1.0.7.0", "description": "Lokale Explorer-Suite mit Mehrtab-Dateibrowser, Vorschau, Datenschutzprüfung, Duplikatsuche und Offline-Workflow.", "executable": "ExplorerPro.exe", "capabilities": "runFullTrust", diff --git a/tests/test_assets_and_icons.py b/tests/test_assets_and_icons.py index 7885ff8..8a0e400 100644 --- a/tests/test_assets_and_icons.py +++ b/tests/test_assets_and_icons.py @@ -172,6 +172,79 @@ def test_store_assets_integrity(): assert img.size == size, f"{name} size {img.size} != {size}" +def _composited_on_black_luminance(rgba_pixel): + """Luminanz eines RGBA-Pixels, wenn er auf Schwarz kompositiert wird. + Ein teilweise opakes, aber urspruenglich helles Randpixel (unverarbeitete + Antialiasing-Uebergangsfarbe) faellt hier auf, auch wenn sein Alpha + nicht extrem hoch ist -- genau der T-20260820-729932431-Regressionsfall.""" + r, g, b, a = rgba_pixel + cr, cg, cb = r * a / 255.0, g * a / 255.0, b * a / 255.0 + return 0.299 * cr + 0.587 * cg + 0.114 * cb + + +def test_store_tiles_have_transparent_corners(): + """Regression fuer T-20260820-729932431: die vom AppxManifest referenzierten + Kacheln duerfen keinen gebackenen opaken Hintergrund haben (weisser Halo), + und der Antialiasing-Uebergang am Rand darf keinen hellen Fransensaum + hinterlassen (auf Schwarz kompositiert gemessen -- der urspruengliche Fix + liess genau ein 1px breites, zu schmal geschwellwertetes Uebergangspixel + unverarbeitet und damit sichtbar hell zurueck).""" + store_dir = REPO_ROOT / "store_assets" + manifest_tiles = [ + "Square44x44Logo.png", + "Square150x150Logo.png", + "Square310x310Logo.png", + "StoreLogo.png", + ] + max_ring_luminance = 200.0 + for name in manifest_tiles: + with Image.open(store_dir / name).convert("RGBA") as img: + w, h = img.size + corner_alpha = img.getpixel((0, 0))[3] + assert corner_alpha < 30, ( + f"{name}: Ecke (0,0) ist fast opak (Alpha {corner_alpha}) -- " + "gebackener Hintergrund statt Transparenz (weisser Halo)." + ) + for y in range(h): + for x in range(w): + px = img.getpixel((x, y)) + if px[3] == 0 or px[3] == 255: + continue # nicht Teil des Antialiasing-Uebergangs + lum = _composited_on_black_luminance(px) + assert lum <= max_ring_luminance, ( + f"{name}: Uebergangspixel bei ({x},{y}) hat auf Schwarz " + f"kompositiert Luminanz {lum:.1f} (> {max_ring_luminance}) " + f"-- heller Fransensaum am Rand." + ) + + # Wide310x150Logo bettet das Motiv mittig in ein 310x150-Canvas ein; + # sowohl der aeussere Rand als auch das eingebettete Quadrat muessen + # an ihren Ecken transparent sein (der Halo im Mittelfeld war 2026-09-26 + # zunaechst unentdeckt geblieben, weil nur die Aussen-Ecken geprueft wurden). + with Image.open(store_dir / "Wide310x150Logo.png").convert("RGBA") as wide: + w, h = wide.size + assert wide.getpixel((0, 0))[3] < 30, "Wide310x150Logo: aeussere Ecke nicht transparent" + square_left = (w - h) // 2 + for x_offset in (2, h - 3): + for y_offset in (2, h - 3): + a = wide.getpixel((square_left + x_offset, y_offset))[3] + assert a < 30, ( + f"Wide310x150Logo: Ecke des eingebetteten Quadrats bei " + f"({square_left + x_offset},{y_offset}) ist fast opak (Alpha {a})." + ) + for y in range(h): + for x in range(w): + px = wide.getpixel((x, y)) + if px[3] == 0 or px[3] == 255: + continue + lum = _composited_on_black_luminance(px) + assert lum <= max_ring_luminance, ( + f"Wide310x150Logo: Uebergangspixel bei ({x},{y}) hat auf Schwarz " + f"kompositiert Luminanz {lum:.1f} (> {max_ring_luminance}) " + f"-- heller Fransensaum am Rand." + ) + + def test_app_icon_loader_returns_valid_icon(): """Verify src.main.load_app_icon() returns a valid, non-null QIcon.""" from PySide6.QtWidgets import QApplication diff --git a/tests/test_metadata_contract.py b/tests/test_metadata_contract.py index c6d4f3b..8e74357 100644 --- a/tests/test_metadata_contract.py +++ b/tests/test_metadata_contract.py @@ -225,7 +225,7 @@ def test_llms_txt_structure(): assert "file-bricks/ExplorerPro" in content assert "PySide6" in content assert "Last-checked: 2026-09-26" in content - assert "1.0.6" in content + assert "1.0.7" in content assert "ci.yml" in content @@ -267,9 +267,9 @@ def test_version_parity(): claude_text = (REPO_ROOT / "CLAUDE.md").read_text(encoding="utf-8") changelog_text = (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8") - assert 'version = "1.0.6"' in pyproject_text - assert "version: 1.0.6" in claude_text - assert "## [1.0.6]" in changelog_text + assert 'version = "1.0.7"' in pyproject_text + assert "version: 1.0.7" in claude_text + assert "## [1.0.7]" in changelog_text def test_readme_18_point_navigation_parity():