Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-11
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Context

Document Viewer v1.2.0 uses `react-pdf` to render PDFs via PDF.js. The `options` object passed to the `<Document>` component includes `cMapUrl` and `standardFontDataUrl` as relative paths. PDF.js passes these to the worker thread for font/cmap fetching. When the worker is loaded from a cross-origin URL (the default is unpkg CDN: `//unpkg.com/pdfjs-dist@.../pdf.worker.min.mjs`), the worker's `fetch()` cannot resolve relative URLs — it has no document origin to resolve against — causing a `TypeError: Failed to parse URL` and silent font load failure.

The customer's W9 PDF draws a checkmark using the ZapfDingbats standard font (glyph `0x34`) via a Form XObject. Without the font, PDF.js renders a blank rectangle.

## Goals / Non-Goals

**Goals:**

- Fix standard font and cmap fetching when the PDF.js worker is cross-origin
- No new XML properties, no API surface changes, no dependency updates

**Non-Goals:**

- Changing how the worker URL is configured
- Supporting self-hosted worker deployments (already supported via `pdfjsWorkerUrl` prop)

## Decisions

### Use an absolute `origin` to make resource URLs absolute

Compute a module-scope `origin` and prepend it to both `cMapUrl` and `standardFontDataUrl` at module evaluation time:

```ts
const origin: string = (window.mx?.appUrl ?? window.location.origin).replace(/\/$/, "");
```

**Rationale:** The worker needs absolute URLs. `window.mx.appUrl` is the canonical Mendix application URL and is correct even when the app is served from a non-origin base path or behind a reverse proxy; `window.location.origin` is the fallback when `mx.appUrl` is unset (e.g. tests). The trailing-slash strip prevents `//` in the joined resource URLs. Evaluated at module load (not per-render), so no React re-render cost.

**Alternative considered:** Use `window.location.origin` alone. Rejected — does not account for apps served under a base path that `mx.appUrl` encodes.

**Alternative considered:** Move `options` inside the component and use `useMemo`. Rejected — no reactive dependencies, module-scope evaluation is simpler and equivalent.

**Alternative considered:** Set `useWorkerFetch: false` to force main-thread font loading. Rejected — works around the symptom, not the cause; disabling worker fetch has broader performance implications.

### Supporting typings

`window.mx` is not in the widget's ambient types, and CSS imports (`react-pdf/dist/Page/*.css`) need a module declaration. Added `typings/global.d.ts` (declares `Window.mx?: { appUrl?: string }`) and `typings/modules.d.ts` (`declare module "*.css"`).

## Risks / Trade-offs

- Neither `window.mx?.appUrl` nor `window.location.origin` is guaranteed in SSR. Tests don't exercise PDF rendering and `mx.appUrl` falls back to `location.origin` — no impact. If server-side rendering is ever added, this will need to be guarded.
- If the Mendix app is served from a subpath (e.g. `/app/`), `mx.appUrl` already encodes it correctly; fonts resolve under the app's `/widgets/` path.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

PDFs containing glyphs from ZapfDingbats (a PDF standard font) — such as checkmarks generated by .NET PDF libraries — render blank in Document Viewer. The checkmark is drawn via a Form XObject using the ZapfDingbats font, which PDF.js must fetch from `standardFontDataUrl`. The URL was relative, causing the PDF.js worker (loaded from unpkg CDN, a different origin) to fail parsing it as an absolute URL.

## What Changes

- `standardFontDataUrl` and `cMapUrl` in `PDFViewer.tsx` are prefixed with an absolute `origin` so the worker can fetch font resources regardless of where it was loaded from.
- `origin` resolves to `window.mx.appUrl` (the Mendix app URL) when available, falling back to `window.location.origin`. A trailing slash is stripped to avoid double slashes in the resulting resource URLs.
- A trailing slash is added to `standard_fonts/` so the font directory URL is well-formed.
- Supporting typings added: `window.mx` declaration (`typings/global.d.ts`) and a CSS module declaration (`typings/modules.d.ts`).

## Capabilities

### New Capabilities

- `pdf-form-rendering`: Correct visual rendering of standard-font glyphs (ZapfDingbats checkmarks, Symbol characters) in PDFs rendered by the Document Viewer widget

### Modified Capabilities

<!-- none -->

## Impact

