feat: apercu PDF via PDF.js (rendu portable, build legacy)
Remplace l'<iframe> (lecteur PDF natif du moteur) par un rendu PDF.js sur <canvas>, une page par canvas empilee et scrollable. Rendu identique partout (Qt WebEngine, WebView2, navigateur), worker bundle localement par Vite -> fonctionne hors-ligne. Utilise le build *legacy* de pdfjs-dist : il embarque les polyfills requis par les Chromium embarques (Qt WebEngine, WebView2), plus anciens que Chrome. Le build par defaut appelle `Map.prototype.getOrInsertComputed` (proposition JS recente) sans polyfill, ce qui cassait silencieusement le rendu dans la fenetre bureau. Le catch logue desormais l'erreur au lieu de l'avaler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
278
frontend/package-lock.json
generated
278
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.1",
|
||||
"pdfjs-dist": "^6.0.227",
|
||||
"vue": "^3.4.21",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-router": "^4.6.4"
|
||||
@@ -515,6 +516,271 @@
|
||||
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -1680,6 +1946,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.1",
|
||||
"pdfjs-dist": "^6.0.227",
|
||||
"vue": "^3.4.21",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-router": "^4.6.4"
|
||||
|
||||
@@ -5,63 +5,142 @@
|
||||
<span class="text-sm font-medium truncate">{{ fileName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- PDF via native browser viewer -->
|
||||
<iframe
|
||||
v-if="pdfUrl"
|
||||
:src="pdfUrl"
|
||||
type="application/pdf"
|
||||
class="flex-1 w-full border-0"
|
||||
<!-- États -->
|
||||
<div
|
||||
v-if="loading"
|
||||
class="flex-1 flex items-center justify-center text-gray-400 text-sm"
|
||||
>
|
||||
Chargement du PDF…
|
||||
</div>
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="flex-1 flex items-center justify-center px-4 text-center text-red-400 text-sm"
|
||||
>
|
||||
{{ error }}
|
||||
</div>
|
||||
<!-- Rendu PDF.js : une <canvas> par page, empilées et scrollables.
|
||||
Ne dépend pas du lecteur PDF natif du moteur (Qt WebEngine,
|
||||
WebView2, navigateur) -> rendu identique partout. -->
|
||||
<div
|
||||
v-else-if="hasSource"
|
||||
ref="container"
|
||||
class="flex-1 overflow-auto p-2 bg-gray-800"
|
||||
/>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-else class="flex-1 flex items-center justify-center text-gray-400 text-sm">
|
||||
<div
|
||||
v-else
|
||||
class="flex-1 flex items-center justify-center text-gray-400 text-sm"
|
||||
>
|
||||
Aucun PDF sélectionné
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { ref, watch, onUnmounted, nextTick } 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({
|
||||
file: {
|
||||
type: File,
|
||||
default: null
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
default: 'document.pdf'
|
||||
}
|
||||
file: { type: File, default: null },
|
||||
url: { type: String, default: null },
|
||||
fileName: { type: String, default: 'document.pdf' }
|
||||
})
|
||||
|
||||
const pdfUrl = ref(null)
|
||||
let objectUrl = null
|
||||
const container = ref(null)
|
||||
const loading = ref(false)
|
||||
const error = ref(null)
|
||||
const hasSource = ref(false)
|
||||
|
||||
function cleanup() {
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
objectUrl = null
|
||||
let loadingTask = null
|
||||
// Jeton de génération : invalide les rendus concurrents (changement de source).
|
||||
let renderToken = 0
|
||||
|
||||
async function resolveSource() {
|
||||
if (props.file) return { data: await props.file.arrayBuffer() }
|
||||
if (props.url) return { url: props.url }
|
||||
return null
|
||||
}
|
||||
|
||||
async function destroyTask() {
|
||||
if (loadingTask) {
|
||||
try {
|
||||
await loadingTask.destroy()
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
loadingTask = null
|
||||
}
|
||||
}
|
||||
|
||||
watch([() => props.file, () => props.url], ([newFile, newUrl]) => {
|
||||
cleanup()
|
||||
async function render() {
|
||||
const token = ++renderToken
|
||||
error.value = null
|
||||
await destroyTask()
|
||||
|
||||
if (newUrl) {
|
||||
pdfUrl.value = newUrl
|
||||
} else if (newFile) {
|
||||
objectUrl = URL.createObjectURL(newFile)
|
||||
pdfUrl.value = objectUrl
|
||||
} else {
|
||||
pdfUrl.value = null
|
||||
const source = await resolveSource()
|
||||
hasSource.value = source !== null
|
||||
if (!source) {
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
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)
|
||||
if (token === renderToken) {
|
||||
error.value = "Impossible d'afficher le PDF."
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch([() => props.file, () => props.url], render, { immediate: true })
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
renderToken++
|
||||
destroyTask()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user