mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
perf(compression): add vite-plugin-compression for gzip and Brotli support (#6279)
Signed-off-by: Balázs Szücs <[email protected]>
This commit is contained in:
@@ -110,8 +110,8 @@ jobs:
|
|||||||
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
NPM_CONFIG_IGNORE_SCRIPTS: "true"
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
run: |
|
run: |
|
||||||
mkdir -p src/assets
|
mkdir -p editor/src/assets
|
||||||
npx --yes license-report --only=prod --output=json > src/assets/3rdPartyLicenses.json
|
npx --yes license-report --only=prod --output=json > editor/src/assets/3rdPartyLicenses.json
|
||||||
|
|
||||||
- name: Postprocess with project script (BASE version)
|
- name: Postprocess with project script (BASE version)
|
||||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ app/core/src/main/resources/static/robots.txt
|
|||||||
app/core/src/main/resources/static/pdfium/
|
app/core/src/main/resources/static/pdfium/
|
||||||
app/core/src/main/resources/static/pdfjs/
|
app/core/src/main/resources/static/pdfjs/
|
||||||
app/core/src/main/resources/static/vendor/
|
app/core/src/main/resources/static/vendor/
|
||||||
|
app/core/src/main/resources/static/**/*.gz
|
||||||
|
app/core/src/main/resources/static/**/*.br
|
||||||
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
|
# Note: Keep backend-managed files like fonts/, css/, js/, pdfjs/, etc.
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
|
|||||||
+17
-1
@@ -207,13 +207,29 @@ def resourcesStaticDir = file('src/main/resources/static')
|
|||||||
def generatedFrontendPaths = [
|
def generatedFrontendPaths = [
|
||||||
'assets',
|
'assets',
|
||||||
'index.html',
|
'index.html',
|
||||||
|
'index.html.gz',
|
||||||
|
'index.html.br',
|
||||||
|
'sw.js',
|
||||||
|
'sw.js.gz',
|
||||||
|
'sw.js.br',
|
||||||
|
'manifest.json.gz',
|
||||||
|
'manifest.json.br',
|
||||||
|
'site.webmanifest.gz',
|
||||||
|
'site.webmanifest.br',
|
||||||
|
'browserconfig.xml.gz',
|
||||||
|
'browserconfig.xml.br',
|
||||||
|
'manifest-classic.json',
|
||||||
|
'manifest-classic.json.gz',
|
||||||
|
'manifest-classic.json.br',
|
||||||
'locales',
|
'locales',
|
||||||
'Login',
|
'Login',
|
||||||
'classic-logo',
|
'classic-logo',
|
||||||
'modern-logo',
|
'modern-logo',
|
||||||
'og_images',
|
'og_images',
|
||||||
'samples',
|
'samples',
|
||||||
'manifest-classic.json'
|
'pdfium',
|
||||||
|
'vendor',
|
||||||
|
'pdfjs'
|
||||||
]
|
]
|
||||||
|
|
||||||
tasks.register('npmInstall', Exec) {
|
tasks.register('npmInstall', Exec) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|||||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
import org.springframework.web.servlet.resource.EncodedResourceResolver;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@@ -49,14 +50,16 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
"/sw.js", "/manifest.json", "/site.webmanifest", "/browserconfig.xml")
|
"/sw.js", "/manifest.json", "/site.webmanifest", "/browserconfig.xml")
|
||||||
.addResourceLocations(staticPath, "classpath:/static/")
|
.addResourceLocations(staticPath, "classpath:/static/")
|
||||||
.setCacheControl(CacheControl.noStore())
|
.setCacheControl(CacheControl.noStore())
|
||||||
.resourceChain(true);
|
.resourceChain(true)
|
||||||
|
.addResolver(new EncodedResourceResolver());
|
||||||
|
|
||||||
// 2. Vite fingerprinted assets (immutable)
|
// 2. Vite fingerprinted assets (immutable)
|
||||||
// These already have content hashes in filenames (e.g. index-ChAS4tCC.js)
|
// These already have content hashes in filenames (e.g. index-ChAS4tCC.js)
|
||||||
registry.addResourceHandler("/assets/**")
|
registry.addResourceHandler("/assets/**")
|
||||||
.addResourceLocations(staticPath + "assets/", "classpath:/static/assets/")
|
.addResourceLocations(staticPath + "assets/", "classpath:/static/assets/")
|
||||||
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
||||||
.resourceChain(true);
|
.resourceChain(true)
|
||||||
|
.addResolver(new EncodedResourceResolver());
|
||||||
|
|
||||||
// 3. Media and fonts (immutable)
|
// 3. Media and fonts (immutable)
|
||||||
registry.addResourceHandler("/images/**", "/fonts/**")
|
registry.addResourceHandler("/images/**", "/fonts/**")
|
||||||
@@ -66,7 +69,8 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
staticPath + "fonts/",
|
staticPath + "fonts/",
|
||||||
"classpath:/static/fonts/")
|
"classpath:/static/fonts/")
|
||||||
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
.setCacheControl(IMMUTABLE_ONE_YEAR)
|
||||||
.resourceChain(true);
|
.resourceChain(true)
|
||||||
|
.addResolver(new EncodedResourceResolver());
|
||||||
|
|
||||||
// 4. Branding and stable non-fingerprinted assets (1 day + SWR)
|
// 4. Branding and stable non-fingerprinted assets (1 day + SWR)
|
||||||
// Use stale-while-revalidate to improve perceived performance.
|
// Use stale-while-revalidate to improve perceived performance.
|
||||||
@@ -114,19 +118,27 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
|||||||
staticPath + "og_images/",
|
staticPath + "og_images/",
|
||||||
"classpath:/static/og_images/",
|
"classpath:/static/og_images/",
|
||||||
staticPath + "Login/",
|
staticPath + "Login/",
|
||||||
"classpath:/static/Login/")
|
"classpath:/static/Login/",
|
||||||
|
staticPath + "icons/",
|
||||||
|
"classpath:/static/icons/",
|
||||||
|
staticPath + "modern-logo/",
|
||||||
|
"classpath:/static/modern-logo/",
|
||||||
|
staticPath + "classic-logo/",
|
||||||
|
"classpath:/static/classic-logo/")
|
||||||
.setCacheControl(
|
.setCacheControl(
|
||||||
CacheControl.maxAge(Duration.ofDays(1))
|
CacheControl.maxAge(Duration.ofDays(1))
|
||||||
.cachePublic()
|
.cachePublic()
|
||||||
.staleWhileRevalidate(Duration.ofDays(7)))
|
.staleWhileRevalidate(Duration.ofDays(7)))
|
||||||
.resourceChain(true);
|
.resourceChain(true)
|
||||||
|
.addResolver(new EncodedResourceResolver());
|
||||||
|
|
||||||
// 5. Catch-all (SPA fallback)
|
// 5. Catch-all (SPA fallback)
|
||||||
// Must check with server to ensure index.html is always fresh.
|
// Must check with server to ensure index.html is always fresh.
|
||||||
registry.addResourceHandler("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations(staticPath, "classpath:/static/")
|
.addResourceLocations(staticPath, "classpath:/static/")
|
||||||
.setCacheControl(NO_CACHE)
|
.setCacheControl(NO_CACHE)
|
||||||
.resourceChain(true);
|
.resourceChain(true)
|
||||||
|
.addResolver(new EncodedResourceResolver());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ spring.security.filter.dispatcher-types=REQUEST,ERROR
|
|||||||
# Response compression
|
# Response compression
|
||||||
server.compression.enabled=true
|
server.compression.enabled=true
|
||||||
server.compression.min-response-size=1024
|
server.compression.min-response-size=1024
|
||||||
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript,image/svg+xml,application/x-font-ttf,font/opentype,application/vnd.ms-fontobject,font/woff,font/woff2,application/font-woff,application/font-woff2
|
server.compression.mime-types=application/json,application/xml,text/html,text/plain,text/css,application/javascript,image/svg+xml,application/x-font-ttf,font/opentype,application/vnd.ms-fontobject,font/woff,font/woff2,application/font-woff,application/font-woff2,application/wasm
|
||||||
|
|
||||||
spring.web.error.path=/error
|
spring.web.error.path=/error
|
||||||
spring.web.error.whitelabel.enabled=false
|
spring.web.error.whitelabel.enabled=false
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ import { RedactionAPIBridge } from "@app/components/viewer/RedactionAPIBridge";
|
|||||||
import { DocumentPermissionsAPIBridge } from "@app/components/viewer/DocumentPermissionsAPIBridge";
|
import { DocumentPermissionsAPIBridge } from "@app/components/viewer/DocumentPermissionsAPIBridge";
|
||||||
import { DocumentReadyWrapper } from "@app/components/viewer/DocumentReadyWrapper";
|
import { DocumentReadyWrapper } from "@app/components/viewer/DocumentReadyWrapper";
|
||||||
import { ActiveDocumentProvider } from "@app/components/viewer/ActiveDocumentContext";
|
import { ActiveDocumentProvider } from "@app/components/viewer/ActiveDocumentContext";
|
||||||
import { absoluteWithBasePath } from "@app/constants/app";
|
import { pdfiumWasmUrl } from "@app/services/wasmPrecompiler";
|
||||||
import { FormFieldOverlay } from "@app/tools/formFill/FormFieldOverlay";
|
import { FormFieldOverlay } from "@app/tools/formFill/FormFieldOverlay";
|
||||||
import { ButtonAppearanceOverlay } from "@app/tools/formFill/ButtonAppearanceOverlay";
|
import { ButtonAppearanceOverlay } from "@app/tools/formFill/ButtonAppearanceOverlay";
|
||||||
import SignatureFieldOverlay from "@app/components/viewer/SignatureFieldOverlay";
|
import SignatureFieldOverlay from "@app/components/viewer/SignatureFieldOverlay";
|
||||||
@@ -296,7 +296,7 @@ export function LocalEmbedPDF({
|
|||||||
|
|
||||||
// Initialize the engine with the React hook - use local WASM for offline support
|
// Initialize the engine with the React hook - use local WASM for offline support
|
||||||
const { engine, isLoading, error } = usePdfiumEngine({
|
const { engine, isLoading, error } = usePdfiumEngine({
|
||||||
wasmUrl: absoluteWithBasePath("/pdfium/pdfium.wasm"),
|
wasmUrl: pdfiumWasmUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Early return if no file or URL provided
|
// Early return if no file or URL provided
|
||||||
|
|||||||
@@ -12,9 +12,34 @@
|
|||||||
* `getSignatures`, `saveAsCopy`, …) wrap the `PdfEngine` interface so callers
|
* `getSignatures`, `saveAsCopy`, …) wrap the `PdfEngine` interface so callers
|
||||||
* never have to deal with raw pointers or Tasks.
|
* never have to deal with raw pointers or Tasks.
|
||||||
*/
|
*/
|
||||||
import { init, type WrappedPdfiumModule } from "@embedpdf/pdfium";
|
import {
|
||||||
|
init,
|
||||||
|
type WrappedPdfiumModule,
|
||||||
|
type PdfiumModule,
|
||||||
|
} from "@embedpdf/pdfium";
|
||||||
|
import {
|
||||||
|
pdfiumWasmModulePromise,
|
||||||
|
startEagerWasmCompilation,
|
||||||
|
pdfiumWasmUrl,
|
||||||
|
} from "@app/services/wasmPrecompiler";
|
||||||
import type { FormField, WidgetCoordinates } from "@app/tools/formFill/types";
|
import type { FormField, WidgetCoordinates } from "@app/tools/formFill/types";
|
||||||
|
|
||||||
|
interface ExtendedPdfiumRuntime {
|
||||||
|
HEAPU8: Uint8Array;
|
||||||
|
HEAPF32: Float32Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PdfiumModuleOverrides extends Partial<PdfiumModule> {
|
||||||
|
locateFile?: (url: string, scriptDirectory?: string) => string;
|
||||||
|
instantiateWasm?: (
|
||||||
|
imports: WebAssembly.Imports,
|
||||||
|
successCallback: (
|
||||||
|
instance: WebAssembly.Instance,
|
||||||
|
module: WebAssembly.Module,
|
||||||
|
) => void,
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
// PDF form field type constants (matching PDFium C API FPDF_FORMFIELD_* values)
|
// PDF form field type constants (matching PDFium C API FPDF_FORMFIELD_* values)
|
||||||
const FPDF_FORMFIELD_UNKNOWN = 0;
|
const FPDF_FORMFIELD_UNKNOWN = 0;
|
||||||
const FPDF_FORMFIELD_PUSHBUTTON = 1;
|
const FPDF_FORMFIELD_PUSHBUTTON = 1;
|
||||||
@@ -46,8 +71,7 @@ let _module: WrappedPdfiumModule | null = null;
|
|||||||
* Resolve the absolute WASM URL using the same pattern as LocalEmbedPDF.
|
* Resolve the absolute WASM URL using the same pattern as LocalEmbedPDF.
|
||||||
*/
|
*/
|
||||||
function wasmUrl(): string {
|
function wasmUrl(): string {
|
||||||
const base = (import.meta as any).env?.BASE_URL ?? "/";
|
return pdfiumWasmUrl;
|
||||||
return `${base}pdfium/pdfium.wasm`.replace(/\/\//g, "/");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -59,9 +83,47 @@ function wasmUrl(): string {
|
|||||||
export async function getPdfiumModule(): Promise<WrappedPdfiumModule> {
|
export async function getPdfiumModule(): Promise<WrappedPdfiumModule> {
|
||||||
if (_module) return _module;
|
if (_module) return _module;
|
||||||
if (!_initPromise) {
|
if (!_initPromise) {
|
||||||
_initPromise = init({
|
// Ensure eager compilation has started if PDF service is requested before idle timeout
|
||||||
|
startEagerWasmCompilation();
|
||||||
|
|
||||||
|
const overrides: PdfiumModuleOverrides = {
|
||||||
locateFile: () => wasmUrl(),
|
locateFile: () => wasmUrl(),
|
||||||
} as any).then((m) => {
|
};
|
||||||
|
|
||||||
|
// Eagerly reuse pre-compiled WASM module from app boot if available
|
||||||
|
overrides.instantiateWasm = (
|
||||||
|
imports: WebAssembly.Imports,
|
||||||
|
successCallback: (
|
||||||
|
instance: WebAssembly.Instance,
|
||||||
|
module: WebAssembly.Module,
|
||||||
|
) => void,
|
||||||
|
) => {
|
||||||
|
pdfiumWasmModulePromise
|
||||||
|
.then((wasmModule) => {
|
||||||
|
if (wasmModule) {
|
||||||
|
return WebAssembly.instantiate(wasmModule, imports).then(
|
||||||
|
(instance) => {
|
||||||
|
successCallback(instance, wasmModule);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error("No pre-compiled WASM module found");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
console.warn(
|
||||||
|
"Eager WebAssembly instantiation failed, falling back to streaming compilation:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
WebAssembly.instantiateStreaming(fetch(wasmUrl()), imports).then(
|
||||||
|
(result) => {
|
||||||
|
successCallback(result.instance, result.module);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
_initPromise = init(overrides as Partial<PdfiumModule>).then((m) => {
|
||||||
// Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up
|
// Call PDFiumExt_Init to ensure extensions (form fill etc.) are set up
|
||||||
try {
|
try {
|
||||||
m.PDFiumExt_Init();
|
m.PDFiumExt_Init();
|
||||||
@@ -110,9 +172,9 @@ export function readAnnotRectAdjusted(
|
|||||||
annotPtr: number,
|
annotPtr: number,
|
||||||
rectBuf: number,
|
rectBuf: number,
|
||||||
): boolean {
|
): boolean {
|
||||||
const ext = (m as any).EPDFAnnot_GetRect;
|
const ext = m.EPDFAnnot_GetRect;
|
||||||
if (typeof ext === "function") {
|
if (typeof ext === "function") {
|
||||||
return ext.call(m, annotPtr, rectBuf);
|
return ext(annotPtr, rectBuf);
|
||||||
}
|
}
|
||||||
return m.FPDFAnnot_GetRect(annotPtr, rectBuf);
|
return m.FPDFAnnot_GetRect(annotPtr, rectBuf);
|
||||||
}
|
}
|
||||||
@@ -186,15 +248,13 @@ export function readEffectivePageBox(
|
|||||||
let result: PageBox | null = null;
|
let result: PageBox | null = null;
|
||||||
try {
|
try {
|
||||||
// CropBox is the effective visible area
|
// CropBox is the effective visible area
|
||||||
if (
|
if (m.FPDFPage_GetCropBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)) {
|
||||||
(m as any).FPDFPage_GetCropBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)
|
|
||||||
) {
|
|
||||||
result = read();
|
result = read();
|
||||||
}
|
}
|
||||||
// Fall back to MediaBox
|
// Fall back to MediaBox
|
||||||
if (
|
if (
|
||||||
!result &&
|
!result &&
|
||||||
(m as any).FPDFPage_GetMediaBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)
|
m.FPDFPage_GetMediaBox(pagePtr, buf, buf + 4, buf + 8, buf + 12)
|
||||||
) {
|
) {
|
||||||
result = read();
|
result = read();
|
||||||
}
|
}
|
||||||
@@ -226,7 +286,7 @@ function copyToWasmHeap(
|
|||||||
bytes: Uint8Array,
|
bytes: Uint8Array,
|
||||||
ptr: number,
|
ptr: number,
|
||||||
): void {
|
): void {
|
||||||
new Uint8Array((m.pdfium.wasmExports as any).memory.buffer).set(bytes, ptr);
|
(m.pdfium as typeof m.pdfium & ExtendedPdfiumRuntime).HEAPU8.set(bytes, ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1338,9 +1398,13 @@ async function renderWidgetAppearance(
|
|||||||
formEnvPtr: number,
|
formEnvPtr: number,
|
||||||
dpr: number,
|
dpr: number,
|
||||||
): Promise<ImageData | null> {
|
): Promise<ImageData | null> {
|
||||||
const pdfiumWasm = m.pdfium as any;
|
|
||||||
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
||||||
const matrixView = new Float32Array(pdfiumWasm.HEAPF32.buffer, matrixPtr, 6);
|
const pdfiumRuntime = m.pdfium as typeof m.pdfium & ExtendedPdfiumRuntime;
|
||||||
|
const matrixView = new Float32Array(
|
||||||
|
pdfiumRuntime.HEAPF32.buffer,
|
||||||
|
matrixPtr,
|
||||||
|
6,
|
||||||
|
);
|
||||||
const sx = wDev / pdfW;
|
const sx = wDev / pdfW;
|
||||||
const sy = hDev / pdfH;
|
const sy = hDev / pdfH;
|
||||||
matrixView.set([sx, 0, 0, -sy, -sx * annotLeft, sy * annotTop]);
|
matrixView.set([sx, 0, 0, -sy, -sx * annotLeft, sy * annotTop]);
|
||||||
@@ -1364,7 +1428,7 @@ async function renderWidgetAppearance(
|
|||||||
let imageData: ImageData | null = null;
|
let imageData: ImageData | null = null;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
const rgba = new Uint8ClampedArray(
|
const rgba = new Uint8ClampedArray(
|
||||||
pdfiumWasm.HEAPU8.buffer.slice(heapPtr, heapPtr + bytes),
|
pdfiumRuntime.HEAPU8.subarray(heapPtr, heapPtr + bytes),
|
||||||
);
|
);
|
||||||
let hasVisible = false;
|
let hasVisible = false;
|
||||||
for (let i = 3; i < rgba.length; i += 4) {
|
for (let i = 3; i < rgba.length; i += 4) {
|
||||||
@@ -1417,7 +1481,7 @@ async function renderWidgetAppearance(
|
|||||||
m.FPDFBitmap_Destroy(bmp2);
|
m.FPDFBitmap_Destroy(bmp2);
|
||||||
|
|
||||||
const rgba2 = new Uint8ClampedArray(
|
const rgba2 = new Uint8ClampedArray(
|
||||||
pdfiumWasm.HEAPU8.buffer.slice(heap2, heap2 + bytes),
|
pdfiumRuntime.HEAPU8.subarray(heap2, heap2 + bytes),
|
||||||
);
|
);
|
||||||
let hasVisible2 = false;
|
let hasVisible2 = false;
|
||||||
for (let i = 3; i < rgba2.length; i += 4) {
|
for (let i = 3; i < rgba2.length; i += 4) {
|
||||||
@@ -1560,8 +1624,6 @@ export async function renderSignatureFieldAppearances(
|
|||||||
const hDev = Math.max(1, Math.round(pdfH * dpr));
|
const hDev = Math.max(1, Math.round(pdfH * dpr));
|
||||||
const stride = wDev * 4;
|
const stride = wDev * 4;
|
||||||
const bytes = stride * hDev;
|
const bytes = stride * hDev;
|
||||||
const pdfiumWasm = m.pdfium as any;
|
|
||||||
|
|
||||||
const heapPtr = m.pdfium.wasmExports.malloc(bytes);
|
const heapPtr = m.pdfium.wasmExports.malloc(bytes);
|
||||||
const bitmapPtr = m.FPDFBitmap_CreateEx(
|
const bitmapPtr = m.FPDFBitmap_CreateEx(
|
||||||
wDev,
|
wDev,
|
||||||
@@ -1577,8 +1639,10 @@ export async function renderSignatureFieldAppearances(
|
|||||||
const sx = wDev / pdfW;
|
const sx = wDev / pdfW;
|
||||||
const sy = hDev / pdfH;
|
const sy = hDev / pdfH;
|
||||||
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
const matrixPtr = m.pdfium.wasmExports.malloc(6 * 4);
|
||||||
|
const pdfiumRuntime = m.pdfium as typeof m.pdfium &
|
||||||
|
ExtendedPdfiumRuntime;
|
||||||
const matrixView = new Float32Array(
|
const matrixView = new Float32Array(
|
||||||
pdfiumWasm.HEAPF32.buffer,
|
pdfiumRuntime.HEAPF32.buffer,
|
||||||
matrixPtr,
|
matrixPtr,
|
||||||
6,
|
6,
|
||||||
);
|
);
|
||||||
@@ -1603,7 +1667,7 @@ export async function renderSignatureFieldAppearances(
|
|||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
const rgba = new Uint8ClampedArray(
|
const rgba = new Uint8ClampedArray(
|
||||||
pdfiumWasm.HEAPU8.buffer.slice(heapPtr, heapPtr + bytes),
|
pdfiumRuntime.HEAPU8.subarray(heapPtr, heapPtr + bytes),
|
||||||
);
|
);
|
||||||
let hasVisible = false;
|
let hasVisible = false;
|
||||||
for (let i = 3; i < rgba.length; i += 4) {
|
for (let i = 3; i < rgba.length; i += 4) {
|
||||||
@@ -1663,7 +1727,7 @@ export async function renderSignatureFieldAppearances(
|
|||||||
m.FPDFBitmap_Destroy(bmp2);
|
m.FPDFBitmap_Destroy(bmp2);
|
||||||
|
|
||||||
const rgba2 = new Uint8ClampedArray(
|
const rgba2 = new Uint8ClampedArray(
|
||||||
pdfiumWasm.HEAPU8.buffer.slice(heap2, heap2 + bytes),
|
pdfiumRuntime.HEAPU8.subarray(heap2, heap2 + bytes),
|
||||||
);
|
);
|
||||||
let hasVisible2 = false;
|
let hasVisible2 = false;
|
||||||
for (let i = 3; i < rgba2.length; i += 4) {
|
for (let i = 3; i < rgba2.length; i += 4) {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { BASE_PATH } from "@app/constants/app";
|
||||||
|
import pdfiumWasmAssetUrl from "@embedpdf/pdfium/pdfium.wasm?url";
|
||||||
|
|
||||||
|
const getWasmUrl = (): string => {
|
||||||
|
if (
|
||||||
|
pdfiumWasmAssetUrl.startsWith("http://") ||
|
||||||
|
pdfiumWasmAssetUrl.startsWith("https://") ||
|
||||||
|
pdfiumWasmAssetUrl.startsWith("//")
|
||||||
|
) {
|
||||||
|
return pdfiumWasmAssetUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
return `${origin}${BASE_PATH}/pdfium/pdfium.wasm`;
|
||||||
|
}
|
||||||
|
const cleanAssetUrl = pdfiumWasmAssetUrl
|
||||||
|
.replace(/^\.\//, "")
|
||||||
|
.replace(/^\//, "");
|
||||||
|
return `${origin}${BASE_PATH}/${cleanAssetUrl}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pdfiumWasmUrl = getWasmUrl();
|
||||||
|
|
||||||
|
let resolvePromise: (module: WebAssembly.Module | null) => void;
|
||||||
|
let compilationStarted = false;
|
||||||
|
|
||||||
|
export const pdfiumWasmModulePromise = new Promise<WebAssembly.Module | null>(
|
||||||
|
(resolve) => {
|
||||||
|
resolvePromise = resolve;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export function startEagerWasmCompilation(): void {
|
||||||
|
if (compilationStarted) return;
|
||||||
|
compilationStarted = true;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof WebAssembly === "object" &&
|
||||||
|
typeof WebAssembly.compileStreaming === "function"
|
||||||
|
) {
|
||||||
|
WebAssembly.compileStreaming(fetch(pdfiumWasmUrl))
|
||||||
|
.then(resolvePromise)
|
||||||
|
.catch((err) => {
|
||||||
|
console.warn(
|
||||||
|
"Eager WASM compilation failed or not supported in this environment:",
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
resolvePromise(null);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolvePromise(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,24 @@ import posthog from "posthog-js";
|
|||||||
import { PostHogProvider } from "@posthog/react";
|
import { PostHogProvider } from "@posthog/react";
|
||||||
import { BASE_PATH } from "@app/constants/app";
|
import { BASE_PATH } from "@app/constants/app";
|
||||||
|
|
||||||
|
import { startEagerWasmCompilation } from "@app/services/wasmPrecompiler";
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const scheduleCompilation = () => {
|
||||||
|
if (typeof requestIdleCallback === "function") {
|
||||||
|
requestIdleCallback(() => startEagerWasmCompilation(), { timeout: 2000 });
|
||||||
|
} else {
|
||||||
|
setTimeout(startEagerWasmCompilation, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === "complete") {
|
||||||
|
scheduleCompilation();
|
||||||
|
} else {
|
||||||
|
window.addEventListener("load", scheduleCompilation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
posthog.init(import.meta.env.VITE_PUBLIC_POSTHOG_KEY, {
|
||||||
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
api_host: import.meta.env.VITE_PUBLIC_POSTHOG_HOST,
|
||||||
defaults: "2025-05-24",
|
defaults: "2025-05-24",
|
||||||
|
|||||||
@@ -2,6 +2,76 @@ import { defineConfig, loadEnv, type PluginOption } from "vite";
|
|||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { viteStaticCopy } from "vite-plugin-static-copy";
|
import { viteStaticCopy } from "vite-plugin-static-copy";
|
||||||
|
import { compression, defineAlgorithm } from "vite-plugin-compression2";
|
||||||
|
import { constants, gzip, brotliCompress } from "node:zlib";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const gzipPromise = promisify(gzip);
|
||||||
|
const brotliPromise = promisify(brotliCompress);
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
function compressStaticCopyPlugin(): PluginOption {
|
||||||
|
return {
|
||||||
|
name: "compress-static-copy",
|
||||||
|
apply: "build" as const,
|
||||||
|
async closeBundle() {
|
||||||
|
const distDir = path.resolve(__dirname, "dist");
|
||||||
|
const targets = ["pdfium", "vendor", "pdfjs"];
|
||||||
|
|
||||||
|
const excludedExtensions = [
|
||||||
|
".gz",
|
||||||
|
".br",
|
||||||
|
".png",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".gif",
|
||||||
|
".webp",
|
||||||
|
".woff",
|
||||||
|
".woff2",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function walkAndCompress(dirOrFile: string) {
|
||||||
|
let stat;
|
||||||
|
try {
|
||||||
|
stat = await fs.stat(dirOrFile);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat.isFile()) {
|
||||||
|
const ext = path.extname(dirOrFile).toLowerCase();
|
||||||
|
if (stat.size >= 1024 && !excludedExtensions.includes(ext)) {
|
||||||
|
const content = await fs.readFile(dirOrFile);
|
||||||
|
|
||||||
|
// Gzip (level 9)
|
||||||
|
const gzipped = await gzipPromise(content, { level: 9 });
|
||||||
|
await fs.writeFile(`${dirOrFile}.gz`, gzipped);
|
||||||
|
|
||||||
|
// Brotli (quality 11)
|
||||||
|
const brotlied = await brotliPromise(content, {
|
||||||
|
params: {
|
||||||
|
[constants.BROTLI_PARAM_QUALITY]: 11,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await fs.writeFile(`${dirOrFile}.br`, brotlied);
|
||||||
|
}
|
||||||
|
} else if (stat.isDirectory()) {
|
||||||
|
const files = await fs.readdir(dirOrFile);
|
||||||
|
for (const file of files) {
|
||||||
|
await walkAndCompress(path.join(dirOrFile, file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const target of targets) {
|
||||||
|
await walkAndCompress(path.join(distDir, target));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const VALID_MODES = [
|
const VALID_MODES = [
|
||||||
"core",
|
"core",
|
||||||
@@ -78,6 +148,18 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
tsconfigPaths({
|
tsconfigPaths({
|
||||||
projects: [tsconfigProject],
|
projects: [tsconfigProject],
|
||||||
}),
|
}),
|
||||||
|
compression({
|
||||||
|
threshold: 1024,
|
||||||
|
exclude: [/\.(png|jpg|jpeg|gif|webp|woff|woff2)$/],
|
||||||
|
algorithms: [
|
||||||
|
defineAlgorithm("gzip", { level: 9 }),
|
||||||
|
defineAlgorithm("brotliCompress", {
|
||||||
|
params: {
|
||||||
|
[constants.BROTLI_PARAM_QUALITY]: 11,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
// Set ANALYZE=true to emit dist/stats.html (treemap) alongside the
|
// Set ANALYZE=true to emit dist/stats.html (treemap) alongside the
|
||||||
// build; rollup-plugin-visualizer is ESM-only so we import dynamically.
|
// build; rollup-plugin-visualizer is ESM-only so we import dynamically.
|
||||||
...(process.env.ANALYZE === "true"
|
...(process.env.ANALYZE === "true"
|
||||||
@@ -119,6 +201,7 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
compressStaticCopyPlugin(),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: true,
|
host: true,
|
||||||
@@ -139,6 +222,20 @@ export default defineConfig(async ({ mode }) => {
|
|||||||
strictPort: true,
|
strictPort: true,
|
||||||
proxy: backendProxyConfig,
|
proxy: backendProxyConfig,
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
target: "esnext",
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
"vendor-react": ["react", "react-dom"],
|
||||||
|
"pdf-engine": ["@embedpdf/engines", "@embedpdf/pdfium"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
exclude: ["@embedpdf/pdfium"],
|
||||||
|
},
|
||||||
// base: "./" produces relative asset URLs which work when dist/ is served
|
// base: "./" produces relative asset URLs which work when dist/ is served
|
||||||
// at any path (e.g. Spring Boot bundling the frontend at /). But under
|
// at any path (e.g. Spring Boot bundling the frontend at /). But under
|
||||||
// `vite preview` for deep SPA routes (e.g. /workflow/sign/<token>), the
|
// `vite preview` for deep SPA routes (e.g. /workflow/sign/<token>), the
|
||||||
|
|||||||
Generated
+19
@@ -132,6 +132,7 @@
|
|||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.44.1",
|
"typescript-eslint": "^8.44.1",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.3.2",
|
||||||
|
"vite-plugin-compression2": "^2.5.3",
|
||||||
"vite-plugin-static-copy": "^3.1.4",
|
"vite-plugin-static-copy": "^3.1.4",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
@@ -16503,6 +16504,13 @@
|
|||||||
"bare-path": "^3.0.0"
|
"bare-path": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tar-mini": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-mini/-/tar-mini-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-+qfUHz700DWnRutdUsxRRVZ38G1Qr27OetwaMYTdg8hcPxf46U0S1Zf76dQMWRBmusOt2ZCK5kbIaiLkoGO7WQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tar-stream": {
|
"node_modules/tar-stream": {
|
||||||
"version": "3.1.8",
|
"version": "3.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
|
||||||
@@ -17412,6 +17420,17 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-compression2": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-compression2/-/vite-plugin-compression2-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-ItPgqQWkcnBbVw7is9OKwiZ8v6+ju9rYROl5Lp6QfQDEx/d55AwJQb/KLpsQqsU9HoigYBsZ8tK6I02UwJNvEw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@rollup/pluginutils": "^5.1.0",
|
||||||
|
"tar-mini": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite-plugin-static-copy": {
|
"node_modules/vite-plugin-static-copy": {
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.4.0.tgz",
|
||||||
|
|||||||
@@ -152,6 +152,7 @@
|
|||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.2",
|
||||||
"typescript-eslint": "^8.44.1",
|
"typescript-eslint": "^8.44.1",
|
||||||
"vite": "^7.3.2",
|
"vite": "^7.3.2",
|
||||||
|
"vite-plugin-compression2": "^2.5.3",
|
||||||
"vite-plugin-static-copy": "^3.1.4",
|
"vite-plugin-static-copy": "^3.1.4",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.2.4"
|
"vitest": "^3.2.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user