- **Files**:
- `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx`
- `packages/pluggableWidgets/document-viewer-web/typings/global.d.ts` (new — `window.mx` type)
- `packages/pluggableWidgets/document-viewer-web/typings/modules.d.ts` (new — `*.css` module type)
- **Behavior**: ZapfDingbats and other standard font glyphs now render correctly when PDF.js worker is loaded from a cross-origin URL (e.g. unpkg CDN)
- **No API or XML changes**
- **No dependency version changes**
- **Affected widget**: `@mendix/document-viewer-web` v1.2.0+
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## ADDED Requirements

### Requirement: Standard font glyphs render correctly in PDFs

The Document Viewer SHALL correctly render PDF glyphs that depend on PDF standard fonts (ZapfDingbats, Symbol, etc.) by fetching font resources using absolute URLs resolvable by the PDF.js worker.

#### Scenario: ZapfDingbats checkmark renders in cross-origin worker context

- **WHEN** a PDF contains a glyph drawn from the ZapfDingbats standard font (e.g. a checkmark drawn via a Form XObject)
- **AND** the PDF.js worker is loaded from a cross-origin URL (e.g. unpkg CDN)
- **THEN** the glyph SHALL render visibly on the canvas

#### Scenario: Font fetch uses absolute URL

- **WHEN** PDF.js requests a standard font file from the worker thread
- **THEN** the request URL SHALL be an absolute URL rooted at the Mendix application URL (`window.mx.appUrl`, falling back to `window.location.origin`, with any trailing slash stripped) — e.g. `https://example.com/widgets/.../FoxitDingbats.pfb`

#### Scenario: PDFs without standard fonts are unaffected

- **WHEN** a PDF contains no glyphs requiring standard font substitution
- **THEN** rendering SHALL be identical to previous behavior
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## 1. Implementation

- [x] 1.1 Prepend an absolute `origin` (`window.mx?.appUrl ?? window.location.origin`, trailing slash stripped) to `cMapUrl` and `standardFontDataUrl` in `packages/pluggableWidgets/document-viewer-web/src/components/PDFViewer.tsx`
- [x] 1.2 Add `typings/global.d.ts` (`window.mx`) and `typings/modules.d.ts` (`*.css`)

## 2. Verification

- [x] 2.1 Run unit tests: `cd packages/pluggableWidgets/document-viewer-web && pnpm run test`
- [x] 2.2 Build widget: `pnpm --filter @mendix/document-viewer-web run build`
- [x] 2.3 Verify customer W9 PDF shows Section 3.a checkbox as checked in Document Viewer
- [x] 2.4 Verify a PDF without AcroForms renders correctly (no regression)
4 changes: 4 additions & 0 deletions packages/pluggableWidgets/document-viewer-web/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- We changed the internal structure of the widget

### Fixed

- We fixed an issue where checkmarks and other special characters in PDFs were not displayed correctly.

## [1.2.0] - 2025-10-29

### Added
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { ChangeEvent, FormEvent, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import { If } from "@mendix/widget-plugin-component-kit/If";
import "react-pdf/dist/Page/AnnotationLayer.css";
import "react-pdf/dist/Page/TextLayer.css";
import { downloadFile } from "../utils/helpers";
import { useZoomScale } from "../utils/useZoomScale";
import BaseViewer from "./BaseViewer";
import { DocRendererElement, DocumentRendererProps, DocumentStatus } from "./documentRenderer";
import { If } from "@mendix/widget-plugin-component-kit/If";
import { downloadFile } from "../utils/helpers";
import { useZoomScale } from "../utils/useZoomScale";

const origin: string = (window.mx?.appUrl ?? window.location.origin).replace(/\/$/, "");
const options = {
cMapUrl: "/widgets/com/mendix/shared/pdfjs/cmaps/",
standardFontDataUrl: "/widgets/com/mendix/shared/pdfjs/standard_fonts"
cMapUrl: `${origin}/widgets/com/mendix/shared/pdfjs/cmaps/`,
standardFontDataUrl: `${origin}/widgets/com/mendix/shared/pdfjs/standard_fonts/`
};

const PDFViewer: DocRendererElement = (props: DocumentRendererProps) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare global {
interface Window {
mx?: { appUrl?: string };
}
}

export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "*.css";
Loading