1.7.5: WebP beim Hochladen in JPG/PNG umwandeln statt ablehnen [deploy]
All checks were successful
Deploy bericht / deploy (push) Successful in 13s
All checks were successful
Deploy bericht / deploy (push) Successful in 13s
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 <noreply@anthropic.com>
This commit is contained in:
parent
7b69c0f180
commit
1d3070e6e2
10 changed files with 137 additions and 14 deletions
19
ChangeLog.md
19
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
|
||||
|
|
|
|||
|
|
@ -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}`, …)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue