From 1d3070e6e2b7ca5276a377ec968ab1d30a36bb7e Mon Sep 17 00:00:00 2001 From: Eduard Wisch Date: Mon, 19 Oct 2026 22:03:06 +0200 Subject: [PATCH] 1.7.5: WebP beim Hochladen in JPG/PNG umwandeln statt ablehnen [deploy] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Editor bot WebP an, der Server lehnte es mit „Dateityp nicht erlaubt" ab; ein WebP unter „.jpg" kam roh durch. bericht_upload_bild_umwandeln() wandelt jetzt AVIF und WebP vor der Endungsprüfung um (deckend → JPG, transparent → PNG über GD). Bewegte WebP werden mit Meldung abgelehnt. Grund für die Umwandlung: Bericht-PDF/LibreOffice setzen WebP nicht sicher (KB #447). Alle drei Upload-Wege (Editor, PWA-Foto, Handy-Upload), in der Testinstanz über den Editor geprüft. Co-Authored-By: Claude Opus 5.5 --- ChangeLog.md | 19 ++++++ README.md | 2 +- ajax/upload_extra.php | 6 +- api/orders.php | 6 +- core/modules/modBericht.class.php | 2 +- js/editor.js | 2 +- langs/de_DE/bericht.lang | 2 + langs/en_US/bericht.lang | 2 + lib/bericht_avif.lib.php | 102 +++++++++++++++++++++++++++++- mobile_upload.php | 8 +-- 10 files changed, 137 insertions(+), 14 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0a3d0a4..619eb2a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,24 @@ # Changelog +## 1.7.5 — 2026-10-19 + +### WebP-Bilder werden beim Hochladen umgewandelt statt abgelehnt +Der Editor bot WebP im Dateidialog an, der Server lehnte es dann mit „Dateityp nicht erlaubt" ab. +Ein WebP, das ein Bild-CDN unter „.jpg" ausliefert, kam dagegen roh durch. Jetzt wandelt +`bericht_upload_bild_umwandeln()` (lib/bericht_avif.lib.php) AVIF **und** WebP vor der +Endungsprüfung um: deckend → JPG, mit Transparenz → PNG (GD, Alpha bleibt erhalten). +Umgewandelt wird, statt WebP durchzulassen, weil das Bericht-PDF WebP nicht sicher setzt und +LibreOffice WebP beim ODT→PDF-Export stillschweigend weglässt (KB #447). + +- Erkennung am Inhalt (RIFF/„WEBP"), Transparenz und Animation aus dem ersten Chunk + (VP8X-Merker bzw. VP8L-Bildkopf), ohne zu dekodieren. +- Bewegte WebP liest GD nicht: Meldung „Bewegte WebP-Bilder kann der Bericht nicht aufnehmen" + statt eines unklaren Fehlers. +- Gilt für alle drei Upload-Wege: Editor (`ajax/upload_extra.php`), PWA-Foto + (`api/orders.php?action=upload_photo`), Handy-Upload per QR (`mobile_upload.php`). +- Getestet in der Testinstanz über den Editor: WebP deckend → `.jpg`, WebP transparent → `.png` + (Ecke transparent), WebP unter „.jpg" → echtes JPEG, bewegtes WebP → Meldung. + ## 1.7.4 — 2026-10-19 ### AVIF-Bilder werden beim Hochladen umgewandelt diff --git a/README.md b/README.md index 6ae9c32..599a0cd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Bilder und PDFs lassen sich im Browser annotieren (Pfeile, Kreise, Rechtecke, Te - **Bild einer Seite austauschen** ohne sie zu löschen — Reihenfolge, Notiz und Anmerkungen bleiben erhalten. Bei Raster-Layouts lässt sich jeder Platz einzeln tauschen, direkt auf der Seite anklickbar. - **Ziehen und Ablegen**: Kachel auf die Seitenliste legt neue Seiten an, Kachel auf die Arbeitsfläche ersetzt das Bild der offenen Seite, Kachel auf einen Raster-Platz setzt genau diesen. Dateien vom Rechner auf die Anhänge-Spalte werden hochgeladen. - **Mobiler Upload per QR-Code** — Handy scannt, lädt Fotos direkt in den offenen Bericht (zeitlich begrenzte Token) -- **AVIF-Bilder (1.7.4)** — beim Hochladen (Editor, PWA, Handy-Upload) am Inhalt erkannt und in JPG bzw. PNG (mit Transparenz) umgewandelt (`lib/bericht_avif.lib.php`, KB #1698) +- **AVIF- und WebP-Bilder (1.7.4/1.7.5)** — beim Hochladen (Editor, PWA, Handy-Upload) am Inhalt erkannt und in JPG bzw. PNG (mit Transparenz) umgewandelt (`lib/bericht_avif.lib.php`, `bericht_upload_bild_umwandeln()`, KB #1698, #447). Bewegte WebP werden mit Meldung abgelehnt. - **Ablage am Beleg als Beilage (1.7.2)** — Fotos, Notizen, Sprachnotizen, Scans und Bericht-PDFs, die im Belegordner landen, stehen im ECM-Index als `uploaded` (`lib/bericht_index.lib.php`): nur im Reiter „Verknüpfte Dokumente", nie vorne im Kasten der Belegkarte (KB #1641) - **Notizen pro Seite** — werden im finalen PDF unten auf der Seite gedruckt - **Deckblatt aus ODT-Vorlage** mit Platzhaltern (`{auftragsnummer}`, `{kunde_name}`, `{datum}`, …) diff --git a/ajax/upload_extra.php b/ajax/upload_extra.php index 7ad73ee..bac4734 100644 --- a/ajax/upload_extra.php +++ b/ajax/upload_extra.php @@ -15,10 +15,10 @@ if ($bericht->fetch($berichtid) <= 0) bericht_ajax_fail('Bericht nicht gefunden' if (empty($_FILES['file']['tmp_name'])) bericht_ajax_fail('Keine Datei hochgeladen'); -// AVIF (aus dem Netz, auch unter „.jpg") vor Endungsprüfung und Namensbildung in JPG/PNG umwandeln +// AVIF/WebP (aus dem Netz, auch unter „.jpg") vor Endungsprüfung und Namensbildung in JPG/PNG umwandeln require_once __DIR__.'/../lib/bericht_avif.lib.php'; -$avif_fehler = bericht_upload_avif_umwandeln($_FILES['file']); -if ($avif_fehler !== '') bericht_ajax_fail($avif_fehler); +$bild_fehler = bericht_upload_bild_umwandeln($_FILES['file']); +if ($bild_fehler !== '') bericht_ajax_fail($bild_fehler); $origname = dol_sanitizeFileName($_FILES['file']['name']); $ext = strtolower(pathinfo($origname, PATHINFO_EXTENSION)); diff --git a/api/orders.php b/api/orders.php index 4c52a83..dce073a 100644 --- a/api/orders.php +++ b/api/orders.php @@ -227,10 +227,10 @@ if ($action === 'upload_photo' && $_SERVER['REQUEST_METHOD'] === 'POST') { if (!$user->hasRight('bericht', 'write')) api_fail('Schreibrechte fehlen', 403); if (empty($_FILES['file']['tmp_name'])) api_fail('file fehlt'); - // AVIF (Galerie/Teilen aus dem Netz, auch unter „.jpg") vor der Prüfung in JPG/PNG umwandeln + // AVIF/WebP (Galerie/Teilen aus dem Netz, auch unter „.jpg") vor der Prüfung in JPG/PNG umwandeln require_once __DIR__.'/../lib/bericht_avif.lib.php'; - $avif_fehler = bericht_upload_avif_umwandeln($_FILES['file']); - if ($avif_fehler !== '') api_fail($avif_fehler); + $bild_fehler = bericht_upload_bild_umwandeln($_FILES['file']); + if ($bild_fehler !== '') api_fail($bild_fehler); // Datei validieren $orig = dol_sanitizeFileName($_FILES['file']['name']); diff --git a/core/modules/modBericht.class.php b/core/modules/modBericht.class.php index bd134f9..19a2598 100644 --- a/core/modules/modBericht.class.php +++ b/core/modules/modBericht.class.php @@ -27,7 +27,7 @@ class modBericht extends DolibarrModules $this->editor_name = 'Alles Watt läuft'; $this->editor_url = ''; - $this->version = '1.7.4'; + $this->version = '1.7.5'; $this->const_name = 'MAIN_MODULE_'.strtoupper($this->name); $this->picto = 'fa-file-pdf'; diff --git a/js/editor.js b/js/editor.js index 357846b..d118805 100644 --- a/js/editor.js +++ b/js/editor.js @@ -2390,7 +2390,7 @@ * per Drag & Drop. Fortschritt steht auf der Schaltfläche. */ async function uploadFiles(files) { - // AVIF wandelt der Server beim Hochladen in JPG/PNG um (lib/bericht_avif.lib.php) + // AVIF und WebP wandelt der Server beim Hochladen in JPG/PNG um (lib/bericht_avif.lib.php) const allowed = /\.(pdf|png|jpe?g|webp|avif)$/i; const rejected = files.filter(f => !allowed.test(f.name)); files = files.filter(f => allowed.test(f.name)); diff --git a/langs/de_DE/bericht.lang b/langs/de_DE/bericht.lang index b16e163..377e262 100644 --- a/langs/de_DE/bericht.lang +++ b/langs/de_DE/bericht.lang @@ -99,6 +99,8 @@ BerichtSignDelivery = Lieferung unterschreiben BerichtViewSignedPdf = Unterschriebenes PDF ansehen BerichtAvifNichtLesbar = %s: Das AVIF-Bild ließ sich nicht in JPG umwandeln. BerichtAvifTransparenz = %s: AVIF mit transparentem Hintergrund kann der Server noch nicht umwandeln — der Hintergrund würde schwarz. Bitte vorerst als PNG speichern. +BerichtWebpNichtLesbar = %s: Das WebP-Bild ließ sich nicht in JPG/PNG umwandeln. +BerichtWebpAnimiert = %s: Bewegte WebP-Bilder kann der Bericht nicht aufnehmen. Bitte ein Standbild als JPG oder PNG speichern. Permission50002101 = Berichte lesen Permission50002102 = Berichte erstellen und bearbeiten Permission50002103 = Berichte löschen diff --git a/langs/en_US/bericht.lang b/langs/en_US/bericht.lang index 5b4c96d..521b757 100644 --- a/langs/en_US/bericht.lang +++ b/langs/en_US/bericht.lang @@ -99,6 +99,8 @@ BerichtSignDelivery = Sign delivery BerichtViewSignedPdf = View signed PDF BerichtAvifNichtLesbar = %s: the AVIF image could not be converted to JPG. BerichtAvifTransparenz = %s: the server cannot convert AVIF with a transparent background yet — the background would turn black. Please save it as PNG for now. +BerichtWebpNichtLesbar = %s: the WebP image could not be converted to JPG/PNG. +BerichtWebpAnimiert = %s: animated WebP images cannot be added to a report. Please save a still image as JPG or PNG. Permission50002101 = Read reports Permission50002102 = Create and edit reports Permission50002103 = Delete reports diff --git a/lib/bericht_avif.lib.php b/lib/bericht_avif.lib.php index 6115ea4..298821a 100644 --- a/lib/bericht_avif.lib.php +++ b/lib/bericht_avif.lib.php @@ -10,7 +10,8 @@ /** * \file bericht/lib/bericht_avif.lib.php * \ingroup bericht - * \brief Hochgeladene AVIF-Bilder in JPEG (deckend) bzw. PNG (transparent) umwandeln. + * \brief Hochgeladene AVIF- und WebP-Bilder in JPEG (deckend) bzw. PNG (transparent) umwandeln. + * Einstieg für Upload-Wege: bericht_upload_bild_umwandeln(). * * Anlass (Eddy, 19.10.2026): „es gibt immer mehr Bilder im Netz, die AVIF verwenden". Die * Upload-Wege von Bericht nehmen nur JPG/PNG (Editor zusätzlich PDF) — ein AVIF wurde @@ -170,3 +171,102 @@ function bericht_upload_avif_umwandeln(array &$datei) $datei['type'] = $alpha ? 'image/png' : 'image/jpeg'; return ''; } + +/** + * Ist die Datei ein WebP? Am INHALT erkannt (RIFF-Container mit Kennung „WEBP"), nicht an der Endung — + * Bild-CDNs liefern WebP auch unter „.jpg" aus. + * + * @param string $pfad Absoluter Pfad + * @return bool + */ +function bericht_bild_ist_webp($pfad) +{ + $kopf = @file_get_contents($pfad, false, null, 0, 12); + return is_string($kopf) && strlen($kopf) === 12 && substr($kopf, 0, 4) === 'RIFF' && substr($kopf, 8, 4) === 'WEBP'; +} + +/** + * Alphakanal und Animation eines WebP aus dem ersten Chunk, ohne zu dekodieren: + * „VP8 " (verlustbehaftet, einfach) hat nie Alpha; „VP8L" trägt das Bit alpha_is_used im Bildkopf + * (nach Signatur 0x2f: 14 Bit Breite-1, 14 Bit Höhe-1, dann das Bit); „VP8X" (erweitert) hat Merker im + * ersten Byte: 0x10 = Alpha, 0x02 = Animation. + * + * @param string $pfad Absoluter Pfad + * @return array array('alpha' => bool, 'animiert' => bool) + */ +function bericht_webp_merkmale($pfad) +{ + $m = array('alpha' => false, 'animiert' => false); + $kopf = @file_get_contents($pfad, false, null, 0, 25); + if (!is_string($kopf) || strlen($kopf) < 21) { + return $m; + } + $chunk = substr($kopf, 12, 4); + if ($chunk === 'VP8X') { + $merker = ord($kopf[20]); + $m['alpha'] = ($merker & 0x10) !== 0; + $m['animiert'] = ($merker & 0x02) !== 0; + } elseif ($chunk === 'VP8L' && strlen($kopf) >= 25 && ord($kopf[20]) === 0x2f) { + $m['alpha'] = ((unpack('V', substr($kopf, 21, 4))[1] >> 28) & 1) === 1; + } + return $m; +} + +/** + * Hochgeladenes WebP an Ort und Stelle in JPEG (deckend) bzw. PNG (transparent) umwandeln — gleiches + * Vorgehen wie bericht_upload_avif_umwandeln(). Grund: TCPDF setzt WebP im Bericht-PDF nicht sicher, und + * LibreOffice lässt WebP beim ODT→PDF-Export stillschweigend weg (KB #447). GD liest WebP mit Alpha + * (lokal und auf Prod vorhanden). Bewegte WebP liest GD nicht — die werden mit Meldung abgelehnt. + * + * @param array $datei array('name', 'tmp_name', 'size'[, 'type']) — z. B. $_FILES['x'] per Referenz + * @return string '' = kein WebP oder umgewandelt; sonst Fehlermeldung (transnoentities, auch für JSON) + */ +function bericht_upload_webp_umwandeln(array &$datei) +{ + global $langs; + + $tmp = (string) ($datei['tmp_name'] ?? ''); + if ($tmp === '' || !is_file($tmp) || !bericht_bild_ist_webp($tmp)) { + return ''; + } + $name = basename((string) ($datei['name'] ?? '')); + $m = bericht_webp_merkmale($tmp); + $neu = $tmp.'.webpneu'.($m['alpha'] ? '.png' : '.jpg'); + $ok = false; + if (!$m['animiert'] && function_exists('imagecreatefromwebp')) { + $bild = @imagecreatefromwebp($tmp); + if ($bild) { + if ($m['alpha']) { + imagealphablending($bild, false); + imagesavealpha($bild, true); + $ok = imagepng($bild, $neu, 6); + } else { + $ok = imagejpeg($bild, $neu, 90); + } + imagedestroy($bild); + } + } + if (!$ok || !is_file($neu) || filesize($neu) === 0 || !@rename($neu, $tmp)) { + @unlink($neu); + $langs->load('bericht@bericht'); + return $langs->transnoentities($m['animiert'] ? 'BerichtWebpAnimiert' : 'BerichtWebpNichtLesbar', $name); + } + clearstatcache(true, $tmp); + $stamm = pathinfo($name, PATHINFO_FILENAME); + $datei['name'] = ($stamm !== '' ? $stamm : 'bild').($m['alpha'] ? '.png' : '.jpg'); + $datei['size'] = (int) filesize($tmp); + $datei['type'] = $m['alpha'] ? 'image/png' : 'image/jpeg'; + return ''; +} + +/** + * Einstieg für alle Upload-Wege: AVIF und WebP vor Endungsprüfung und Namensbildung in JPG/PNG umwandeln. + * + * @param array $datei $_FILES-Eintrag per Referenz + * @return string '' = in Ordnung; sonst Fehlermeldung + */ +function bericht_upload_bild_umwandeln(array &$datei) +{ + $fehler = bericht_upload_avif_umwandeln($datei); + return $fehler !== '' ? $fehler : bericht_upload_webp_umwandeln($datei); +} diff --git a/mobile_upload.php b/mobile_upload.php index 89018bd..02df6ab 100644 --- a/mobile_upload.php +++ b/mobile_upload.php @@ -53,12 +53,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_FILES['file']['tmp_name']) exit; } - // AVIF (die Seite verkleinert nur Bilder über 2000 px zu JPEG, kleinere kommen roh) vor der + // AVIF/WebP (die Seite verkleinert nur Bilder über 2000 px zu JPEG, kleinere kommen roh) vor der // Endungsprüfung in JPG/PNG umwandeln require_once __DIR__.'/lib/bericht_avif.lib.php'; - $avif_fehler = bericht_upload_avif_umwandeln($_FILES['file']); - if ($avif_fehler !== '') { - echo json_encode(array('success' => false, 'error' => $avif_fehler)); + $bild_fehler = bericht_upload_bild_umwandeln($_FILES['file']); + if ($bild_fehler !== '') { + echo json_encode(array('success' => false, 'error' => $bild_fehler)); exit; }