* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file lib/qrcode.class.php * \ingroup epcqr * \brief QR-Code Generator mit lokalem Caching und Styling */ /** * QRCode Generator Klasse * Generiert QR-Codes mit optionalen Styling-Optionen (Farben, Logo) */ class QRCodeGenerator { private $db; private $cacheDir; // Styling-Optionen private $fgColor = array(0, 0, 0); // Vordergrundfarbe (schwarz) private $bgColor = array(255, 255, 255); // Hintergrundfarbe (weiß) private $bgTransparent = false; // Transparenter Hintergrund private $logoPath = ''; // Pfad zum Logo private $logoSize = 20; // Logo-Größe in Prozent (20% der QR-Code-Größe) private $pixelSize = 8; // Pixel-Größe für Module private $moduleStyle = 'square'; // Modul-Stil: square, rounded, dots, diamond private $borderStyle = 'none'; // Rahmen-Stil: none, simple, rounded, double, dashed private $borderWidth = 3; // Rahmenbreite in Pixeln private $borderPadding = 8; // Abstand zwischen QR-Code und Rahmen /** * Constructor * * @param DoliDB $db Database handler */ public function __construct($db) { global $conf; $this->db = $db; // Cache-Verzeichnis für QR-Codes $this->cacheDir = $conf->epcqr->dir_output.'/qrcodes'; // Verzeichnis erstellen falls nicht vorhanden if (!is_dir($this->cacheDir)) { dol_mkdir($this->cacheDir); } // Styling aus Konfiguration laden $this->loadStyleFromConfig(); } /** * Lädt Styling-Einstellungen aus der Dolibarr-Konfiguration */ private function loadStyleFromConfig() { // Vordergrundfarbe (Hex -> RGB) $fgHex = getDolGlobalString('EPCQR_FG_COLOR', '#000000'); $this->fgColor = $this->hexToRgb($fgHex); // Hintergrundfarbe (Hex -> RGB) oder transparent $bgHex = getDolGlobalString('EPCQR_BG_COLOR', '#FFFFFF'); if (strtolower($bgHex) === 'transparent' || $bgHex === '') { $this->bgTransparent = true; $this->bgColor = array(255, 255, 255); // Fallback für Logo-Hintergrund } else { $this->bgTransparent = false; $this->bgColor = $this->hexToRgb($bgHex); } // Logo-Pfad $this->logoPath = getDolGlobalString('EPCQR_LOGO_PATH', ''); // Logo-Größe (Prozent) $this->logoSize = getDolGlobalInt('EPCQR_LOGO_SIZE', 20); if ($this->logoSize < 5) $this->logoSize = 5; if ($this->logoSize > 30) $this->logoSize = 30; // Max 30% wegen Fehlerkorrektur // Modul-Stil $this->moduleStyle = getDolGlobalString('EPCQR_MODULE_STYLE', 'square'); if (!in_array($this->moduleStyle, array('square', 'rounded', 'dots', 'diamond'))) { $this->moduleStyle = 'square'; } // Rahmen-Stil $this->borderStyle = getDolGlobalString('EPCQR_BORDER_STYLE', 'none'); if (!in_array($this->borderStyle, array('none', 'simple', 'rounded', 'double', 'dashed'))) { $this->borderStyle = 'none'; } } /** * Konvertiert Hex-Farbe zu RGB-Array * * @param string $hex Hex-Farbcode (z.B. #FF0000) * @return array RGB-Array [r, g, b] */ private function hexToRgb($hex) { $hex = ltrim($hex, '#'); if (strlen($hex) == 3) { $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; } return array( hexdec(substr($hex, 0, 2)), hexdec(substr($hex, 2, 2)), hexdec(substr($hex, 4, 2)) ); } /** * Generiert einen EPC-QR-Code für SEPA-Überweisungen * * @param string $accountHolder Kontoinhaber Name * @param string $iban IBAN * @param string $bic BIC (optional) * @param float $amount Betrag * @param string $reference Verwendungszweck * @return string|false Pfad zur generierten QR-Code-Datei oder false bei Fehler */ public function generateEPCQRCode($accountHolder, $iban, $bic, $amount, $reference) { // Eindeutigen Dateinamen generieren basierend auf Parametern UND Styling $styleHash = md5(implode('', $this->fgColor).implode('', $this->bgColor).($this->bgTransparent ? 'T' : 'F').$this->logoPath.$this->logoSize.$this->moduleStyle.$this->borderStyle); $hash = md5($accountHolder.$iban.$bic.$amount.$reference.$styleHash); $filename = 'epc_'.$hash.'.png'; $filepath = $this->cacheDir.'/'.$filename; // Prüfen ob QR-Code bereits cached ist if (file_exists($filepath)) { dol_syslog("QRCodeGenerator: QR-Code aus Cache geladen: ".$filepath, LOG_DEBUG); return $filepath; } // QR-Code generieren $qrData = $this->generateEPCData($accountHolder, $iban, $bic, $amount, $reference); $result = $this->generateQRImage($qrData, $filepath); if ($result) { dol_syslog("QRCodeGenerator: QR-Code generiert: ".$filepath, LOG_DEBUG); return $filepath; } dol_syslog("QRCodeGenerator: Fehler beim Generieren des QR-Codes", LOG_ERR); return false; } /** * Generiert einen generischen QR-Code aus beliebigem Text * * @param string $data Daten für QR-Code * @param string $prefix Präfix für Dateinamen (default: 'qr') * @return string|false Pfad zur generierten QR-Code-Datei oder false bei Fehler */ public function generateQRCode($data, $prefix = 'qr') { // Eindeutigen Dateinamen generieren $styleHash = md5(implode('', $this->fgColor).implode('', $this->bgColor).($this->bgTransparent ? 'T' : 'F').$this->logoPath.$this->logoSize.$this->moduleStyle.$this->borderStyle); $hash = md5($data.$styleHash); $filename = $prefix.'_'.$hash.'.png'; $filepath = $this->cacheDir.'/'.$filename; // Prüfen ob QR-Code bereits cached ist if (file_exists($filepath)) { dol_syslog("QRCodeGenerator: QR-Code aus Cache geladen: ".$filepath, LOG_DEBUG); return $filepath; } // QR-Code generieren $result = $this->generateQRImage($data, $filepath); if ($result) { dol_syslog("QRCodeGenerator: QR-Code generiert: ".$filepath, LOG_DEBUG); return $filepath; } dol_syslog("QRCodeGenerator: Fehler beim Generieren des QR-Codes", LOG_ERR); return false; } /** * Generiert EPC-Datenstring für SEPA-QR-Codes * * @param string $accountHolder Kontoinhaber Name * @param string $iban IBAN * @param string $bic BIC * @param float $amount Betrag * @param string $reference Verwendungszweck * @return string EPC-Datenstring */ private function generateEPCData($accountHolder, $iban, $bic, $amount, $reference) { // EPC QR-Code Format (GiroCode Standard) $epcData = array( 'BCD', // Service Tag '002', // Version '1', // Character set (1 = UTF-8) 'SCT', // Identification (SEPA Credit Transfer) $bic, // BIC $accountHolder, // Empfänger Name $iban, // Empfänger IBAN 'EUR'.number_format($amount, 2, '.', ''), // Betrag '', // Purpose (optional) $reference, // Verwendungszweck '' // Beneficiary to originator information (optional) ); return implode("\n", $epcData); } /** * Generiert QR-Code-Bild aus Daten mit Styling * * @param string $data Daten für QR-Code * @param string $filepath Zielpfad für PNG-Datei * @return bool true bei Erfolg, false bei Fehler */ private function generateQRImage($data, $filepath) { // TCPDF 2D Barcode Bibliothek laden require_once DOL_DOCUMENT_ROOT.'/includes/tecnickcom/tcpdf/tcpdf_barcodes_2d.php'; try { // QR-Code mit hoher Fehlerkorrektur erstellen (H = 30% - notwendig für Logo) $errorLevel = !empty($this->logoPath) && file_exists($this->logoPath) ? 'H' : 'M'; $barcodeobj = new TCPDF2DBarcode($data, 'QRCODE,'.$errorLevel); // PNG-Daten generieren $imageData = $barcodeobj->getBarcodePngData($this->pixelSize, $this->pixelSize, $this->fgColor); if ($imageData === false) { dol_syslog("QRCodeGenerator: TCPDF konnte kein PNG generieren", LOG_ERR); return false; } // Bild aus String erstellen für weitere Verarbeitung $image = imagecreatefromstring($imageData); if ($image === false) { // Fallback: Direkt speichern ohne Styling $result = file_put_contents($filepath, $imageData); return ($result !== false); } // Modul-Stil anwenden (rounded, dots, diamond) oder Standard-Verarbeitung if ($this->moduleStyle !== 'square') { $image = $this->applyModuleStyle($image); } else { // Standard: Hintergrund anwenden (transparent oder Farbe) if ($this->bgTransparent) { $image = $this->applyTransparentBackground($image); } elseif ($this->bgColor != array(255, 255, 255)) { $image = $this->applyBackgroundColor($image); } } // Logo hinzufügen (wenn konfiguriert) if (!empty($this->logoPath) && file_exists($this->logoPath)) { $image = $this->addLogo($image); } // Rahmen hinzufügen (wenn konfiguriert) if ($this->borderStyle !== 'none') { $image = $this->addBorder($image); } // Bild speichern $result = imagepng($image, $filepath); imagedestroy($image); return $result; } catch (Exception $e) { dol_syslog("QRCodeGenerator: Exception bei QR-Generierung: ".$e->getMessage(), LOG_ERR); return false; } } /** * Wendet Hintergrundfarbe auf das Bild an * * @param resource $image GD-Bild-Ressource * @return resource Bearbeitetes Bild */ private function applyBackgroundColor($image) { $width = imagesx($image); $height = imagesy($image); // Neues Bild mit Hintergrundfarbe erstellen $newImage = imagecreatetruecolor($width, $height); $bgColorAlloc = imagecolorallocate($newImage, $this->bgColor[0], $this->bgColor[1], $this->bgColor[2]); imagefill($newImage, 0, 0, $bgColorAlloc); // Vordergrundfarbe einmal allokieren $fgColorAlloc = imagecolorallocate($newImage, $this->fgColor[0], $this->fgColor[1], $this->fgColor[2]); // QR-Code-Module (dunkle Pixel) kopieren for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($image, $x, $y); // Farbwerte extrahieren (funktioniert für indexed und truecolor) if (imageistruecolor($image)) { $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; } else { $colors = imagecolorsforindex($image, $rgb); $r = $colors['red']; $g = $colors['green']; $b = $colors['blue']; } // Helligkeit berechnen (0 = schwarz, 255 = weiß) $brightness = ($r + $g + $b) / 3; // Dunkle Pixel (QR-Code-Module) = Vordergrundfarbe setzen if ($brightness < 128) { imagesetpixel($newImage, $x, $y, $fgColorAlloc); } } } imagedestroy($image); return $newImage; } /** * Wendet transparenten Hintergrund auf das Bild an * * @param resource $image GD-Bild-Ressource * @return resource Bearbeitetes Bild mit Transparenz */ private function applyTransparentBackground($image) { $width = imagesx($image); $height = imagesy($image); // Neues Bild mit Alpha-Kanal erstellen $newImage = imagecreatetruecolor($width, $height); // Alpha-Blending deaktivieren und Alpha speichern aktivieren imagealphablending($newImage, false); imagesavealpha($newImage, true); // Transparente Farbe erstellen und füllen $transparent = imagecolorallocatealpha($newImage, 0, 0, 0, 127); imagefill($newImage, 0, 0, $transparent); // Für das Zeichnen Alpha-Blending aktivieren imagealphablending($newImage, true); // Vordergrundfarbe einmal allokieren $fgColorAlloc = imagecolorallocate($newImage, $this->fgColor[0], $this->fgColor[1], $this->fgColor[2]); // QR-Code-Module (dunkle Pixel) erkennen und kopieren // Verwendet Helligkeitswert statt exakte Farbprüfung für bessere Kompatibilität for ($x = 0; $x < $width; $x++) { for ($y = 0; $y < $height; $y++) { $rgb = imagecolorat($image, $x, $y); // Farbwerte extrahieren (funktioniert für indexed und truecolor) if (imageistruecolor($image)) { $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; } else { $colors = imagecolorsforindex($image, $rgb); $r = $colors['red']; $g = $colors['green']; $b = $colors['blue']; } // Helligkeit berechnen (0 = schwarz, 255 = weiß) $brightness = ($r + $g + $b) / 3; // Dunkle Pixel (QR-Code-Module) = Helligkeit < 128 // Helle Pixel (Hintergrund) = transparent lassen if ($brightness < 128) { imagesetpixel($newImage, $x, $y, $fgColorAlloc); } } } // Alpha speichern nochmal sicherstellen imagealphablending($newImage, false); imagesavealpha($newImage, true); imagedestroy($image); return $newImage; } /** * Wendet Modul-Stil auf das QR-Code-Bild an (rounded, dots, diamond) * * @param resource $image GD-Bild-Ressource * @return resource Bearbeitetes Bild mit gestylten Modulen */ private function applyModuleStyle($image) { $width = imagesx($image); $height = imagesy($image); // Modul-Matrix extrahieren (welche Module sind dunkel) $moduleMatrix = $this->extractModuleMatrix($image); $moduleCount = count($moduleMatrix); if ($moduleCount == 0) { return $image; } // Neue Bildgröße berechnen (etwas Padding hinzufügen) $moduleSize = $this->pixelSize; $newWidth = $moduleCount * $moduleSize; $newHeight = $moduleCount * $moduleSize; // Neues Bild erstellen $newImage = imagecreatetruecolor($newWidth, $newHeight); // Alpha für transparenten Hintergrund if ($this->bgTransparent) { imagealphablending($newImage, false); imagesavealpha($newImage, true); $bgColorAlloc = imagecolorallocatealpha($newImage, 0, 0, 0, 127); } else { $bgColorAlloc = imagecolorallocate($newImage, $this->bgColor[0], $this->bgColor[1], $this->bgColor[2]); } imagefill($newImage, 0, 0, $bgColorAlloc); if ($this->bgTransparent) { imagealphablending($newImage, true); } // Vordergrundfarbe $fgColorAlloc = imagecolorallocate($newImage, $this->fgColor[0], $this->fgColor[1], $this->fgColor[2]); // Module zeichnen for ($row = 0; $row < $moduleCount; $row++) { for ($col = 0; $col < $moduleCount; $col++) { if (!empty($moduleMatrix[$row][$col])) { $x = $col * $moduleSize; $y = $row * $moduleSize; switch ($this->moduleStyle) { case 'rounded': $this->drawRoundedModule($newImage, $x, $y, $moduleSize, $fgColorAlloc); break; case 'dots': $this->drawDotModule($newImage, $x, $y, $moduleSize, $fgColorAlloc); break; case 'diamond': $this->drawDiamondModule($newImage, $x, $y, $moduleSize, $fgColorAlloc); break; default: imagefilledrectangle($newImage, $x, $y, $x + $moduleSize - 1, $y + $moduleSize - 1, $fgColorAlloc); } } } } if ($this->bgTransparent) { imagealphablending($newImage, false); imagesavealpha($newImage, true); } imagedestroy($image); return $newImage; } /** * Extrahiert die Modul-Matrix aus dem QR-Code-Bild * * @param resource $image GD-Bild-Ressource * @return array 2D-Array mit true/false für jedes Modul */ private function extractModuleMatrix($image) { $width = imagesx($image); $height = imagesy($image); // Anzahl der Module berechnen $moduleCount = (int)($width / $this->pixelSize); $matrix = array(); for ($row = 0; $row < $moduleCount; $row++) { $matrix[$row] = array(); for ($col = 0; $col < $moduleCount; $col++) { // Mitte des Moduls samplen $x = ($col * $this->pixelSize) + (int)($this->pixelSize / 2); $y = ($row * $this->pixelSize) + (int)($this->pixelSize / 2); if ($x < $width && $y < $height) { $rgb = imagecolorat($image, $x, $y); if (imageistruecolor($image)) { $r = ($rgb >> 16) & 0xFF; $g = ($rgb >> 8) & 0xFF; $b = $rgb & 0xFF; } else { $colors = imagecolorsforindex($image, $rgb); $r = $colors['red']; $g = $colors['green']; $b = $colors['blue']; } $brightness = ($r + $g + $b) / 3; $matrix[$row][$col] = ($brightness < 128); } else { $matrix[$row][$col] = false; } } } return $matrix; } /** * Zeichnet ein abgerundetes Modul */ private function drawRoundedModule($image, $x, $y, $size, $color) { $radius = (int)($size * 0.3); // 30% Radius für Rundung // Gefülltes Rechteck mit abgerundeten Ecken simulieren // Hauptrechteck (ohne Ecken) imagefilledrectangle($image, $x + $radius, $y, $x + $size - $radius - 1, $y + $size - 1, $color); imagefilledrectangle($image, $x, $y + $radius, $x + $size - 1, $y + $size - $radius - 1, $color); // Ecken als gefüllte Kreise imagefilledellipse($image, $x + $radius, $y + $radius, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x + $size - $radius - 1, $y + $radius, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x + $radius, $y + $size - $radius - 1, $radius * 2, $radius * 2, $color); imagefilledellipse($image, $x + $size - $radius - 1, $y + $size - $radius - 1, $radius * 2, $radius * 2, $color); } /** * Zeichnet ein kreisförmiges Modul (Punkt) */ private function drawDotModule($image, $x, $y, $size, $color) { $centerX = $x + (int)($size / 2); $centerY = $y + (int)($size / 2); $diameter = $size - 2; // Etwas kleiner für Abstand imagefilledellipse($image, $centerX, $centerY, $diameter, $diameter, $color); } /** * Zeichnet ein rautenförmiges Modul (Diamant) */ private function drawDiamondModule($image, $x, $y, $size, $color) { $centerX = $x + (int)($size / 2); $centerY = $y + (int)($size / 2); $half = (int)($size / 2) - 1; $points = array( $centerX, $y + 1, // Oben $x + $size - 2, $centerY, // Rechts $centerX, $y + $size - 2, // Unten $x + 1, $centerY // Links ); imagefilledpolygon($image, $points, $color); } /** * Fügt Logo in die Mitte des QR-Codes ein * * @param resource $image GD-Bild-Ressource * @return resource Bearbeitetes Bild */ private function addLogo($image) { // Logo laden $logoInfo = getimagesize($this->logoPath); if ($logoInfo === false) { dol_syslog("QRCodeGenerator: Logo konnte nicht gelesen werden: ".$this->logoPath, LOG_WARNING); return $image; } $logoType = $logoInfo[2]; $logo = null; switch ($logoType) { case IMAGETYPE_PNG: $logo = imagecreatefrompng($this->logoPath); break; case IMAGETYPE_JPEG: $logo = imagecreatefromjpeg($this->logoPath); break; case IMAGETYPE_GIF: $logo = imagecreatefromgif($this->logoPath); break; default: dol_syslog("QRCodeGenerator: Nicht unterstütztes Logo-Format", LOG_WARNING); return $image; } if ($logo === false) { return $image; } $qrWidth = imagesx($image); $qrHeight = imagesy($image); $logoOrigWidth = imagesx($logo); $logoOrigHeight = imagesy($logo); // Logo-Größe berechnen (Prozent der QR-Code-Größe) $logoNewWidth = (int)($qrWidth * $this->logoSize / 100); $logoNewHeight = (int)($logoOrigHeight * ($logoNewWidth / $logoOrigWidth)); // Logo-Position (Mitte) $logoX = (int)(($qrWidth - $logoNewWidth) / 2); $logoY = (int)(($qrHeight - $logoNewHeight) / 2); // Hintergrund für Logo-Bereich (mit etwas Padding) // Nur wenn NICHT transparent - bei transparentem Hintergrund kein Rechteck zeichnen $padding = 4; if (!$this->bgTransparent) { $bgColorAlloc = imagecolorallocate($image, $this->bgColor[0], $this->bgColor[1], $this->bgColor[2]); imagefilledrectangle( $image, $logoX - $padding, $logoY - $padding, $logoX + $logoNewWidth + $padding, $logoY + $logoNewHeight + $padding, $bgColorAlloc ); } else { // Bei transparentem Hintergrund: Bereich mit Transparenz füllen imagealphablending($image, false); $transparent = imagecolorallocatealpha($image, 0, 0, 0, 127); imagefilledrectangle( $image, $logoX - $padding, $logoY - $padding, $logoX + $logoNewWidth + $padding, $logoY + $logoNewHeight + $padding, $transparent ); imagealphablending($image, true); } // Logo skaliert einfügen imagecopyresampled( $image, $logo, $logoX, $logoY, 0, 0, $logoNewWidth, $logoNewHeight, $logoOrigWidth, $logoOrigHeight ); imagedestroy($logo); return $image; } /** * Fügt einen Rahmen um den QR-Code hinzu * * @param resource $image GD-Bild-Ressource * @return resource Bearbeitetes Bild mit Rahmen */ private function addBorder($image) { $origWidth = imagesx($image); $origHeight = imagesy($image); // Neue Bildgröße mit Padding und Rahmen $totalPadding = $this->borderPadding + $this->borderWidth; $newWidth = $origWidth + ($totalPadding * 2); $newHeight = $origHeight + ($totalPadding * 2); // Neues Bild erstellen $newImage = imagecreatetruecolor($newWidth, $newHeight); // Alpha für transparenten Hintergrund if ($this->bgTransparent) { imagealphablending($newImage, false); imagesavealpha($newImage, true); $bgColorAlloc = imagecolorallocatealpha($newImage, 0, 0, 0, 127); } else { $bgColorAlloc = imagecolorallocate($newImage, $this->bgColor[0], $this->bgColor[1], $this->bgColor[2]); } imagefill($newImage, 0, 0, $bgColorAlloc); if ($this->bgTransparent) { imagealphablending($newImage, true); } // Rahmenfarbe (Vordergrundfarbe) $borderColor = imagecolorallocate($newImage, $this->fgColor[0], $this->fgColor[1], $this->fgColor[2]); // Rahmen zeichnen je nach Stil switch ($this->borderStyle) { case 'simple': $this->drawSimpleBorder($newImage, $newWidth, $newHeight, $borderColor); break; case 'rounded': $this->drawRoundedBorder($newImage, $newWidth, $newHeight, $borderColor); break; case 'double': $this->drawDoubleBorder($newImage, $newWidth, $newHeight, $borderColor); break; case 'dashed': $this->drawDashedBorder($newImage, $newWidth, $newHeight, $borderColor); break; } // Original-Bild in die Mitte kopieren imagecopy($newImage, $image, $totalPadding, $totalPadding, 0, 0, $origWidth, $origHeight); if ($this->bgTransparent) { imagealphablending($newImage, false); imagesavealpha($newImage, true); } imagedestroy($image); return $newImage; } /** * Zeichnet einen einfachen Rahmen */ private function drawSimpleBorder($image, $width, $height, $color) { imagesetthickness($image, $this->borderWidth); $offset = (int)($this->borderWidth / 2); imagerectangle($image, $offset, $offset, $width - $offset - 1, $height - $offset - 1, $color); } /** * Zeichnet einen abgerundeten Rahmen */ private function drawRoundedBorder($image, $width, $height, $color) { $radius = 15; $thickness = $this->borderWidth; // Dicke Linien für die geraden Seiten imagesetthickness($image, $thickness); $offset = (int)($thickness / 2); // Obere Linie (ohne Ecken) imageline($image, $radius, $offset, $width - $radius, $offset, $color); // Untere Linie imageline($image, $radius, $height - $offset - 1, $width - $radius, $height - $offset - 1, $color); // Linke Linie imageline($image, $offset, $radius, $offset, $height - $radius, $color); // Rechte Linie imageline($image, $width - $offset - 1, $radius, $width - $offset - 1, $height - $radius, $color); // Abgerundete Ecken als Bögen imagesetthickness($image, $thickness); // Oben-links imagearc($image, $radius, $radius, $radius * 2, $radius * 2, 180, 270, $color); // Oben-rechts imagearc($image, $width - $radius - 1, $radius, $radius * 2, $radius * 2, 270, 360, $color); // Unten-links imagearc($image, $radius, $height - $radius - 1, $radius * 2, $radius * 2, 90, 180, $color); // Unten-rechts imagearc($image, $width - $radius - 1, $height - $radius - 1, $radius * 2, $radius * 2, 0, 90, $color); } /** * Zeichnet einen doppelten Rahmen */ private function drawDoubleBorder($image, $width, $height, $color) { $gap = 4; // Abstand zwischen den Linien imagesetthickness($image, 2); // Äußerer Rahmen imagerectangle($image, 1, 1, $width - 2, $height - 2, $color); // Innerer Rahmen imagerectangle($image, 1 + $gap, 1 + $gap, $width - 2 - $gap, $height - 2 - $gap, $color); } /** * Zeichnet einen gestrichelten Rahmen */ private function drawDashedBorder($image, $width, $height, $color) { $dashLength = 8; $gapLength = 4; // Transparente Farbe für Lücken if ($this->bgTransparent) { $gapColor = imagecolorallocatealpha($image, 0, 0, 0, 127); } else { $gapColor = imagecolorallocate($image, $this->bgColor[0], $this->bgColor[1], $this->bgColor[2]); } // Strichmuster erstellen $style = array(); for ($i = 0; $i < $dashLength; $i++) { $style[] = $color; } for ($i = 0; $i < $gapLength; $i++) { $style[] = $gapColor; } imagesetstyle($image, $style); imagesetthickness($image, $this->borderWidth); $offset = (int)($this->borderWidth / 2); // Rahmen mit Stil zeichnen imageline($image, $offset, $offset, $width - $offset, $offset, IMG_COLOR_STYLED); imageline($image, $width - $offset - 1, $offset, $width - $offset - 1, $height - $offset, IMG_COLOR_STYLED); imageline($image, $width - $offset, $height - $offset - 1, $offset, $height - $offset - 1, IMG_COLOR_STYLED); imageline($image, $offset, $height - $offset, $offset, $offset, IMG_COLOR_STYLED); } /** * Löscht gecachte QR-Codes die älter als X Tage sind * * @param int $days Anzahl Tage (default: 30) * @return int Anzahl gelöschter Dateien */ public function cleanCache($days = 30) { $deleted = 0; $threshold = time() - ($days * 86400); if (!is_dir($this->cacheDir)) { return 0; } $files = scandir($this->cacheDir); foreach ($files as $file) { if ($file === '.' || $file === '..') { continue; } $filepath = $this->cacheDir.'/'.$file; if (is_file($filepath) && filemtime($filepath) < $threshold) { if (unlink($filepath)) { $deleted++; } } } dol_syslog("QRCodeGenerator: ".$deleted." alte QR-Codes gelöscht", LOG_INFO); return $deleted; } /** * Löscht alle gecachten QR-Codes (für Style-Änderungen) * * @return int Anzahl gelöschter Dateien */ public function clearCache() { return $this->cleanCache(0); } }