Migrate PDF.js to DxPdfViewer in receiver signing page

This commit introduces a detailed migration plan to replace the
`PDF.js` rendering engine with `DxPdfViewer` in the receiver
signing experience (`EnvelopeReceiverPage.razor`). The migration
preserves the existing signing workflow and behavior while
introducing a new rendering layer.

Key changes:
- Replaced `PDF.js` rendering surface with `DxPdfViewer`.
- Preserved page-level orchestration for authorization, document
  loading, signature handling, and toolbar interactions.
- Introduced a custom overlay adapter for signature placeholders
  and applied signature overlays.
- Centralized page/zoom geometry acquisition for overlay alignment.
- Maintained signature navigation logic and thumbnail sidebar
  behavior.
- Updated `pdf-viewer.js` to separate engine-specific logic and
  adapt it for `DxPdfViewer`.
- Updated styles in `envelope-viewer.css` to support the new viewer.

This migration ensures that all existing workflow behaviors remain
functional, including navigation, zoom, signature placement, and
validation, while transitioning to the new rendering engine.
This commit is contained in:
2026-06-29 11:04:53 +02:00
parent 6ca03a50eb
commit a5e4f97397

View File

@@ -543,6 +543,538 @@ A useful way to think about the page is:
---
## Detailed Technical Migration Plan: `PDF.js` -> `DxPdfViewer`
This section records the implementation plan for migrating the active receiver signing experience from `PDF.js` to `DxPdfViewer` without losing any workflow behavior.
### 1. Core migration principle
This migration must be treated as:
- a **rendering engine replacement**
- not a signing workflow redesign
- not a simplification to a read-only viewer
- not a direct PDF stamping rewrite
The page-level orchestration in `EnvelopeReceiverPage.razor` remains the source of truth for:
- receiver authorization
- document loading
- signature placeholder loading
- cached signature loading/saving
- popup and validation flow
- signature locking rules
- reset/restart behavior
- toolbar intent
The rendering layer must be swapped while preserving those behaviors.
### 2. Current implementation findings from code
Based on the current files:
- `EnvelopeReceiverPage.razor`
- `wwwroot/js/pdf-viewer.js`
- `wwwroot/css/envelope-viewer.css`
- `EnvelopeReceiverPage_DxPdfViewer.razor`
the current page is tightly coupled to a custom `pdfViewer` JavaScript object.
That object currently owns all viewer-engine behaviors:
- PDF load and initialization
- current page state
- page rendering
- zoom rendering
- thumbnail rendering
- resize listener attachment
- signature button rendering
- applied signature overlay rendering
- signature navigation state
This means the migration cannot be done by replacing only the Razor markup. The current JavaScript object is effectively both:
- a `PDF.js` adapter
- and a receiver-signing overlay controller
Those responsibilities must be separated conceptually during migration.
### 3. Verified coordinate and viewport behavior from `PDF.js`
Inspection of the `PDF.js` source comments and implementation confirms the following useful facts:
1. `PageViewport` is created from:
- `viewBox`
- `scale`
- `rotation`
- optional offsets
2. `PDF.js` explicitly documents that `PageViewport` creates a transform that converts:
- **PDF coordinate system**
- into **normal canvas-like coordinates**
3. `PageViewportParameters.viewBox` is documented as:
- `xMin, yMin, xMax, yMax`
4. `getViewport(...)` is documented as returning an object containing:
- `width`
- `height`
- transforms required for rendering
5. `convertToViewportPoint(x, y)` is documented as converting:
- PDF coordinates
- into viewport coordinates
6. `convertToPdfPoint(x, y)` is documented as converting:
- viewport coordinates
- back into PDF coordinates
- specifically useful for converting canvas pixel locations into PDF coordinates
7. `rawDims` exposes unscaled page dimensions:
- `pageWidth`
- `pageHeight`
- `pageX`
- `pageY`
8. For rotation `0`, `PageViewport` flips the Y axis into canvas-style coordinates unless `dontFlip` is used.
### 4. Practical meaning of the current overlay math
The current `pdf-viewer.js` implementation does **not** call `convertToViewportPoint(...)` directly.
Instead, it uses this simplified mapping:
- `xPx = sig.x * scale`
- `yPx = sig.y * scale`
This works only because the current upstream data contract already prepares signature coordinates in a way that matches the displayed page layout used by this app.
From the wider workspace context, the receiver page data service already converts placeholder coordinates to `UnitOfLength.Point` before they reach the UI.
Therefore, the active overlay contract is effectively:
- incoming signature coordinates are already normalized for the receiver UI
- JavaScript currently assumes a top-left, page-relative, point-based overlay space
- visual pixel placement is obtained by multiplying by current display scale
This is extremely important for the migration:
- do **not** casually reinterpret existing signature coordinates as raw PDF bottom-left coordinates
- do **not** assume `DxPdfViewer` uses the same visible page coordinate origin
- preserve the effective contract already used by the receiver workflow
### 5. Why the migration is technically hard
Although both viewers display PDFs, they are fundamentally different integration surfaces.
#### Current `PDF.js` model
- low-level canvas rendering
- direct access to viewport scale
- direct control over single page rendering
- page DOM is owned by our code
- overlays are placed over known custom elements:
- `pdf-canvas`
- `pdf-text-layer`
- `pdf-signature-layer`
#### Target `DxPdfViewer` model
- component-driven render surface
- internal DOM is owned by DevExpress
- page layout lifecycle is controlled by the component
- zoom/page events may differ from `PDF.js`
- overlay host placement must be rediscovered or reintroduced
So the migration is not a `canvas -> component` rename.
It is a **viewer capability remapping** problem.
### 6. Technical target architecture
The target architecture should be treated as four cooperating layers.
#### 6.1 Blazor orchestration layer
Owned by `EnvelopeReceiverPage.razor`.
Remains responsible for:
- auth and redirect
- data loading
- signature popup state
- signature metadata validation
- cached signature reuse
- signed/unsigned counts
- restart behavior
- toolbar user intent
#### 6.2 Viewer host layer
Main display surface becomes `DxPdfViewer`.
This layer must provide equivalents for:
- document load
- current page tracking
- total pages tracking
- page navigation
- zoom control
- layout change detection
#### 6.3 Overlay adapter layer
Custom layer that translates page/zoom/layout information into overlay placement.
This layer must handle:
- signature placeholder buttons
- applied signature overlays
- page-relative visibility
- redraw after page/zoom/layout changes
#### 6.4 Thumbnail/navigation support layer
Custom or hybrid support for:
- left sidebar thumbnails
- active page highlight
- width persistence
- signature previous/next navigation
### 7. Mandatory first implementation step: extract the current viewer contract
Before changing behavior, the following current `pdfViewer` capabilities must be listed and then mapped one-by-one to the target implementation:
- `initialize`
- `getTotalPages`
- `getCurrentPage`
- `nextPage`
- `previousPage`
- `goToPage`
- `zoomIn`
- `zoomOut`
- `setScale`
- `getScale`
- `fitToWidth`
- `renderThumbnail`
- `attachResizeListeners`
- `startResize`
- `renderSignatureButtons`
- `applySignature`
- `getSignatureNavState`
- `goToNextSignature`
- `goToPreviousSignature`
- `dispose`
The migration should preserve this capability contract at the page level even if the internal engine changes.
### 8. Planned migration strategy
#### Phase 1 — Confirm the `DxPdfViewer` integration surface
Use `EnvelopeReceiverPage_DxPdfViewer.razor` as a reference and determine exactly what `DxPdfViewer` exposes for:
- document content binding
- page navigation
- current page reading
- page change events
- zoom setting
- zoom change events
- page layout readiness
- DOM surface that can host overlays
Key question:
- can we reliably obtain visible page geometry from the live `DxPdfViewer` DOM?
If yes, overlays can be positioned relative to the rendered page.
If no, a wrapper-based approximation or alternative integration is required.
#### Phase 2 — Replace the main render surface in `EnvelopeReceiverPage.razor`
The current custom `canvas + text layer + signature layer` block should be replaced with a `DxPdfViewer` host region while keeping:
- the same page route
- the same page state
- the same toolbar
- the same popup
- the same thumbnail sidebar shell
At this stage, only the main document display needs to work.
Signature overlays may temporarily be disabled while the host geometry is established.
#### Phase 3 — Introduce a dedicated overlay host above `DxPdfViewer`
The new viewer surface should be wrapped with a custom page container.
Planned structure:
- outer frame
- thumbnail sidebar
- splitter
- main viewer wrapper
- `DxPdfViewer`
- absolute-positioned custom overlay layer
The overlay layer must be independently controlled by our code and not depend on internal `PDF.js` DOM ids.
#### Phase 4 — Rebuild page/zoom geometry acquisition
Because the current implementation derives position using only `sig.x * scale`, the target implementation must determine:
- currently visible page rectangle
- page top-left origin inside the wrapper
- effective display scale relative to logical point coordinates
- current page number
At redraw time, the adapter must produce page-relative pixel coordinates for:
- placeholder buttons
- applied signature blocks
This should be centralized in one geometry function rather than scattered across multiple viewer actions.
#### Phase 5 — Move signature overlay state to a canonical model
The current JavaScript keeps runtime state in:
- `signatureButtons`
- `appliedSignatures`
- `appliedSignatureElements`
- `_allSignatures`
- `_lastViewedSignatureId`
This must remain stable across redraws.
Preferably:
- Blazor continues to own the authoritative business state
- JavaScript owns transient rendered DOM state only
If necessary, applied signature state should be re-sendable from .NET after viewer redraw.
That prevents losing visual signatures when page layout changes.
#### Phase 6 — Re-implement signature placeholder rendering on top of `DxPdfViewer`
The current implementation filters placeholders by:
- current page
- not already applied
That same behavior must remain.
Rendering rules to preserve:
- only placeholders for the active page are shown
- already applied placeholders are hidden as buttons
- button size grows/shrinks with zoom
- button click invokes `.NET` callback `OnSignatureButtonClick`
The existing scaling behavior uses `baseScale = 1.5` as the visual reference.
That visual convention should be preserved initially unless a new normalized sizing model is intentionally introduced.
#### Phase 7 — Re-implement applied signature rendering on top of `DxPdfViewer`
The current applied signature overlay includes:
- signature image
- horizontal separator line
- signer full name
- optional position
- place and date
The same visual block must be retained.
Required behavior:
- clicking a placeholder converts it to an applied signature overlay
- applied signature remains visible on the correct page
- applied signature rescales with zoom
- applied signature repositions on page/zoom changes
- applied signature is hidden when the user navigates to a different page
#### Phase 8 — Re-implement page navigation through a central page-change pipeline
The following actions must all converge into a single page navigation routine:
- previous page button
- next page button
- direct page number input
- thumbnail click
- signature previous/next navigation when target is on another page
That routine must do all of the following in order:
1. instruct viewer to change page
2. update canonical current page state
3. wait for rendered page surface readiness if needed
4. redraw placeholder overlays
5. refresh signature navigation counter state
#### Phase 9 — Re-implement zoom through a central zoom-change pipeline
The following actions must converge into one zoom update path:
- toolbar zoom in
- toolbar zoom out
- slider change
- fit-to-width
- viewer-native zoom events if the user triggers zoom through gestures
That routine must:
1. clamp to `50% - 300%`
2. update canonical zoom state
3. wait for layout/render completion if needed
4. redraw placeholder overlays
5. rescale applied signatures
The current implementation already treats redraw after zoom as mandatory. That must remain true.
#### Phase 10 — Preserve signature navigation independently from the viewer engine
Current navigation logic is driven by the global ordered signature list and not by PDF rendering internals alone.
This behavior must remain engine-independent:
- previous/next traverses the full signature list
- traversal wraps around at the edges
- if the target signature is on another page, the viewer jumps first
- after page jump, the target element is scrolled into view
- toolbar counters reflect total/signed/unsigned/current index
If `DxPdfViewer` changes scrolling mechanics, the `scrollToElement` / `scrollToButton` logic must be adapted, but the traversal rules must remain unchanged.
#### Phase 11 — Thumbnail sidebar should remain custom unless `DxPdfViewer` can match all current behavior
The current sidebar is not just decorative. It also provides:
- show/hide toggle
- click navigation
- active page highlight
- resizable width
- width persistence in `localStorage`
- progressive rendering strategy
Because of that, the safest plan is:
- keep the custom sidebar shell and resize behavior
- only replace the source of main document rendering
If `DxPdfViewer` cannot provide matching thumbnails directly, a hybrid solution is acceptable:
- main document rendered by `DxPdfViewer`
- thumbnails still generated through a separate custom preview pipeline
This still satisfies the requirement that `DxPdfViewer` becomes the main viewer technology.
#### Phase 12 — Keep signature capture popup unchanged unless integration forces a minimal adjustment
`receiver-signature.js` and the popup flow should remain mostly untouched.
Preserve exactly:
- draw tab
- text tab
- image tab
- full name required
- place required
- signature data required
- cached signature reuse
- signature change lock after signing starts
This area is low-risk and should not be refactored unnecessarily during the viewer migration.
### 9. File-by-file execution plan
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
Planned work:
- remove `PDF.js` CDN asset dependency from the active receiver page
- replace the custom canvas-based main surface with a `DxPdfViewer` host
- preserve toolbar, popup, auth, state and receiver-specific loading logic
- redirect viewer method calls through a new or adapted JS interop surface
- preserve current state fields wherever possible to minimize workflow regressions
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
Planned work:
- split engine-specific logic from workflow-specific logic
- remove dependency on direct `pdf-canvas` rendering for the main viewer path
- introduce `DxPdfViewer`-compatible geometry and overlay placement helpers
- preserve signature navigation logic semantics
- preserve overlay rendering semantics
- preserve resize integration for sidebar handling
This file will likely become a viewer adapter rather than a pure `PDF.js` implementation.
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
Planned work:
- keep page shell, toolbar, thumbnails, splitter and popup-related styles
- replace canvas/text-layer specific assumptions with viewer-wrapper styles
- introduce overlay host styles for the `DxPdfViewer` surface
- ensure z-index ordering still makes signature buttons clickable
- avoid CSS leaking into DevExpress internal DOM more than necessary
#### `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor`
Planned role:
- reference only
- used to understand document binding and minimal `DxPdfViewer` setup
- not the primary delivery target unless portions are borrowed into the main receiver page
### 10. Risks that must be explicitly tested during implementation
1. `DxPdfViewer` may not expose visible page geometry directly.
2. `DxPdfViewer` may render asynchronously in a way that requires delayed overlay redraw.
3. Built-in zoom/page state may not map 1:1 to current custom toolbar assumptions.
4. Sidebar width changes may require explicit overlay recalculation.
5. Applied signatures may visually drift if scaling math is not centralized.
6. Signature navigation may fail if target DOM nodes are created later than expected.
7. Built-in viewer scrolling may differ from the current wrapper scrolling model.
### 11. Acceptance checklist for the migration
The migration is only acceptable if all of the following still work in the active page:
- receiver authorization before document access
- document load for the authenticated receiver
- single active page viewing
- previous/next page navigation
- direct page input navigation
- thumbnail click navigation
- zoom in/out and slider zoom
- overlay alignment after zoom changes
- signature placeholder rendering on the correct page
- placeholder click applying signature overlay
- previous/next signature navigation across pages
- signed/unsigned counters updating correctly
- cached signature reuse
- signature popup validation
- signature change lock after signing starts
- restart signing behavior
- thumbnail width persistence
### 12. Final implementation guidance
The correct technical approach is:
- keep `EnvelopeReceiverPage.razor` as the receiver workflow orchestrator
- treat `DxPdfViewer` as the new main rendering engine
- rebuild overlay placement as a viewer-agnostic custom layer
- preserve the current receiver-specific state machine
- preserve signature navigation rules
- preserve thumbnail/sidebar interaction model where practical
In short:
- **replace the renderer**
- **preserve the workflow**
- **rebuild the overlay adapter**
- **do not redesign the signing experience**
---
## Files Most Likely Relevant For Future Work
### Main page