feat: polish envelopes (#2090)

## Description

The rest of the owl
This commit is contained in:
David Nguyen
2025-10-24 16:22:06 +11:00
committed by GitHub
parent 88836404d1
commit 03eb6af69a
141 changed files with 5171 additions and 2402 deletions

View File

@ -5,58 +5,74 @@ import {
RECIPIENT_COLOR_STYLES,
} from '@documenso/ui/lib/recipient-colors';
import {
DEFAULT_HANDWRITING_FONT_SIZE,
DEFAULT_STANDARD_FONT_SIZE,
MIN_HANDWRITING_FONT_SIZE,
} from '../../constants/pdf';
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../../constants/pdf';
import { AppError } from '../../errors/app-error';
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items';
import { calculateFieldPosition } from './field-renderer';
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
const minFontSize = MIN_HANDWRITING_FONT_SIZE;
const maxFontSize = DEFAULT_HANDWRITING_FONT_SIZE;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let SkiaImage: any = undefined;
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer } = options;
void (async () => {
if (typeof window === 'undefined') {
const mod = await import('skia-canvas');
SkiaImage = mod.Image;
}
})();
console.log({
pageWidth,
pageHeight,
});
const getImageDimensions = (img: HTMLImageElement, fieldWidth: number, fieldHeight: number) => {
let imageWidth = img.width;
let imageHeight = img.height;
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
imageWidth = imageWidth * scalingFactor;
imageHeight = imageHeight * scalingFactor;
const imageX = (fieldWidth - imageWidth) / 2;
const imageY = (fieldHeight - imageHeight) / 2;
return {
width: imageWidth,
height: imageHeight,
x: imageX,
y: imageY,
};
};
const createFieldSignature = (
field: FieldToRender,
options: RenderFieldElementOptions,
): Konva.Text | Konva.Image => {
const { pageWidth, pageHeight, mode = 'edit', translations } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const fontSize = field.fieldMeta?.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE;
const fieldText: Konva.Text =
pageLayer.findOne(`#${field.renderId}-text`) ||
new Konva.Text({
id: `${field.renderId}-text`,
name: 'field-text',
});
const fieldText = new Konva.Text({
id: `${field.renderId}-text`,
name: 'field-text',
});
const fieldTypeName = translations?.[field.type] || field.type;
// Calculate text positioning based on alignment
const textX = 0;
const textY = 0;
const textAlign = 'center';
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
let textFontSize = DEFAULT_STANDARD_FONT_SIZE;
const textPadding = 10;
let textToRender: string = field.type;
let textToRender: string = fieldTypeName;
const signature = field.signature;
// Handle edit mode.
if (mode === 'edit') {
textToRender = field.type; // Todo: Envelope - Need translations
textToRender = fieldTypeName;
}
// Handle sign mode.
if (mode === 'sign' || mode === 'export') {
textToRender = field.type; // Todo: Envelope - Need translations
textFontSize = DEFAULT_STANDARD_FONT_SIZE;
textVerticalAlign = 'middle';
textToRender = fieldTypeName;
if (field.inserted && !signature) {
throw new AppError('MISSING_SIGNATURE');
@ -65,20 +81,57 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
if (signature?.typedSignature) {
textToRender = signature.typedSignature;
}
if (signature?.signatureImageAsBase64) {
if (typeof window !== 'undefined') {
// Create a new HTML Image element
const img = new Image();
const image = new Konva.Image({
image: img,
x: 0,
y: 0,
width: fieldWidth,
height: fieldHeight,
});
img.onload = () => {
image.setAttrs({
...getImageDimensions(img, fieldWidth, fieldHeight),
});
};
img.src = signature.signatureImageAsBase64;
return image;
} else {
// Node.js with skia-canvas
if (!SkiaImage) {
throw new Error('Skia image not found');
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const img = new SkiaImage(signature?.signatureImageAsBase64) as unknown as HTMLImageElement;
const image = new Konva.Image({
image: img,
...getImageDimensions(img, fieldWidth, fieldHeight),
});
return image;
}
}
}
fieldText.setAttrs({
x: textX,
y: textY,
verticalAlign: textVerticalAlign,
wrap: 'word',
padding: textPadding,
verticalAlign: 'middle',
wrap: 'char',
text: textToRender,
fontSize: textFontSize,
fontFamily: 'Caveat, Inter', // Todo: Envelopes - Fix all fonts for sans
align: textAlign,
fontSize,
fontFamily: 'Caveat, sans-serif',
align: 'center',
width: fieldWidth,
height: fieldHeight,
} satisfies Partial<Konva.TextConfig>);
@ -96,78 +149,63 @@ export const renderSignatureFieldElement = (
const fieldGroup = upsertFieldGroup(field, options);
// ABOVE IS GENERIC, EXTRACT IT.
// Render the field background and text.
const fieldRect = upsertFieldRect(field, options);
const fieldText = upsertFieldText(field, options);
// Clear previous children and listeners to re-render fresh.
fieldGroup.removeChildren();
fieldGroup.off('transform');
// Assign elements to group and any listeners that should only be run on initialization.
if (isFirstRender) {
fieldGroup.add(fieldRect);
fieldGroup.add(fieldText);
pageLayer.add(fieldGroup);
// This is to keep the text inside the field at the same size
// when the field is resized. Without this the text would be stretched.
fieldGroup.on('transform', () => {
const groupScaleX = fieldGroup.scaleX();
const groupScaleY = fieldGroup.scaleY();
// Adjust text scale so it doesn't change while group is resized.
fieldText.scaleX(1 / groupScaleX);
fieldText.scaleY(1 / groupScaleY);
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
// // Update text group position and clipping
// fieldGroup.clipFunc(function (ctx) {
// ctx.rect(0, 0, rectWidth, rectHeight);
// });
// Update text dimensions
fieldText.width(rectWidth); // Account for padding
fieldText.height(rectHeight);
console.log({
rectWidth,
});
// Force Konva to recalculate text layout
// textInsideField.getTextHeight(); // This forces recalculation
fieldText.height(); // This forces recalculation
// fieldGroup.draw();
fieldGroup.getLayer()?.batchDraw();
});
// Reset the text after transform has ended.
fieldGroup.on('transformend', () => {
fieldText.scaleX(1);
fieldText.scaleY(1);
const rectWidth = fieldRect.width();
const rectHeight = fieldRect.height();
// // Update text group position and clipping
// fieldGroup.clipFunc(function (ctx) {
// ctx.rect(0, 0, rectWidth, rectHeight);
// });
// Update text dimensions
fieldText.width(rectWidth); // Account for padding
fieldText.height(rectHeight);
// Force Konva to recalculate text layout
// textInsideField.getTextHeight(); // This forces recalculation
fieldText.height(); // This forces recalculation
// fieldGroup.draw();
fieldGroup.getLayer()?.batchDraw();
});
}
// Render the field background and text.
const fieldRect = upsertFieldRect(field, options);
const fieldSignature = createFieldSignature(field, options);
fieldGroup.add(fieldRect);
fieldGroup.add(fieldSignature);
// This is to keep the text inside the field at the same size
// when the field is resized. Without this the text would be stretched.
fieldGroup.on('transform', () => {
const groupScaleX = fieldGroup.scaleX();
const groupScaleY = fieldGroup.scaleY();
// Adjust text scale so it doesn't change while group is resized.
fieldSignature.scaleX(1 / groupScaleX);
fieldSignature.scaleY(1 / groupScaleY);
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
// Update text dimensions
fieldSignature.width(rectWidth);
fieldSignature.height(rectHeight);
// Force Konva to recalculate text layout
fieldSignature.height();
fieldGroup.getLayer()?.batchDraw();
});
// Reset the text after transform has ended.
fieldGroup.on('transformend', () => {
fieldSignature.scaleX(1);
fieldSignature.scaleY(1);
const rectWidth = fieldRect.width();
const rectHeight = fieldRect.height();
// Update text dimensions
fieldSignature.width(rectWidth); // Account for padding
fieldSignature.height(rectHeight);
// Force Konva to recalculate text layout
fieldSignature.height();
fieldGroup.getLayer()?.batchDraw();
});
// Handle export mode.
if (mode === 'export') {
// Hide the rectangle.