Compare commits

...

5 Commits

Author SHA1 Message Date
Lucas Smith df678d7d69 v2.3.0 2025-12-17 22:10:47 +11:00
Lucas Smith 6739242554 fix: use cpu for skia-canvas rendering (#2334)
Seems there's a memory leak in gpu rendering with skia canvas
where contexts can live for much longer than expected escaping gc
cleanup

CPU rendering seems better albeit a bit slower.

Synthetic tests were ran with `--expose-gc` to simulate load over time.
2025-12-17 14:48:21 +11:00
Konrad a5e5eecf8b fix: mark links for translation (#2333) 2025-12-17 12:02:12 +11:00
Lucas Smith b0248c20eb v2.2.8 2025-12-16 16:04:07 +11:00
Lucas Smith f129968968 fix: ensure PDF form appearance streams have required /Subtype /Form entry (#2328)
When flattening PDF forms, some appearance streams lack the required
/Subtype /Form dictionary entry needed when used as XObjects. This
causes
corruption in Adobe Reader which fails to render these flattened fields.

Per PDF spec, Form XObject streams require:
- /Subtype /Form (required)
- /FormType 1 (optional)

The normalizeAppearanceStream function ensures these entries exist
before
adding appearance streams as XObjects to the page content stream.

Fixes rendering issues where flattened fields don't display in PDF
viewers.
2025-12-16 16:00:11 +11:00
10 changed files with 60 additions and 11 deletions
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState, useTransition } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Role, Subscription } from '@prisma/client';
import { Edit, Loader } from 'lucide-react';
import { Link } from 'react-router';
@@ -82,7 +83,7 @@ export const AdminDashboardUsersTable = ({
<Button className="w-24" asChild>
<Link to={`/admin/users/${row.original.id}`}>
<Edit className="-ml-1 mr-2 h-4 w-4" />
Edit
<Trans>Edit</Trans>
</Link>
</Button>
);
@@ -82,7 +82,9 @@ export const OrganisationGroupsDataTable = () => {
cell: ({ row }) => (
<div className="flex justify-end space-x-2">
<Button asChild variant="outline">
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>Manage</Link>
<Link to={`/o/${organisation.url}/settings/groups/${row.original.id}`}>
<Trans>Manage</Trans>
</Link>
</Button>
<OrganisationGroupDeleteDialog
@@ -120,7 +120,9 @@ export default function OrganisationSettingsTeamsPage() {
</div>
<Button asChild>
<Link to={`/o/${organisation.url}/settings`}>Manage Organisation</Link>
<Link to={`/o/${organisation.url}/settings`}>
<Trans>Manage Organisation</Trans>
</Link>
</Button>
</div>
@@ -115,7 +115,9 @@ export default function RejectedSigningPage({ loaderData }: Route.ComponentProps
{user && (
<Button className="mt-6" asChild>
<Link to={`/`}>Return Home</Link>
<Link to={`/`}>
<Trans>Return Home</Trans>
</Link>
</Button>
)}
</div>
@@ -98,7 +98,9 @@ export default function WaitingForTurnToSignPage({ loaderData }: Route.Component
</Button>
) : (
<Button variant="link" asChild>
<Link to="/">Return Home</Link>
<Link to="/">
<Trans>Return Home</Trans>
</Link>
</Button>
)}
</div>
+1 -1
View File
@@ -107,5 +107,5 @@
"vite-plugin-babel-macros": "^1.0.6",
"vite-tsconfig-paths": "^5.1.4"
},
"version": "2.2.7"
"version": "2.3.0"
}
+3 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@documenso/root",
"version": "2.2.7",
"version": "2.3.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@documenso/root",
"version": "2.2.7",
"version": "2.3.0",
"hasInstallScript": true,
"workspaces": [
"apps/*",
@@ -109,7 +109,7 @@
},
"apps/remix": {
"name": "@documenso/remix",
"version": "2.2.7",
"version": "2.3.0",
"dependencies": {
"@cantoo/pdf-lib": "^2.5.3",
"@documenso/api": "*",
+1 -1
View File
@@ -5,7 +5,7 @@
"apps/*",
"packages/*"
],
"version": "2.2.7",
"version": "2.3.0",
"scripts": {
"postinstall": "patch-package",
"build": "turbo run build",
+6 -1
View File
@@ -9,7 +9,10 @@ globalThis.Image = Image;
class SkiaCanvasFactory {
_createCanvas(width: number, height: number) {
return new Canvas(width, height);
const canvas = new Canvas(width, height);
canvas.gpu = false;
return canvas;
}
create(width: number, height: number) {
@@ -60,6 +63,8 @@ export const pdfToImages = async (pdfBytes: Uint8Array, options: PdfToImagesOpti
const viewport = page.getViewport({ scale });
const canvas = new Canvas(viewport.width, viewport.height);
canvas.gpu = false;
const canvasContext = canvas.getContext('2d');
await page.render({
@@ -4,8 +4,10 @@ import {
PDFDict,
type PDFDocument,
PDFName,
PDFNumber,
PDFRadioGroup,
PDFRef,
PDFStream,
drawObject,
popGraphicsState,
pushGraphicsState,
@@ -103,6 +105,36 @@ const getAppearanceRefForWidget = (field: PDFField, widget: PDFWidgetAnnotation)
}
};
/**
* Ensures that an appearance stream has the required dictionary entries to be
* used as a Form XObject. Some PDFs have appearance streams that are missing
* the /Subtype /Form entry, which causes Adobe Reader to fail to render them.
*
* Per PDF spec, a Form XObject stream requires:
* - /Subtype /Form (required)
* - /BBox (required, but should already exist for appearance streams)
* - /FormType 1 (optional, defaults to 1)
*/
const normalizeAppearanceStream = (document: PDFDocument, appearanceRef: PDFRef) => {
const appearanceStream = document.context.lookup(appearanceRef);
if (!(appearanceStream instanceof PDFStream)) {
return;
}
const dict = appearanceStream.dict;
// Ensure /Subtype /Form is set (required for XObject Form)
if (!dict.has(PDFName.of('Subtype'))) {
dict.set(PDFName.of('Subtype'), PDFName.of('Form'));
}
// Ensure /FormType is set (optional, but good practice)
if (!dict.has(PDFName.of('FormType'))) {
dict.set(PDFName.of('FormType'), PDFNumber.of(1));
}
};
const flattenWidget = (document: PDFDocument, field: PDFField, widget: PDFWidgetAnnotation) => {
try {
const page = getPageForWidget(document, widget);
@@ -117,6 +149,9 @@ const flattenWidget = (document: PDFDocument, field: PDFField, widget: PDFWidget
return;
}
// Ensure the appearance stream has required XObject Form dictionary entries
normalizeAppearanceStream(document, appearanceRef);
const xObjectKey = page.node.newXObject('FlatWidget', appearanceRef);
const rectangle = widget.getRectangle();