feat(apercu): revient au lecteur PDF natif du moteur
Le rendu maison sur <canvas> dimensionnait chaque page à la largeur du volet, sans jamais pouvoir aller au-delà : sur une page A4 (595,3 pt) dans un volet de 495 px — soit 40 % d'une fenêtre bureau de 1280 px — cela faisait une échelle de 0,83x, et les tableaux du CRG composés en 7 pt tombaient à 5,8 px de haut. Illisible, sans recours : aucun zoom, aucun re-rendu au redimensionnement (la canvas était simplement étirée en CSS), et devicePixelRatio à 1 sur écran non-HiDPI, donc aucun suréchantillonnage. L'<iframe> rend au moteur le zoom, la recherche, l'impression et la sélection de texte, tous absents jusqu'ici, et redessine à chaque niveau de zoom. Le document s'ouvre à `#zoom=100` : « ajuster à la largeur » ne sauverait rien dans un volet étroit (54 %, soit 5 px pour du 7 pt) alors qu'à 100 % le 7 pt fait 9,3 px. Les mots-clés `page-width`, `view=FitH` et `pagemode=none` sont ignorés par le lecteur de Chrome ; une valeur numérique est honorée. Un lien « ouvrir dans un nouvel onglet » sert de porte de sortie si le moteur n'embarque pas de lecteur PDF. pdfjs-dist n'a plus d'utilisateur : bundle principal 966 Ko -> 492 Ko, et le worker de 1,3 Mo n'est plus émis. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
278
frontend/package-lock.json
generated
278
frontend/package-lock.json
generated
@@ -9,7 +9,6 @@
|
|||||||
"version": "0.1.1",
|
"version": "0.1.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"pdfjs-dist": "^6.0.227",
|
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-chartjs": "^5.3.0",
|
"vue-chartjs": "^5.3.0",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
@@ -568,271 +567,6 @@
|
|||||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@napi-rs/canvas": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-Jqxcy1XOIqj+lH9sl1GT+il6GR3uQv13vI2mrwubP3uT8Olak2ClDrK2RnxlQKjwv8BRr4b3ug0YR7c6hBX8wg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"workspaces": [
|
|
||||||
"e2e/*"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@napi-rs/canvas-android-arm64": "1.0.0",
|
|
||||||
"@napi-rs/canvas-darwin-arm64": "1.0.0",
|
|
||||||
"@napi-rs/canvas-darwin-x64": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-arm-gnueabihf": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-arm64-gnu": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-arm64-musl": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-riscv64-gnu": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-x64-gnu": "1.0.0",
|
|
||||||
"@napi-rs/canvas-linux-x64-musl": "1.0.0",
|
|
||||||
"@napi-rs/canvas-win32-arm64-msvc": "1.0.0",
|
|
||||||
"@napi-rs/canvas-win32-x64-msvc": "1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-3hNKJObUK7JsCF9aJlVCs1J0/KE/gGfZNeK8MO1ge6bB3aicr5walGme9t9No1f/oyk9GgvdAT/rjSdsx3gbIw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"android"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-ZIja19/BiGz2puhki+WUYSRriwFeFJ8Mi9eK3hZdSS85w4Y60cuEAJVhMCfKwswQkKkUtrnzdKMBuO7TupvexA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-hImggWc82jqZVpEsFR9S7PE9OQYjq/H/D7vwCGB6X1jRH+UVBP1+1niJTPBOat1B154T6GKK7/kcFtoWgjgFzQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-hlJRy6d+kWLKVOG/+1rEvNQVURZ0DxxRPJsLmEWwhwiXZUJc0BF5o9esALHSEP4CoJK4wChRtj3hnyBgVx2oWA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-5Hru4T3RXkosRQafcjelv7AUzw9mXqmGYsxnzeDDOWveFCJyEPMSJltvGCM+jfH98seOCbfwm9KyFg6Jm5FhAA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-LTUl9jS8WsLSUGaxQZKQkxfluOJRpgvBuxxdM4pYcjib+di8AU4OzQc6+L6SzGMLcKc9H0RAjojRatBhTMqYdg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-Iz931SAZf+WVDzpjk52Q3ffW3zw0YflFwEZMgs036Wfu1kX/LrwT9wGjsuSqyduqefUkl91/vTdAjn8hQu5ezA==",
|
|
||||||
"cpu": [
|
|
||||||
"riscv64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-pFEQ5eFK4JusgN1K6KkO9DKP/Hi1WMJOkF8Ch03/khTc4bFbCKkCCsJG4YcOMOW9bI4XbT2/eMAWxhO0xaWgPA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-jnvr8NrLHiZ3NCiOKWqDbkI4Ah+QDrqtZ+sddPZBltEb1mQ2coSvCSJYfict+oAwcm0c970oTmVySpjKP/lnaA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-y2j9/Gfd5joqiqxdP/L1smqjQ+uAx3C4N0EC7bDHrnZEEH8ToM/OC5p3uHvtj4Lq591aHj+ArL01UDLNwT5HgQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-qwdhh9N6Gge/hC4pL9S1tQp0iKwhSl/dYjg7+RGp9k26iRGRi5MqqUyKGOXIWli0zOcuy5Y2wIH/jk2ry6i/jA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -2187,18 +1921,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/pdfjs-dist": {
|
|
||||||
"version": "6.0.227",
|
|
||||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.0.227.tgz",
|
|
||||||
"integrity": "sha512-/P6M4SXw+70waMVLUM7rdRtvo+dEzqE1t6W/zQNvBETo2MaRa5rrvCcAYdfWGiUzadTgM0lJmRApUrW0d9zgKg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=22.13.0 || >=24"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@napi-rs/canvas": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"pdfjs-dist": "^6.0.227",
|
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-chartjs": "^5.3.0",
|
"vue-chartjs": "^5.3.0",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.6.4"
|
||||||
|
|||||||
@@ -1,53 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col h-full bg-gray-950 overflow-hidden relative">
|
<div class="flex flex-col h-full bg-gray-950 overflow-hidden">
|
||||||
<!-- Header -->
|
<!-- Bandeau -->
|
||||||
<div class="flex-shrink-0 flex items-center px-3 py-2 bg-gray-900 border-b border-gray-700 text-white">
|
<div class="flex-shrink-0 flex items-center gap-2 px-3 py-2 bg-gray-900 border-b border-gray-700 text-white">
|
||||||
<span class="text-sm font-medium truncate">{{ fileName }}</span>
|
<span class="text-sm font-medium truncate flex-1">{{ fileName }}</span>
|
||||||
|
<!-- Porte de sortie : si le moteur n'a pas de lecteur PDF intégré, le
|
||||||
|
volet reste vide et ce lien reste le seul moyen d'ouvrir le
|
||||||
|
document. Il sert aussi à le lire en grand sans quitter la page. -->
|
||||||
|
<a
|
||||||
|
v-if="pdfUrl"
|
||||||
|
:href="pdfUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="flex-shrink-0 p-1 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
|
||||||
|
title="Ouvrir le PDF dans un nouvel onglet"
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- États -->
|
<div class="flex-1 relative overflow-hidden">
|
||||||
<div
|
<!-- Lecteur PDF natif du moteur : il apporte le zoom, la recherche, la
|
||||||
v-if="loading"
|
sélection de texte et l'impression sans code de notre côté, et
|
||||||
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
redessine à chaque niveau de zoom (donc net partout). Un rendu
|
||||||
>
|
maison sur <canvas> était figé à la largeur du volet, soit 0,83x
|
||||||
Chargement du PDF…
|
sur une page A4 dans une fenêtre de 1280 px : les tableaux du CRG
|
||||||
</div>
|
en 7 pt tombaient à 6 px de haut, illisibles. -->
|
||||||
<div
|
<iframe
|
||||||
v-else-if="error"
|
v-if="pdfUrl"
|
||||||
class="flex-1 flex items-center justify-center px-4 text-center text-red-400 text-sm"
|
:key="pdfUrl"
|
||||||
>
|
:src="urlLecteur"
|
||||||
{{ error }}
|
type="application/pdf"
|
||||||
</div>
|
:title="fileName"
|
||||||
<!-- Rendu PDF.js : une <canvas> par page, empilées et scrollables.
|
class="absolute inset-0 w-full h-full border-0"
|
||||||
Ne dépend pas du lecteur PDF natif du moteur (Qt WebEngine,
|
/>
|
||||||
WebView2, navigateur) -> rendu identique partout. -->
|
|
||||||
<div
|
<!-- Aucune source -->
|
||||||
v-else-if="hasSource"
|
<div v-else class="h-full flex items-center justify-center text-gray-500 text-sm">
|
||||||
ref="container"
|
Aucun PDF sélectionné
|
||||||
class="flex-1 overflow-auto custom-scrollbar p-2 bg-gray-950"
|
</div>
|
||||||
/>
|
|
||||||
<!-- Empty state -->
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex-1 flex items-center justify-center text-gray-500 text-sm"
|
|
||||||
>
|
|
||||||
Aucun PDF sélectionné
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onUnmounted, nextTick } from 'vue'
|
import { computed, ref, watch, onUnmounted } from 'vue'
|
||||||
// Build « legacy » (et non le build par défaut) : il embarque les polyfills
|
|
||||||
// nécessaires aux moteurs Chromium embarqués (Qt WebEngine, WebView2), plus
|
|
||||||
// anciens que la dernière version de Chrome. Le build moderne utilise
|
|
||||||
// `Map.prototype.getOrInsertComputed` (proposition JS récente) sans polyfill,
|
|
||||||
// ce qui casse le rendu dans la fenêtre bureau. Voir docs pdf.js « legacy ».
|
|
||||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.mjs'
|
|
||||||
import PdfWorker from 'pdfjs-dist/legacy/build/pdf.worker.min.mjs?url'
|
|
||||||
|
|
||||||
// Worker bundlé localement par Vite (pas de CDN) -> fonctionne hors-ligne.
|
|
||||||
pdfjsLib.GlobalWorkerOptions.workerSrc = PdfWorker
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
file: { type: File, default: null },
|
file: { type: File, default: null },
|
||||||
@@ -55,92 +53,44 @@ const props = defineProps({
|
|||||||
fileName: { type: String, default: 'document.pdf' }
|
fileName: { type: String, default: 'document.pdf' }
|
||||||
})
|
})
|
||||||
|
|
||||||
const container = ref(null)
|
const pdfUrl = ref(null)
|
||||||
const loading = ref(false)
|
let objectUrl = null
|
||||||
const error = ref(null)
|
|
||||||
const hasSource = ref(false)
|
|
||||||
|
|
||||||
let loadingTask = null
|
/** Ouverture à l'échelle 1:1. Le zoom par défaut du lecteur intégré tombe vers
|
||||||
// Jeton de génération : invalide les rendus concurrents (changement de source).
|
35-40 % dans un volet étroit, ce qui réduit les tableaux du CRG (7 pt) à
|
||||||
let renderToken = 0
|
3 px de haut. « Ajuster à la largeur » ne sauve rien ici — dans un volet de
|
||||||
|
430 px cela ne donne que 54 %, soit 5 px — alors qu'à 100 % le 7 pt fait
|
||||||
|
9,3 px et redevient lisible, quitte à défiler tant que le volet est étroit.
|
||||||
|
Les mots-clés `page-width`, `view=FitH` et `pagemode=none` sont ignorés par
|
||||||
|
le lecteur de Chrome ; une valeur numérique, elle, est bien honorée
|
||||||
|
(vérifié à la main). Le reste se règle dans le lecteur lui-même : boutons
|
||||||
|
− / + pour le zoom, ☰ pour replier la barre latérale de vignettes. Ces deux
|
||||||
|
choix sont mémorisés par le moteur, d'où le profil persistant imposé à la
|
||||||
|
fenêtre bureau (voir `desktop.py`). */
|
||||||
|
const urlLecteur = computed(() => (pdfUrl.value ? `${pdfUrl.value}#zoom=100` : null))
|
||||||
|
|
||||||
async function resolveSource() {
|
function libererObjectUrl() {
|
||||||
if (props.file) return { data: await props.file.arrayBuffer() }
|
if (objectUrl) {
|
||||||
if (props.url) return { url: props.url }
|
URL.revokeObjectURL(objectUrl)
|
||||||
return null
|
objectUrl = null
|
||||||
}
|
|
||||||
|
|
||||||
async function destroyTask() {
|
|
||||||
if (loadingTask) {
|
|
||||||
try {
|
|
||||||
await loadingTask.destroy()
|
|
||||||
} catch {
|
|
||||||
/* ignore */
|
|
||||||
}
|
|
||||||
loadingTask = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function render() {
|
watch(
|
||||||
const token = ++renderToken
|
[() => props.file, () => props.url],
|
||||||
error.value = null
|
([fichier, url]) => {
|
||||||
await destroyTask()
|
libererObjectUrl()
|
||||||
|
if (url) {
|
||||||
const source = await resolveSource()
|
pdfUrl.value = url
|
||||||
hasSource.value = source !== null
|
} else if (fichier) {
|
||||||
if (!source) {
|
objectUrl = URL.createObjectURL(fichier)
|
||||||
loading.value = false
|
pdfUrl.value = objectUrl
|
||||||
return
|
} else {
|
||||||
}
|
pdfUrl.value = null
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
loadingTask = pdfjsLib.getDocument(source)
|
|
||||||
const doc = await loadingTask.promise
|
|
||||||
if (token !== renderToken) return
|
|
||||||
|
|
||||||
// Le conteneur n'est monté qu'une fois loading=false.
|
|
||||||
loading.value = false
|
|
||||||
await nextTick()
|
|
||||||
const el = container.value
|
|
||||||
if (!el || token !== renderToken) return
|
|
||||||
el.innerHTML = ''
|
|
||||||
|
|
||||||
const targetWidth = Math.max(el.clientWidth - 16, 0)
|
|
||||||
const dpr = window.devicePixelRatio || 1
|
|
||||||
|
|
||||||
for (let n = 1; n <= doc.numPages; n++) {
|
|
||||||
if (token !== renderToken) return
|
|
||||||
const page = await doc.getPage(n)
|
|
||||||
const base = page.getViewport({ scale: 1 })
|
|
||||||
const scale = targetWidth > 0 ? targetWidth / base.width : 1
|
|
||||||
const viewport = page.getViewport({ scale })
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas')
|
|
||||||
canvas.width = Math.floor(viewport.width * dpr)
|
|
||||||
canvas.height = Math.floor(viewport.height * dpr)
|
|
||||||
canvas.style.width = '100%'
|
|
||||||
canvas.className = 'block mx-auto mb-2 bg-white shadow'
|
|
||||||
|
|
||||||
const ctx = canvas.getContext('2d')
|
|
||||||
ctx.scale(dpr, dpr)
|
|
||||||
await page.render({ canvasContext: ctx, viewport }).promise
|
|
||||||
if (token !== renderToken) return
|
|
||||||
el.appendChild(canvas)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
},
|
||||||
console.error('[PdfPreview] render error:', e?.message, e)
|
{ immediate: true }
|
||||||
if (token === renderToken) {
|
)
|
||||||
error.value = "Impossible d'afficher le PDF."
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([() => props.file, () => props.url], render, { immediate: true })
|
onUnmounted(libererObjectUrl)
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
renderToken++
|
|
||||||
destroyTask()
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
44
frontend/tests/pdfPreview.test.js
Normal file
44
frontend/tests/pdfPreview.test.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// L'apercu sert a relire le compte rendu ligne a ligne pendant qu'on corrige
|
||||||
|
// l'extraction : ce qui compte est qu'il s'ouvre a une echelle lisible, pas
|
||||||
|
// qu'il tienne entier dans le volet.
|
||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import { renderToString } from 'vue/server-renderer'
|
||||||
|
|
||||||
|
import PdfPreview from '../src/components/PdfPreview.vue'
|
||||||
|
|
||||||
|
function rendu(proprietes = {}) {
|
||||||
|
return renderToString(createSSRApp(PdfPreview, proprietes))
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('PdfPreview', () => {
|
||||||
|
it('ouvre le document a l echelle 1:1', async () => {
|
||||||
|
// Le zoom par defaut du lecteur integre tombe vers 35 % dans un volet
|
||||||
|
// etroit, ce qui reduit les tableaux du CRG (7 pt) a 3 px de haut. A 100 %
|
||||||
|
// ils font 9,3 px et redeviennent lisibles, quitte a devoir defiler.
|
||||||
|
const html = await rendu({ url: '/api/documents/37/pdf' })
|
||||||
|
|
||||||
|
expect(html).toContain('/api/documents/37/pdf#zoom=100')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('annonce l absence de document plutot qu un cadre vide', async () => {
|
||||||
|
const html = await rendu()
|
||||||
|
|
||||||
|
expect(html).toContain('Aucun PDF sélectionné')
|
||||||
|
expect(html).not.toContain('<iframe')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('offre d ouvrir le PDF hors du volet', async () => {
|
||||||
|
// Seule issue quand le moteur n'embarque pas de lecteur PDF : sans ce
|
||||||
|
// lien, le volet resterait vide sans recours.
|
||||||
|
const html = await rendu({ url: '/api/documents/37/pdf' })
|
||||||
|
|
||||||
|
expect(html).toContain('target="_blank"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('affiche le nom du fichier relu', async () => {
|
||||||
|
const html = await rendu({ url: '/api/documents/37/pdf', fileName: '2026 07 Servient.pdf' })
|
||||||
|
|
||||||
|
expect(html).toContain('2026 07 Servient.pdf')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user