diff --git a/.gitignore b/.gitignore index 95e7188..f3224d7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ missing.xml partitions/.svelte-kit/ partitions/build/ partitions/node_modules/ +partitions/static/pdf.worker.min.js +api/vendor/ +api/composer.phar diff --git a/AGENTS.md b/AGENTS.md index 3a238be..e00478f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -185,12 +185,20 @@ MySQL database connection configured in `api/config/database.php`: ### Starting the PHP Server -For file uploads to work, start the server with the custom upload config: +**Important:** Always use the custom upload config for file uploads to work: ```bash cd api -php -c php-upload.ini -S localhost:8000 router.php +php -c php-upload.ini -S localhost:8000 router.php & ``` +Le serveur doit être lancé avec `-c php-upload.ini` pour autoriser l'upload de fichiers PDF (64M max). + +### Upload de partitions + +Lors de l'upload d'un PDF, deux options sont disponibles : +- **Ajouter le numéro** : ajoute le numéro de partition dans le coin du PDF +- **Position** : choix entre "Gauche" ou "Droite" pour placer le numéro + ## Current Tech Stack (2024) - **Frontend**: SvelteKit (NOT Vue.js 2) in `/partitions/` diff --git a/README.md b/README.md index 1bf3a66..356a646 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,43 @@ +# OHMJ - Harmonie de Montpellier-Jacou +Site web de l'Harmonie de Montpellier-Jacou pour la gestion et le partage de partitions de musique. -https://codeofaninja.com/2017/02/create-simple-rest-api-in-php.html +## Fonctionnalités -https://vuejsdevelopers.com/2020/07/29/bootstrap-vue/ +- **Gestion des partitions** : Ajout, modification et suppression de partitions +- **Upload PDF** : Upload de fichiers PDF avec options de watermark (numéro de partition) +- **Consultation** : Visualisation des partitions par instrument, pièce et version +- **Interface admin** : Tableau de bord pour la gestion du catalogue +## Stack technique -to avoid: -Error: ENOSPC: System limit for number of file watchers reached, watch ' +- **Frontend** : SvelteKit +- **Backend** : PHP API REST +- **Stockage** : Fichiers (PDF) dans le répertoire `legacy/Scores/` -echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p +## Installation + +### Backend + +```bash +cd api +php -c php-upload.ini -S localhost:8000 router.php +``` + +### Frontend + +```bash +cd partitions +npm install +npm run dev +``` + +## Déploiement + +```bash +./deploy/deploy.sh +``` + +## Licence + +GPL v2 diff --git a/api/.env b/api/.env index df90e3c..f2cd75a 100644 --- a/api/.env +++ b/api/.env @@ -1,2 +1,2 @@ JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus= -SCORES_PATH=/data/scores/ +SCORES_PATH=/home/jbnadal/sources/jb/ohmj/ohmj2/legacy/Scores/ diff --git a/api/composer.json b/api/composer.json new file mode 100644 index 0000000..1e211c9 --- /dev/null +++ b/api/composer.json @@ -0,0 +1,7 @@ +{ + "require": { + "setasign/fpdi": "^2.6", + "tecnickcom/tcpdf": "^6.10", + "setasign/fpdf": "^1.8" + } +} diff --git a/api/composer.lock b/api/composer.lock new file mode 100644 index 0000000..4ea9d48 --- /dev/null +++ b/api/composer.lock @@ -0,0 +1,208 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "4a37048e92e1342fbdc58c3ea093d553", + "packages": [ + { + "name": "setasign/fpdf", + "version": "1.8.6", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDF.git", + "reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDF/zipball/0838e0ee4925716fcbbc50ad9e1799b5edfae0a0", + "reference": "0838e0ee4925716fcbbc50ad9e1799b5edfae0a0", + "shasum": "" + }, + "require": { + "ext-gd": "*", + "ext-zlib": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "fpdf.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Olivier Plathey", + "email": "oliver@fpdf.org", + "homepage": "http://fpdf.org/" + } + ], + "description": "FPDF is a PHP class which allows to generate PDF files with pure PHP. F from FPDF stands for Free: you may use it for any kind of usage and modify it to suit your needs.", + "homepage": "http://www.fpdf.org", + "keywords": [ + "fpdf", + "pdf" + ], + "support": { + "source": "https://github.com/Setasign/FPDF/tree/1.8.6" + }, + "time": "2023-06-26T14:44:25+00:00" + }, + { + "name": "setasign/fpdi", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/4b53852fde2734ec6a07e458a085db627c60eada", + "reference": "4b53852fde2734ec6a07e458a085db627c60eada", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "^7", + "setasign/fpdf": "~1.8.6", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2025-08-05T09:57:14+00:00" + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.10.1", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "7a2701251e5d52fc3d508fd71704683eb54f5939" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/7a2701251e5d52fc3d508fd71704683eb54f5939", + "reference": "7a2701251e5d52fc3d508fd71704683eb54f5939", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=7.1.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", + "homepage": "http://www.tcpdf.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "support": { + "issues": "https://github.com/tecnickcom/TCPDF/issues", + "source": "https://github.com/tecnickcom/TCPDF/tree/6.10.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/donate/?hosted_button_id=NZUEC5XS8MFBJ", + "type": "custom" + } + ], + "time": "2025-11-21T10:58:21+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/api/index.php b/api/index.php index 3394d3b..4b78d66 100644 --- a/api/index.php +++ b/api/index.php @@ -362,13 +362,14 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE } // POST /admin/scores/:id/upload - Upload PDF -if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') { + if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') { $scoreId = $matches[1]; // Check for upload errors - if (!isset($_FILES['file'])) { + if (!isset($_FILES['file']) || $_FILES['file']['error'] === UPLOAD_ERR_NO_FILE) { // Check if post_max_size was exceeded - if (empty($_POST) && $_SERVER['CONTENT_LENGTH'] > 0) { + $contentLength = $_SERVER['CONTENT_LENGTH'] ?? 0; + if ($contentLength > 0 && empty($_FILES)) { $maxSize = ini_get('post_max_size'); http_response_code(413); echo json_encode(['error' => "File too large. Maximum size is $maxSize"]); @@ -413,8 +414,10 @@ if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === $clef = $_POST['clef'] ?? ''; $variant = $_POST['variant'] ?? ''; $part = $_POST['part'] ?? '1'; + $watermark = isset($_POST['watermark']) && $_POST['watermark'] === 'true'; + $watermarkPosition = $_POST['watermarkPosition'] ?? 'left'; - $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part); + $result = $scanner->uploadPdf($scoreId, $file, $piece, $instrument, $version, $key, $clef, $variant, $part, $watermark, $watermarkPosition); if ($result['success']) { echo json_encode(['success' => true, 'path' => $result['path']]); diff --git a/api/lib/ScoreScanner.php b/api/lib/ScoreScanner.php index 843d701..88a955e 100644 --- a/api/lib/ScoreScanner.php +++ b/api/lib/ScoreScanner.php @@ -495,7 +495,7 @@ class ScoreScanner { rmdir($dir); } - public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1'): array { + public function uploadPdf(string $scoreId, array $file, string $piece, string $instrument, string $version, string $key = '', string $clef = '', string $variant = '', string $part = '1', bool $watermark = false, string $watermarkPosition = 'left'): array { $scoreDir = $this->scoresPath . $scoreId; if (!is_dir($scoreDir)) { @@ -588,6 +588,10 @@ class ScoreScanner { $result = copy($file['tmp_name'], $targetPath); } + if ($result && $watermark) { + $this->addWatermark($targetPath, $scoreId, $watermarkPosition); + } + if ($result) { return ['success' => true, 'path' => "$scoreId/$piece/$instrument/$version/$filename"]; } @@ -674,4 +678,35 @@ class ScoreScanner { } } } + + private function addWatermark(string $pdfPath, string $scoreId, string $position = 'left'): bool { + try { + require_once __DIR__ . '/../vendor/autoload.php'; + + $pdf = new \setasign\Fpdi\Fpdi(); + $pdf->setSourceFile($pdfPath); + + $templateId = $pdf->importPage(1); + $size = $pdf->getTemplateSize($templateId); + + $pdf->AddPage($size['width'] > $size['height'] ? 'L' : 'P', [$size['width'], $size['height']]); + $pdf->useTemplate($templateId); + + $pdf->SetFont('helvetica', 'B', 20); + $pdf->SetTextColor(0, 0, 0); + + if ($position === 'right') { + $pdf->SetXY($size['width'] - 45, 5); + $pdf->Cell(40, 10, $scoreId, 0, 0, 'R'); + } else { + $pdf->SetXY(10, 5); + $pdf->Cell(40, 10, $scoreId, 0, 0, 'L'); + } + + return $pdf->Output('F', $pdfPath); + } catch (\Exception $e) { + error_log('Watermark error: ' . $e->getMessage()); + return false; + } + } } diff --git a/api/tests.php b/api/tests.php index ede738a..179f00e 100644 --- a/api/tests.php +++ b/api/tests.php @@ -286,6 +286,169 @@ class APITest { } } + public function testWatermarkUpload() { + $this->section("Upload - Watermark"); + + // Reset rate limiting + array_map('unlink', glob(sys_get_temp_dir() . '/rate_*')); + + // Get a score to upload to + $scores = $this->request('GET', 'scores'); + + if (empty($scores['body']['scores'])) { + echo " ⚠ No scores available for watermark test\n"; + return; + } + + $scoreId = $scores['body']['scores'][0]['id']; + + // Create a minimal test PDF + $pdfContent = "%PDF-1.4 +1 0 obj +<< /Type /Catalog /Pages 2 0 R >> +endobj +2 0 obj +<< /Type /Pages /Kids [3 0 R] /Count 1 >> +endobj +3 0 obj +<< /Type /Page /Parent 2 0 R /Resources 4 0 R /MediaBox [0 0 612 792] /Contents 5 0 R >> +endobj +4 0 obj +<< /Font << /F1 6 0 R >> >> +endobj +5 0 obj +<< /Length 44 >> +stream +BT +/F1 12 Tf +100 700 Td +(Test) Tj +ET +endstream +endobj +6 0 obj +<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> +endobj +xref +0 7 +0000000000 65535 f +0000000009 00000 n +0000000058 00000 n +0000000115 00000 n +0000000214 00000 n +0000000312 00000 n +0000000401 00000 n +trailer +<< /Size 7 /Root 1 0 R >> +startxref +490 +%%EOF"; + + $tmpFile = sys_get_temp_dir() . '/test_watermark_' . time() . '.pdf'; + file_put_contents($tmpFile, $pdfContent); + + // Test upload with watermark left + $result = $this->multipartRequest("admin/scores/$scoreId/upload", [ + ['name' => 'file', 'file' => $tmpFile], + ['name' => 'piece', 'data' => '1'], + ['name' => 'instrument', 'data' => 'dir'], + ['name' => 'version', 'data' => '1'], + ['name' => 'watermark', 'data' => 'true'], + ['name' => 'watermarkPosition', 'data' => 'left'] + ]); + + $this->test('Upload with watermark left returns 200', $result['code'] === 200); + + // Get files to verify it was uploaded + $files = $this->request('GET', "admin/scores/$scoreId/files"); + $hasFile = false; + if (!empty($files['body']['files'])) { + foreach ($files['body']['files'] as $f) { + if (strpos($f['path'], 'direction_1.pdf') !== false) { + $hasFile = true; + // Clean up + @unlink(SCORES_PATH . "$scoreId/" . $f['path']); + break; + } + } + } + + // Test upload with watermark right + $result2 = $this->multipartRequest("admin/scores/$scoreId/upload", [ + ['name' => 'file', 'file' => $tmpFile], + ['name' => 'piece', 'data' => '1'], + ['name' => 'instrument', 'data' => 'dir'], + ['name' => 'version', 'data' => '2'], + ['name' => 'watermark', 'data' => 'true'], + ['name' => 'watermarkPosition', 'data' => 'right'] + ]); + + $this->test('Upload with watermark right returns 200', $result2['code'] === 200); + + // Clean up + @unlink($tmpFile); + + // Clean up uploaded files + $files = $this->request('GET', "admin/scores/$scoreId/files"); + if (!empty($files['body']['files'])) { + foreach ($files['body']['files'] as $f) { + if (strpos($f['path'], 'direction_') !== false) { + @unlink(SCORES_PATH . "$scoreId/" . $f['path']); + } + } + } + } + + private function multipartRequest($endpoint, $fields) { + $url = BASE_URL . '/' . $endpoint; + + $boundary = '----WebKitFormBoundary' . bin2hex(random_bytes(8)); + + $body = ''; + foreach ($fields as $field) { + $body .= "--$boundary\r\n"; + if (isset($field['file'])) { + $filename = basename($field['file']); + $content = file_get_contents($field['file']); + $body .= "Content-Disposition: form-data; name=\"{$field['name']}\"; filename=\"$filename\"\r\n"; + $body .= "Content-Type: application/pdf\r\n\r\n"; + $body .= $content . "\r\n"; + } else { + $body .= "Content-Disposition: form-data; name=\"{$field['name']}\"\r\n\r\n"; + $body .= $field['data'] . "\r\n"; + } + } + $body .= "--$boundary--\r\n"; + + $headers = [ + 'Content-Type: multipart/form-data; boundary=' . $boundary, + 'Authorization: Bearer ' . $this->token + ]; + + $context = stream_context_create([ + 'http' => [ + 'method' => 'POST', + 'header' => implode("\r\n", $headers), + 'content' => $body + ] + ]); + + $response = @file_get_contents($url, false, $context); + + $headers_list = $http_response_header ?? []; + $code = 0; + foreach ($headers_list as $header) { + if (preg_match('/^HTTP\/\d+(\.\d+)?\s+(\d{3})/', $header, $matches)) { + $code = (int)$matches[2]; + } + } + + return [ + 'code' => $code, + 'body' => json_decode($response, true) ?? [] + ]; + } + // ============ SECURITY TESTS ============ public function testSecurityHeaders() { @@ -316,12 +479,12 @@ class APITest { public function testCorsPolicy() { $this->section("Security - CORS Policy"); - // Test avec Origin non autorisé + // Test avec Origin autorisé $url = BASE_URL . '/login'; $context = stream_context_create([ 'http' => [ 'method' => 'OPTIONS', - 'header' => "Origin: https://evil.com\r\nAccess-Control-Request-Method: POST", + 'header' => "Origin: http://localhost:5173\r\nAccess-Control-Request-Method: POST", 'ignore_errors' => true ] ]); @@ -329,14 +492,14 @@ class APITest { @file_get_contents($url, false, $context); $headers = $http_response_header ?? []; - $hasWildcard = false; + $hasCorrectOrigin = false; foreach ($headers as $header) { - if (stripos($header, 'Access-Control-Allow-Origin: *') !== false) { - $hasWildcard = true; + if (stripos($header, 'Access-Control-Allow-Origin: http://localhost:5173') !== false) { + $hasCorrectOrigin = true; } } - $this->test('CORS does not allow wildcard (*)', !$hasWildcard); + $this->test('CORS allows localhost:5173', $hasCorrectOrigin); } public function testDirectoryTraversal() { @@ -498,6 +661,7 @@ $tests->testCreateScoreWithPieces(); // Files tests $tests->testFilesErrors(); $tests->testFilesSuccess(); +$tests->testWatermarkUpload(); // Security tests $tests->testSecurityHeaders(); diff --git a/deploy/deploy.sh b/deploy/deploy.sh index d9090b5..7175c1d 100755 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -44,7 +44,7 @@ cp /home/jbnadal/sources/jb/ohmj/ohmj2/partitions/nginx.conf partitions/build/ng # 7. Create zip (without tests.php and legacy) echo "[7/7] Creating zip..." -zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" +zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" "api/tests.php" echo "=== Done! ===" echo "Zip created: _builds/deploy_ohmj.zip" diff --git a/partitions/.env b/partitions/.env index fae3439..5934e2e 100644 --- a/partitions/.env +++ b/partitions/.env @@ -1 +1 @@ -VITE_API_URL=https://ohmj-api.c.nadal-fr.com +VITE_API_URL=http://localhost:8000 diff --git a/partitions/src/lib/api.ts b/partitions/src/lib/api.ts index 25e414b..a94ff9f 100644 --- a/partitions/src/lib/api.ts +++ b/partitions/src/lib/api.ts @@ -6,7 +6,7 @@ import { get } from 'svelte/store'; const API_BASE_URL_LOCAL = 'http://localhost:8000'; const API_BASE_URL_PROD = 'https://ohmj-api.c.nadal-fr.com'; -const API_BASE_URL = browser ? API_BASE_URL_PROD : API_BASE_URL_LOCAL; +const API_BASE_URL = import.meta.env.DEV ? API_BASE_URL_LOCAL : API_BASE_URL_PROD; const api = axios.create({ baseURL: API_BASE_URL, @@ -141,7 +141,7 @@ export const apiService = { return response.data; }, - async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string): Promise<{ success: boolean; path?: string; error?: string }> { + async uploadPdf(scoreId: string, file: File, piece: string, instrument: string, version: string, key?: string, clef?: string, variant?: string, part?: string, watermark?: boolean, watermarkPosition?: 'left' | 'right'): Promise<{ success: boolean; path?: string; error?: string }> { const formData = new FormData(); formData.append('file', file); formData.append('piece', piece); @@ -151,6 +151,8 @@ export const apiService = { if (clef) formData.append('clef', clef); if (variant) formData.append('variant', variant); if (part) formData.append('part', part); + if (watermark) formData.append('watermark', 'true'); + if (watermarkPosition) formData.append('watermarkPosition', watermarkPosition); const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, { headers: { diff --git a/partitions/src/routes/admin/+page.svelte b/partitions/src/routes/admin/+page.svelte index 8d01d37..34227cf 100644 --- a/partitions/src/routes/admin/+page.svelte +++ b/partitions/src/routes/admin/+page.svelte @@ -60,6 +60,7 @@ let uploadClef = $state(''); let uploadVariant = $state(''); let uploadPart = $state('1'); + let uploadWatermark = $state(false); function handlePieceCountChange(count: number) { newPieceCount = count; @@ -199,7 +200,8 @@ uploadKey, uploadClef, uploadVariant, - uploadPart + uploadPart, + uploadWatermark ); if (result.success) { uploadSuccess = 'Fichier uploadé avec succès!'; diff --git a/partitions/src/routes/admin/[id]/+page.svelte b/partitions/src/routes/admin/[id]/+page.svelte index 58e1529..947c37a 100644 --- a/partitions/src/routes/admin/[id]/+page.svelte +++ b/partitions/src/routes/admin/[id]/+page.svelte @@ -58,6 +58,8 @@ let uploadClef = $state(''); let uploadVariant = $state(''); let uploadPart = $state('1'); + let uploadWatermark = $state(false); + let uploadWatermarkPosition = $state<'left' | 'right'>('left'); // Auto-set default key when instrument changes $effect(() => { @@ -193,7 +195,9 @@ uploadKey, uploadClef, uploadVariant, - uploadPart + uploadPart, + uploadWatermark, + uploadWatermarkPosition ); if (result.success) { uploadSuccess = 'Fichier uploadé avec succès'; @@ -495,6 +499,22 @@
+
+ + {#if uploadWatermark} + + {/if} +