UNPKG

@pdfme/schemas

Version:

TypeScript base PDF generator and React base UI. Open source, developed by the community, and completely free to use under the MIT license!

282 lines (233 loc) 8.46 kB
import type * as CSS from 'csstype'; import { cmyk, degrees, degreesToRadians, rgb, Color } from '@pdfme/pdf-lib'; import { Schema, mm2pt, Mode, isHexValid, ColorType } from '@pdfme/common'; import { IconNode } from 'lucide'; import { getDynamicHeightsForTable as _getDynamicHeightsForTable } from './tables/dynamicTemplate.js'; export const convertForPdfLayoutProps = ({ schema, pageHeight, applyRotateTranslate = true, }: { schema: Schema; pageHeight: number; applyRotateTranslate?: boolean; }) => { const { width: mmWidth, height: mmHeight, position, rotate, opacity } = schema; const { x: mmX, y: mmY } = position; const rotateDegrees = rotate ? -rotate : 0; const width = mm2pt(mmWidth); const height = mm2pt(mmHeight); let x = mm2pt(mmX); // PDF coordinate system is from bottom left, UI is top left, so we need to flip the y axis let y = pageHeight - mm2pt(mmY) - height; if (rotateDegrees && applyRotateTranslate) { // If rotating we must pivot around the same point as the UI performs its rotation. // The UI performs rotation around the objects center point (the pivot point below), // pdflib rotates around the bottom left corner of the object. // We must therefore adjust the X and Y by rotating the bottom left corner by this pivot point. const pivotPoint = { x: x + width / 2, y: pageHeight - mm2pt(mmY) - height / 2 }; const rotatedPoint = rotatePoint({ x, y }, pivotPoint, rotateDegrees); x = rotatedPoint.x; y = rotatedPoint.y; } return { position: { x, y }, height: height, width: width, rotate: degrees(rotateDegrees), opacity, }; }; export const rotatePoint = ( point: { x: number; y: number }, pivot: { x: number; y: number }, angleDegrees: number, ): { x: number; y: number } => { const angleRadians = degreesToRadians(angleDegrees); const x = Math.cos(angleRadians) * (point.x - pivot.x) - Math.sin(angleRadians) * (point.y - pivot.y) + pivot.x; const y = Math.sin(angleRadians) * (point.x - pivot.x) + Math.cos(angleRadians) * (point.y - pivot.y) + pivot.y; return { x, y }; }; export const getDynamicHeightsForTable = _getDynamicHeightsForTable; // ---------------------------------------- export const addAlphaToHex = (hex: string, alphaPercentage: number) => { if (!/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/i.test(hex)) { throw new Error('Invalid HEX color code'); } const alphaValue = Math.round((alphaPercentage / 100) * 255); let alphaHex = alphaValue.toString(16); if (alphaHex.length === 1) alphaHex = '0' + alphaHex; return hex + alphaHex; }; export const isEditable = (mode: Mode, schema: Schema) => mode === 'designer' || (mode === 'form' && schema.readOnly !== true); const hex2rgb = (hex: string) => { if (hex.slice(0, 1) === '#') hex = hex.slice(1); if (hex.length === 3) hex = hex.slice(0, 1) + hex.slice(0, 1) + hex.slice(1, 2) + hex.slice(1, 2) + hex.slice(2, 3) + hex.slice(2, 3); return [hex.slice(0, 2), hex.slice(2, 4), hex.slice(4, 6)].map((str) => parseInt(str, 16)); }; export const hex2RgbColor = (hexString: string | undefined) => { if (hexString) { const isValid = isHexValid(hexString); if (!isValid) { throw new Error(`Invalid hex color value ${hexString}`); } const [r, g, b] = hex2rgb(hexString); return rgb(r / 255, g / 255, b / 255); } return undefined; }; const hex2CmykColor = (hexString: string | undefined) => { if (hexString) { const isValid = isHexValid(hexString); if (!isValid) { throw new Error(`Invalid hex color value ${hexString}`); } // Remove the # if it's present hexString = hexString.replace('#', ''); // Extract the hexadecimal color code and the opacity const hexColor = hexString.substring(0, 6); const opacityColor = hexString.substring(6, 8); const opacity = opacityColor ? parseInt(opacityColor, 16) / 255 : 1; // Convert the hex values to decimal let r = parseInt(hexColor.substring(0, 2), 16) / 255; let g = parseInt(hexColor.substring(2, 4), 16) / 255; let b = parseInt(hexColor.substring(4, 6), 16) / 255; // Apply the opacity r = r * opacity + (1 - opacity); g = g * opacity + (1 - opacity); b = b * opacity + (1 - opacity); // Calculate the CMYK values const k = 1 - Math.max(r, g, b); const c = r === 0 ? 0 : (1 - r - k) / (1 - k); const m = g === 0 ? 0 : (1 - g - k) / (1 - k); const y = b === 0 ? 0 : (1 - b - k) / (1 - k); return cmyk(c, m, y, k); } return undefined; }; export const hex2PrintingColor = (color?: string | Color, colorType?: ColorType) => { // if color is already CMYK, RGB or Grayscale, does not required to convert if (typeof color === 'object') return color; return colorType?.toLowerCase() == 'cmyk' ? hex2CmykColor(color) : hex2RgbColor(color); }; export const readFile = (input: File | FileList | null): Promise<string | ArrayBuffer> => new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = (e) => { if (e.target?.result) { resolve(e.target.result); } }; fileReader.onerror = () => { reject(new Error('[@pdfme/schemas] File reading failed')); }; let file: File | null = null; if (input instanceof FileList && input.length > 0) { file = input[0]; } else if (input instanceof File) { file = input; } if (file) { fileReader.readAsDataURL(file); } else { reject(new Error('[@pdfme/schemas] No files provided')); } }); export const createErrorElm = () => { const container = document.createElement('div'); const containerStyle: CSS.Properties = { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%', }; Object.assign(container.style, containerStyle); const span = document.createElement('span'); const spanStyle: CSS.Properties = { color: 'white', background: 'red', padding: '0.25rem', fontSize: '12pt', fontWeight: 'bold', borderRadius: '2px', fontFamily: "'Open Sans', sans-serif", }; Object.assign(span.style, spanStyle); span.textContent = 'ERROR'; container.appendChild(span); return container; }; export const createSvgStr = (icon: IconNode, attrs?: Record<string, string>): string => { // In lucide 0.475.0, the icon is an array of elements, not a single SVG element // We need to create an SVG wrapper and add the elements as children // Handle non-array input if (!Array.isArray(icon)) { return String(icon); } // Create default SVG attributes const svgAttrs = { xmlns: 'http://www.w3.org/2000/svg', width: '24', height: '24', viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', 'stroke-width': '2', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', ...(attrs || {}), }; // Format SVG attributes string const svgAttrString = Object.entries(svgAttrs) .map(([key, value]) => `${key}="${value}"`) .join(' '); // Helper function to process a single element const processElement = (element: unknown): string => { if (!Array.isArray(element)) { return String(element); } const [tag, attributes = {}, children = []] = element as [ unknown, Record<string, string>, unknown[], ]; const tagName = String(tag); // Format attributes string const attrString = Object.entries(attributes) .map(([key, value]) => `${key}="${value}"`) .join(' '); // Process children recursively let childrenString = ''; if (Array.isArray(children) && children.length > 0) { childrenString = children.map((child) => processElement(child)).join(''); } // Return properly formatted element string if (childrenString) { return `<${String(tagName)}${attrString ? ' ' + String(attrString) : ''}>${childrenString}</${String(tagName)}>`; } else { // Self-closing tag for empty children return `<${String(tagName)}${attrString ? ' ' + String(attrString) : ''}/>`; } }; // Process all elements and join them const elementsString = Array.isArray(icon) ? icon.map((element) => processElement(element)).join('') : processElement(icon); // Return the complete SVG string return `<svg ${svgAttrString}>${elementsString}</svg>`; };