diff --git a/common/config/rush/common-versions.json b/common/config/rush/common-versions.json index 1df9ccf987..6c75d33a8e 100644 --- a/common/config/rush/common-versions.json +++ b/common/config/rush/common-versions.json @@ -58,5 +58,9 @@ // "typescript": [ // "~2.4.0" // ] + + // rekoni vendors an older pdfjs-dist server-side for PDF text extraction, + // while packages/ui uses a modern build for browser canvas rendering. + "pdfjs-dist": ["2.12.313", "4.10.38"] } } diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 928464ae94..f4668a68a9 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1808,6 +1808,9 @@ importers: compression-webpack-plugin: specifier: ^10.0.0 version: 10.0.0(webpack@5.102.1) + copy-webpack-plugin: + specifier: ^11.0.0 + version: 11.0.0(webpack@5.102.1) cross-env: specifier: ~7.0.3 version: 7.0.3 @@ -14994,6 +14997,9 @@ importers: fast-equals: specifier: ^5.2.2 version: 5.3.2 + pdfjs-dist: + specifier: 4.10.38 + version: 4.10.38 plyr: specifier: 3.7.8 version: 3.7.8 @@ -43178,6 +43184,76 @@ packages: cpu: [x64] os: [win32] + '@napi-rs/canvas-android-arm64@0.1.100': + resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/canvas-darwin-arm64@0.1.100': + resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/canvas-darwin-x64@0.1.100': + resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/canvas@0.1.100': + resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} + engines: {node: '>= 10'} + '@napi-rs/snappy-android-arm-eabi@7.3.3': resolution: {integrity: sha512-d4vUFFzNBvazGfB/KU8MnEax6itTIgRWXodPdZDnWKHy9HwVBndpCiedQDcSNHcZNYV36rx034rpn7SAuTL2NA==} engines: {node: '>= 10'} @@ -49917,6 +49993,10 @@ packages: worker-loader: optional: true + pdfjs-dist@4.10.38: + resolution: {integrity: sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==} + engines: {node: '>=20'} + pe-library@0.4.1: resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} engines: {node: '>=12', npm: '>=6'} @@ -53956,6 +54036,54 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': optional: true + '@napi-rs/canvas-android-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-arm64@0.1.100': + optional: true + + '@napi-rs/canvas-darwin-x64@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + optional: true + + '@napi-rs/canvas-linux-x64-musl@0.1.100': + optional: true + + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + optional: true + + '@napi-rs/canvas@0.1.100': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.100 + '@napi-rs/canvas-darwin-arm64': 0.1.100 + '@napi-rs/canvas-darwin-x64': 0.1.100 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 + '@napi-rs/canvas-linux-arm64-musl': 0.1.100 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-musl': 0.1.100 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 + '@napi-rs/canvas-win32-x64-msvc': 0.1.100 + optional: true + '@napi-rs/snappy-android-arm-eabi@7.3.3': optional: true @@ -61601,7 +61729,7 @@ snapshots: node-loader@2.0.0(webpack@5.102.1): dependencies: loader-utils: 2.0.4 - webpack: 5.102.1 + webpack: 5.102.1(esbuild@0.25.12)(webpack-cli@5.1.4) node-localstorage@2.2.1: dependencies: @@ -62025,6 +62153,10 @@ snapshots: pdfjs-dist@2.12.313: {} + pdfjs-dist@4.10.38: + optionalDependencies: + '@napi-rs/canvas': 0.1.100 + pe-library@0.4.1: {} peberminta@0.9.0: {} diff --git a/desktop/webpack.config.js b/desktop/webpack.config.js index d955e51256..012c799f05 100644 --- a/desktop/webpack.config.js +++ b/desktop/webpack.config.js @@ -21,6 +21,13 @@ const { EsbuildPlugin } = require('esbuild-loader') const doValidate = !prod || process.env.DO_VALIDATE === 'true' +// pdfjs-dist is a dependency of @hcengineering/ui, not of this package directly. +// Resolve it from there so the worker entry and the copied cmaps/standard_fonts +// assets stay in sync with whatever version @hcengineering/ui declares. +const pdfjsDistDir = path.dirname( + require.resolve('pdfjs-dist/package.json', { paths: [path.join(__dirname, '../packages/ui')] }) +) + /** * @type {Configuration} */ @@ -127,6 +134,11 @@ module.exports = [ entry: { bundle: ['@hcengineering/theme/styles/global.scss', ...['./src/ui/index.ts']], 'recorder-worker': '@hcengineering/recorder-resources/src/recorder-worker.ts', + // Flat, fixed-path fallback for the pdf.js worker (see createPdfWorker() in + // @hcengineering/ui's pdf-embed-utils.ts) — used when the primary + // `new Worker(new URL('pdfjs-dist/...', import.meta.url))` construction + // throws, mirroring the 'recorder-worker' entry above. + 'pdfjs-worker': path.join(pdfjsDistDir, 'build', 'pdf.worker.min.mjs'), findInPageOverlay: './src/ui/findInPageOverlay.ts' }, ignoreWarnings: [ @@ -321,7 +333,13 @@ module.exports = [ mode, plugins: [ new CopyPlugin({ - patterns: [{ from: 'public', to: 'public' }] + patterns: [ + { from: 'public', to: 'public' }, + // pdf.js needs these served alongside the app to render CJK/subset + // text and the 14 standard PDF fonts without embedded glyph data. + { from: path.join(pdfjsDistDir, 'cmaps'), to: 'cmaps' }, + { from: path.join(pdfjsDistDir, 'standard_fonts'), to: 'standard_fonts' } + ] }), new HtmlWebpackPlugin({ template: './src/ui/index.ejs', diff --git a/dev/prod/package.json b/dev/prod/package.json index eb365cf772..833f19c83f 100644 --- a/dev/prod/package.json +++ b/dev/prod/package.json @@ -28,6 +28,7 @@ "autoprefixer": "^10.4.14", "browserslist": "^4.28.2", "compression-webpack-plugin": "^10.0.0", + "copy-webpack-plugin": "^11.0.0", "cross-env": "~7.0.3", "css-loader": "^5.2.1", "dotenv-webpack": "^8.0.1", @@ -297,4 +298,4 @@ "readable-stream": "^4.7.0", "svelte": "^4.2.20" } -} \ No newline at end of file +} diff --git a/dev/prod/webpack.config.js b/dev/prod/webpack.config.js index 54298c063c..8e319c9b34 100644 --- a/dev/prod/webpack.config.js +++ b/dev/prod/webpack.config.js @@ -21,8 +21,16 @@ const path = require('path') const CompressionPlugin = require('compression-webpack-plugin') const DefinePlugin = require('webpack').DefinePlugin const HtmlWebpackPlugin = require('html-webpack-plugin') +const CopyPlugin = require('copy-webpack-plugin') const { Configuration } = require('webpack') +// pdfjs-dist is a dependency of @hcengineering/ui, not of this package directly. +// Resolve it from there so the copied cmaps/standard_fonts assets stay in sync +// with whatever version @hcengineering/ui declares. +const pdfjsDistDir = path.dirname( + require.resolve('pdfjs-dist/package.json', { paths: [path.join(__dirname, '../../packages/ui')] }) +) + const mode = process.env.NODE_ENV || 'development' const prod = mode === 'production' const clientType = process.env.CLIENT_TYPE ?? '' @@ -438,6 +446,14 @@ module.exports = [ viewport: 'width=device-width, initial-scale=1.0' } }), + new CopyPlugin({ + patterns: [ + // pdf.js needs these served alongside the app to render CJK/subset + // text and the 14 standard PDF fonts without embedded glyph data. + { from: path.join(pdfjsDistDir, 'cmaps'), to: 'cmaps' }, + { from: path.join(pdfjsDistDir, 'standard_fonts'), to: 'standard_fonts' } + ] + }), ...(prod ? [new CompressionPlugin()] : []), // new MiniCssExtractPlugin({ // filename: '[name].[id][contenthash].css' diff --git a/packages/presentation/src/components/PDFViewer.svelte b/packages/presentation/src/components/PDFViewer.svelte index 550d219f44..e37222aa78 100644 --- a/packages/presentation/src/components/PDFViewer.svelte +++ b/packages/presentation/src/components/PDFViewer.svelte @@ -15,6 +15,7 @@ @@ -97,7 +99,7 @@ {:else} - + {/if} {:else}
diff --git a/packages/ui/lang/en.json b/packages/ui/lang/en.json index 60ea507dbf..605923319e 100644 --- a/packages/ui/lang/en.json +++ b/packages/ui/lang/en.json @@ -111,6 +111,8 @@ "ThisYear": "This year", "EmojiStyle": "Emoji Style", "EmojiSystem": "System Default", - "EmojiNoto": "Noto Color" + "EmojiNoto": "Noto Color", + "Download": "Download", + "FailedToLoadPdf": "Failed to load the document" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 790803bd6f..e923d0cb8c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -52,7 +52,8 @@ "fast-equals": "^5.2.2", "svelte": "^4.2.20", "plyr": "3.7.8", - "blurhash": "^2.0.5" + "blurhash": "^2.0.5", + "pdfjs-dist": "4.10.38" }, "repository": { "type": "git", diff --git a/packages/ui/src/__tests__/pdf-embed-utils.test.ts b/packages/ui/src/__tests__/pdf-embed-utils.test.ts new file mode 100644 index 0000000000..05f9d245ea --- /dev/null +++ b/packages/ui/src/__tests__/pdf-embed-utils.test.ts @@ -0,0 +1,69 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { appendToken, computeRenderScale, getAssetBase } from '../pdf-embed-utils' + +describe('pdf-embed-utils', () => { + describe('computeRenderScale', () => { + it('returns 1 (native size) when fit is not requested', () => { + expect(computeRenderScale(false, 800, 400)).toBe(1) + }) + + it('scales the page to the container width when fit is requested', () => { + expect(computeRenderScale(true, 800, 400)).toBe(2) + expect(computeRenderScale(true, 400, 800)).toBe(0.5) + }) + + it('falls back to 1 when the container width is not yet known', () => { + expect(computeRenderScale(true, 0, 400)).toBe(1) + expect(computeRenderScale(true, -1, 400)).toBe(1) + }) + + it('falls back to 1 when the unscaled page width is degenerate', () => { + expect(computeRenderScale(true, 800, 0)).toBe(1) + }) + }) + + describe('appendToken', () => { + it('returns the url unchanged when no token is provided', () => { + expect(appendToken('https://files.example/blob/ws/abc', undefined)).toBe('https://files.example/blob/ws/abc') + expect(appendToken('https://files.example/blob/ws/abc', '')).toBe('https://files.example/blob/ws/abc') + }) + + it('uses ? when the url has no existing query string', () => { + expect(appendToken('https://files.example/blob/ws/abc', 'jwt')).toBe( + 'https://files.example/blob/ws/abc?token=jwt' + ) + }) + + it('uses & when the url already has a query string', () => { + expect(appendToken('https://files.example/files?file=abc&workspace=ws', 'jwt')).toBe( + 'https://files.example/files?file=abc&workspace=ws&token=jwt' + ) + }) + + it('url-encodes the token', () => { + expect(appendToken('https://files.example/blob/ws/abc', 'a.b/c+d=')).toBe( + 'https://files.example/blob/ws/abc?token=a.b%2Fc%2Bd%3D' + ) + }) + }) + + describe('getAssetBase', () => { + it('falls back to / when webpack public path is not defined', () => { + expect(getAssetBase()).toBe('/') + }) + }) +}) diff --git a/packages/ui/src/components/EmbeddedHTML.svelte b/packages/ui/src/components/EmbeddedHTML.svelte index e8cd5adfb7..716a43031f 100644 --- a/packages/ui/src/components/EmbeddedHTML.svelte +++ b/packages/ui/src/components/EmbeddedHTML.svelte @@ -15,7 +15,6 @@ {#if iframeSrc} - +