mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(pdf): load resources required for CJK rendering
Co-authored-by: innocarpe <2222333+innocarpe@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
import { createReadStream, cpSync, existsSync, mkdirSync, statSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
export const PDFJS_VIEWER_ASSET_DIRS = ['cmaps', 'standard_fonts', 'wasm'] as const
|
||||
|
||||
function isAssetDirectory(
|
||||
value: string | undefined
|
||||
): value is (typeof PDFJS_VIEWER_ASSET_DIRS)[number] {
|
||||
return value !== undefined && PDFJS_VIEWER_ASSET_DIRS.some((directory) => directory === value)
|
||||
}
|
||||
|
||||
function pdfjsRoot(): string {
|
||||
return dirname(createRequire(import.meta.url).resolve('pdfjs-dist/package.json'))
|
||||
}
|
||||
|
||||
function assetPath(root: string, pathname: string): string | undefined {
|
||||
let decoded: string
|
||||
try {
|
||||
decoded = decodeURIComponent(pathname)
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
if (decoded.includes('\0') || decoded.includes('\\')) {
|
||||
return undefined
|
||||
}
|
||||
const parts = decoded.split('/').filter(Boolean)
|
||||
const directory = parts[0]
|
||||
if (parts.length < 2 || !isAssetDirectory(directory)) {
|
||||
return undefined
|
||||
}
|
||||
if (parts.some((part) => part === '.' || part === '..' || part.includes('\\'))) {
|
||||
return undefined
|
||||
}
|
||||
const candidate = resolve(root, ...parts)
|
||||
const base = resolve(root, directory)
|
||||
const withinBase = relative(base, candidate)
|
||||
if (withinBase.length === 0 || withinBase.startsWith('..') || isAbsolute(withinBase)) {
|
||||
return undefined
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
function copyAssets(root: string, outputDir: string): void {
|
||||
for (const directory of PDFJS_VIEWER_ASSET_DIRS) {
|
||||
const source = join(root, directory)
|
||||
if (!existsSync(source)) {
|
||||
throw new Error(`[pdfjs-viewer-assets] missing ${source}`)
|
||||
}
|
||||
cpSync(source, join(outputDir, directory), { recursive: true })
|
||||
}
|
||||
}
|
||||
|
||||
export function createPdfjsViewerAssetsPlugin(root = pdfjsRoot()): Plugin {
|
||||
return {
|
||||
name: 'pdfjs-viewer-assets',
|
||||
configureServer(server) {
|
||||
server.middlewares.use((request, response, next) => {
|
||||
let pathname: string
|
||||
try {
|
||||
pathname = new URL(request.url ?? '/', 'http://localhost').pathname
|
||||
} catch {
|
||||
next()
|
||||
return
|
||||
}
|
||||
const filePath = assetPath(root, pathname)
|
||||
if (!filePath || (request.method !== 'GET' && request.method !== 'HEAD')) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
let size: number
|
||||
try {
|
||||
size = statSync(filePath).size
|
||||
} catch {
|
||||
next()
|
||||
return
|
||||
}
|
||||
response.statusCode = 200
|
||||
response.setHeader('Content-Length', size)
|
||||
response.setHeader(
|
||||
'Content-Type',
|
||||
extname(filePath) === '.wasm' ? 'application/wasm' : 'application/octet-stream'
|
||||
)
|
||||
if (request.method === 'HEAD') {
|
||||
response.end()
|
||||
return
|
||||
}
|
||||
createReadStream(filePath).pipe(response)
|
||||
})
|
||||
},
|
||||
writeBundle(options) {
|
||||
if (!options.dir) {
|
||||
throw new Error('[pdfjs-viewer-assets] output directory is required')
|
||||
}
|
||||
mkdirSync(options.dir, { recursive: true })
|
||||
copyAssets(root, options.dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
@@ -18,6 +19,7 @@ const manifestPath = join(rendererOutput, '.vite', 'manifest.json')
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
||||
const selectedFiles = new Set(['web-index.html'])
|
||||
const visitedEntries = new Set()
|
||||
const PDFJS_VIEWER_ASSET_DIRS = ['cmaps', 'standard_fonts', 'wasm']
|
||||
|
||||
function assertEntryIsolation() {
|
||||
const entryKeys = new Set(
|
||||
@@ -119,6 +121,17 @@ function includeReferencedOutputs() {
|
||||
}
|
||||
}
|
||||
|
||||
function includePdfjsViewerAssets() {
|
||||
for (const directory of PDFJS_VIEWER_ASSET_DIRS) {
|
||||
const root = join(rendererOutput, directory)
|
||||
if (existsSync(root)) {
|
||||
for (const outputPath of listOutputFiles(root, directory)) {
|
||||
addOutputPath(outputPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function minifyWebOutput() {
|
||||
await Promise.all(
|
||||
[...selectedFiles]
|
||||
@@ -140,6 +153,7 @@ async function minifyWebOutput() {
|
||||
assertEntryIsolation()
|
||||
visitManifestEntry('web-index.html')
|
||||
includeReferencedOutputs()
|
||||
includePdfjsViewerAssets()
|
||||
|
||||
rmSync(stagingOutput, { force: true, recursive: true })
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineConfig, type UserConfig } from 'electron-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { createBootstrapFatalExitBanner } from './config/build-plugins/bootstrap-fatal-exit-banner'
|
||||
import { createPdfjsViewerAssetsPlugin } from './config/build-plugins/pdfjs-viewer-assets'
|
||||
import { createPlainNodeEntryGuardPlugin } from './config/build-plugins/plain-node-entry-guard'
|
||||
import packageJson from './package.json' with { type: 'json' }
|
||||
|
||||
@@ -307,7 +308,7 @@ export const electronViteConfig: UserConfig = {
|
||||
'@': resolve('src/renderer/src')
|
||||
}
|
||||
},
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), createPdfjsViewerAssetsPlugin()],
|
||||
worker: {
|
||||
format: 'es'
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { IncomingMessage, RequestListener, ServerResponse } from 'node:http
|
||||
import { extname, isAbsolute, posix, relative, resolve } from 'node:path'
|
||||
|
||||
const STATIC_WEB_ALLOWED_PATHS = new Set(['/web-index.html'])
|
||||
const STATIC_WEB_ALLOWED_PREFIXES = ['/assets/']
|
||||
const STATIC_WEB_ALLOWED_PREFIXES = ['/assets/', '/cmaps/', '/standard_fonts/', '/wasm/']
|
||||
const STATIC_WEB_CONTENT_TYPES = new Map([
|
||||
['.css', 'text/css; charset=utf-8'],
|
||||
['.html', 'text/html; charset=utf-8'],
|
||||
@@ -116,12 +116,14 @@ function mapProxyPrefixedStaticPathname(pathname: string): string {
|
||||
if (pathname === '/web-index.html' || pathname.endsWith('/web-index.html')) {
|
||||
return '/web-index.html'
|
||||
}
|
||||
const assetMarker = '/assets/'
|
||||
const assetIndex = pathname.indexOf(assetMarker)
|
||||
if (assetIndex !== -1) {
|
||||
const prefixIndex = STATIC_WEB_ALLOWED_PREFIXES.reduce(
|
||||
(deepest, prefix) => Math.max(deepest, pathname.indexOf(prefix)),
|
||||
-1
|
||||
)
|
||||
if (prefixIndex !== -1) {
|
||||
// Why: reverse proxies may forward the external path prefix through to
|
||||
// Orca. Only the bundled /assets subtree is served after the prefix.
|
||||
return pathname.slice(assetIndex)
|
||||
return pathname.slice(prefixIndex)
|
||||
}
|
||||
return pathname
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { keybindingMatchesAction } from '../../../../shared/keybindings'
|
||||
|
||||
import workerUrl from 'pdfjs-dist/build/pdf.worker.min.mjs?url'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { buildPdfJsDocumentOptions } from './pdf-js-document-options'
|
||||
import {
|
||||
applyPdfScalePreference,
|
||||
stepPdfScalePreference,
|
||||
@@ -240,7 +241,7 @@ export default function PdfViewer({
|
||||
// input listener above can see.
|
||||
eventBus.on('find', markUserMoved)
|
||||
|
||||
const loadingTask = pdfjsLib.getDocument({ data: bytes })
|
||||
const loadingTask = pdfjsLib.getDocument(buildPdfJsDocumentOptions(bytes, document.baseURI))
|
||||
|
||||
loadingTask.promise
|
||||
.then((doc) => {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildPdfJsDocumentOptions } from './pdf-js-document-options'
|
||||
|
||||
describe('pdf.js document resource options', () => {
|
||||
it('resolves CMaps, standard fonts, and WASM beside the document', () => {
|
||||
expect(
|
||||
buildPdfJsDocumentOptions(new Uint8Array([1]), 'https://orca.test/web-index.html')
|
||||
).toEqual({
|
||||
data: new Uint8Array([1]),
|
||||
cMapUrl: 'https://orca.test/cmaps/',
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: 'https://orca.test/standard_fonts/',
|
||||
wasmUrl: 'https://orca.test/wasm/'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
export function buildPdfJsDocumentOptions(data: Uint8Array, baseUrl: string) {
|
||||
return {
|
||||
data,
|
||||
cMapUrl: new URL('cmaps/', baseUrl).href,
|
||||
cMapPacked: true,
|
||||
standardFontDataUrl: new URL('standard_fonts/', baseUrl).href,
|
||||
wasmUrl: new URL('wasm/', baseUrl).href
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -2,13 +2,14 @@ import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { createPdfjsViewerAssetsPlugin } from './config/build-plugins/pdfjs-viewer-assets'
|
||||
|
||||
export default defineConfig({
|
||||
root: resolve('src/renderer'),
|
||||
// Why: pairing URLs may live under a reverse-proxy path prefix like
|
||||
// /orca/web-index.html, so built assets must resolve relative to the page.
|
||||
base: './',
|
||||
plugins: [react(), tailwindcss()],
|
||||
plugins: [react(), tailwindcss(), createPdfjsViewerAssetsPlugin()],
|
||||
define: {
|
||||
ORCA_FEATURE_WALL_ENABLED: 'true'
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user