fix: include cmaps for pdf viewer (#2177)

We were previously omitting cmaps meaning that
when signing documents with certain UTF-8 characters or CJK characters
they would appear as outlined squares in the pdf viewer despite the
actual pdf looking as expected with the characters displaying correctly.
This commit is contained in:
Lucas Smith
2025-11-12 11:00:44 +11:00
committed by GitHub
parent 3d0e3c6e8e
commit 4ade408001
8 changed files with 138 additions and 33 deletions

View File

@ -2,12 +2,19 @@ import { lingui } from '@lingui/vite-plugin';
import { reactRouter } from '@react-router/dev/vite'; import { reactRouter } from '@react-router/dev/vite';
import autoprefixer from 'autoprefixer'; import autoprefixer from 'autoprefixer';
import serverAdapter from 'hono-react-router-adapter/vite'; import serverAdapter from 'hono-react-router-adapter/vite';
import { createRequire } from 'node:module';
import path from 'node:path'; import path from 'node:path';
import tailwindcss from 'tailwindcss'; import tailwindcss from 'tailwindcss';
import { defineConfig } from 'vite'; import { defineConfig, normalizePath } from 'vite';
import macrosPlugin from 'vite-plugin-babel-macros'; import macrosPlugin from 'vite-plugin-babel-macros';
import { viteStaticCopy } from 'vite-plugin-static-copy';
import tsconfigPaths from 'vite-tsconfig-paths'; import tsconfigPaths from 'vite-tsconfig-paths';
const require = createRequire(import.meta.url);
const pdfjsDistPath = path.dirname(require.resolve('pdfjs-dist/package.json'));
const cMapsDir = normalizePath(path.join(pdfjsDistPath, 'cmaps'));
/** /**
* Note: We load the env variables externally so we can have runtime enviroment variables * Note: We load the env variables externally so we can have runtime enviroment variables
* for docker. * for docker.
@ -25,6 +32,14 @@ export default defineConfig({
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [
viteStaticCopy({
targets: [
{
src: cMapsDir,
dest: 'static',
},
],
}),
reactRouter(), reactRouter(),
macrosPlugin(), macrosPlugin(),
lingui(), lingui(),

View File

@ -4,6 +4,7 @@
FROM node:22-alpine3.20 AS base FROM node:22-alpine3.20 AS base
RUN apk add --no-cache openssl RUN apk add --no-cache openssl
RUN apk add --no-cache font-freefont
########################### ###########################
@ -114,4 +115,4 @@ COPY --chown=nodejs:nodejs ./docker/start.sh /app/apps/remix/start.sh
WORKDIR /app/apps/remix WORKDIR /app/apps/remix
CMD ["sh", "start.sh"] CMD ["sh", "start.sh"]

96
package-lock.json generated
View File

@ -52,6 +52,7 @@
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.4.0",
"turbo": "^1.9.3", "turbo": "^1.9.3",
"vite": "^6.3.5", "vite": "^6.3.5",
"vite-plugin-static-copy": "^3.1.4",
"zod-openapi": "^4.2.4", "zod-openapi": "^4.2.4",
"zod-prisma-types": "3.3.5" "zod-prisma-types": "3.3.5"
}, },
@ -19112,10 +19113,13 @@
} }
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.4.4", "version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": { "peerDependencies": {
"picomatch": "^3 || ^4" "picomatch": "^3 || ^4"
}, },
@ -33479,13 +33483,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.13", "version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.4.4", "fdir": "^6.5.0",
"picomatch": "^4.0.2" "picomatch": "^4.0.3"
}, },
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@ -33495,9 +33499,9 @@
} }
}, },
"node_modules/tinyglobby/node_modules/picomatch": { "node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -35944,6 +35948,76 @@
"vite": ">=2" "vite": ">=2"
} }
}, },
"node_modules/vite-plugin-static-copy": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vite-plugin-static-copy/-/vite-plugin-static-copy-3.1.4.tgz",
"integrity": "sha512-iCmr4GSw4eSnaB+G8zc2f4dxSuDjbkjwpuBLLGvQYR9IW7rnDzftnUjOH5p4RYR+d4GsiBqXRvzuFhs5bnzVyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.6.0",
"p-map": "^7.0.3",
"picocolors": "^1.1.1",
"tinyglobby": "^0.2.15"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"peerDependencies": {
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/vite-plugin-static-copy/node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/vite-plugin-static-copy/node_modules/p-map": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz",
"integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/vite-plugin-static-copy/node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/vite-tsconfig-paths": { "node_modules/vite-tsconfig-paths": {
"version": "5.1.4", "version": "5.1.4",
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",

View File

@ -45,6 +45,12 @@
"@commitlint/config-conventional": "^17.7.0", "@commitlint/config-conventional": "^17.7.0",
"@lingui/cli": "^5.2.0", "@lingui/cli": "^5.2.0",
"@prisma/client": "^6.18.0", "@prisma/client": "^6.18.0",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^8.40.0", "eslint": "^8.40.0",
@ -59,18 +65,13 @@
"prisma-json-types-generator": "^3.6.2", "prisma-json-types-generator": "^3.6.2",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"superjson": "^2.2.5", "superjson": "^2.2.5",
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.4.0",
"turbo": "^1.9.3",
"vite": "^6.3.5",
"vite-plugin-static-copy": "^3.1.4",
"zod-openapi": "^4.2.4", "zod-openapi": "^4.2.4",
"@ts-rest/core": "^3.52.1", "zod-prisma-types": "3.3.5"
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"zod-prisma-types": "3.3.5",
"vite": "^6.3.5"
}, },
"name": "@documenso/root", "name": "@documenso/root",
"workspaces": [ "workspaces": [

View File

@ -1,5 +1,7 @@
import Konva from 'konva'; // sort-imports-ignore
import 'konva/skia-backend'; import 'konva/skia-backend';
import Konva from 'konva';
import path from 'node:path'; import path from 'node:path';
import type { Canvas } from 'skia-canvas'; import type { Canvas } from 'skia-canvas';
import { FontLibrary } from 'skia-canvas'; import { FontLibrary } from 'skia-canvas';
@ -21,13 +23,13 @@ export const insertFieldInPDFV2 = async ({
}: InsertFieldInPDFV2Options) => { }: InsertFieldInPDFV2Options) => {
const fontPath = path.join(process.cwd(), 'public/fonts'); const fontPath = path.join(process.cwd(), 'public/fonts');
FontLibrary.use([ FontLibrary.use({
path.join(fontPath, 'caveat.ttf'), ['Caveat']: [path.join(fontPath, 'caveat.ttf')],
path.join(fontPath, 'noto-sans.ttf'), ['Noto Sans']: [path.join(fontPath, 'noto-sans.ttf')],
path.join(fontPath, 'noto-sans-japanese.ttf'), ['Noto Sans Japanese']: [path.join(fontPath, 'noto-sans-japanese.ttf')],
path.join(fontPath, 'noto-sans-chinese.ttf'), ['Noto Sans Chinese']: [path.join(fontPath, 'noto-sans-chinese.ttf')],
path.join(fontPath, 'noto-sans-korean.ttf'), ['Noto Sans Korean']: [path.join(fontPath, 'noto-sans-korean.ttf')],
]); });
const stage = new Konva.Stage({ width: pageWidth, height: pageHeight }); const stage = new Konva.Stage({ width: pageWidth, height: pageHeight });
const layer = new Konva.Layer(); const layer = new Konva.Layer();

View File

@ -9,7 +9,7 @@ import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'
import { calculateFieldPosition } from './field-renderer'; import { calculateFieldPosition } from './field-renderer';
export const konvaTextFontFamily = export const konvaTextFontFamily =
'Noto Sans, Noto Sans Japanese, Noto Sans Chinese, Noto Sans Korean, sans-serif'; '"Noto Sans", "Noto Sans Japanese", "Noto Sans Chinese", "Noto Sans Korean", sans-serif';
export const konvaTextFill = 'black'; export const konvaTextFill = 'black';
export const upsertFieldGroup = ( export const upsertFieldGroup = (

View File

@ -9,6 +9,7 @@ import { type PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
@ -22,6 +23,10 @@ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url, import.meta.url,
).toString(); ).toString();
const pdfViewerOptions = {
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps`,
};
const PDFLoader = () => ( const PDFLoader = () => (
<> <>
<Loader className="text-documenso h-12 w-12 animate-spin" /> <Loader className="text-documenso h-12 w-12 animate-spin" />
@ -167,6 +172,7 @@ export const PdfViewerKonva = ({
</div> </div>
</div> </div>
} }
// options={pdfViewerOptions}
> >
{Array(numPages) {Array(numPages)
.fill(null) .fill(null)

View File

@ -8,9 +8,10 @@ import { base64 } from '@scure/base';
import { Loader } from 'lucide-react'; import { Loader } from 'lucide-react';
import { type PDFDocumentProxy } from 'pdfjs-dist'; import { type PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf'; import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
import 'react-pdf/dist/esm/Page/TextLayer.css';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
// import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
// import 'react-pdf/dist/esm/Page/TextLayer.css';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download'; import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
@ -27,6 +28,10 @@ pdfjs.GlobalWorkerOptions.workerSrc = new URL(
import.meta.url, import.meta.url,
).toString(); ).toString();
const pdfViewerOptions = {
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps`,
};
export type OnPDFViewerPageClick = (_event: { export type OnPDFViewerPageClick = (_event: {
pageNumber: number; pageNumber: number;
numPages: number; numPages: number;
@ -234,6 +239,7 @@ export const PDFViewer = ({
</div> </div>
</div> </div>
} }
// options={pdfViewerOptions}
> >
{Array(numPages) {Array(numPages)
.fill(null) .fill(null)