diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 5c3ed280..4b8987bd 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -21,6 +21,7 @@ import type { Schema, SchemaForUI, Font, + ColorType, BasePdf, Template, GeneratorOptions, @@ -82,6 +83,7 @@ export type { Schema, SchemaForUI, Font, + ColorType, BasePdf, Template, GeneratorOptions, diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts index 83f9f537..32ce37a3 100644 --- a/packages/common/src/schema.ts +++ b/packages/common/src/schema.ts @@ -58,6 +58,8 @@ export const Dict = z.object({ }); export const Mode = z.enum(['viewer', 'form', 'designer']); +export const ColorType = z.enum(['rgb', 'cmyk']).optional(); + export const Size = z.object({ height: z.number(), width: z.number() }); export const Schema = z @@ -113,6 +115,7 @@ const CommonProps = z.object({ // -------------------generate------------------- export const GeneratorOptions = CommonOptions.extend({ + colorType: ColorType, author: z.string().optional(), creationDate: z.date().optional(), creator: z.string().optional(), diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 809c5eb9..3f495f54 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -18,6 +18,7 @@ import { UIProps, PreviewProps, DesignerProps, + ColorType, } from './schema.js'; export type PropPanelSchema = _PropPanelSchema; @@ -158,6 +159,7 @@ export type SchemaForUI = z.infer; * @property {boolean} [subset] - The default is true (use subset font). So if you don't want to use a subset font, please set it to false. */ export type Font = z.infer; +export type ColorType = z.infer; export type BasePdf = z.infer; export type Template = z.infer; export type GeneratorOptions = z.infer; diff --git a/packages/schemas/src/shapes/line.ts b/packages/schemas/src/shapes/line.ts index d3da4e1a..94ce1d4c 100644 --- a/packages/schemas/src/shapes/line.ts +++ b/packages/schemas/src/shapes/line.ts @@ -1,5 +1,5 @@ import type { Schema, Plugin, PDFRenderProps, UIRenderProps } from '@pdfme/common'; -import { rotatePoint, convertForPdfLayoutProps, hex2RgbColor } from '../utils.js'; +import { rotatePoint, convertForPdfLayoutProps, hex2PrintingColor } from '../utils.js'; import { HEX_COLOR_PATTERN } from '../constants.js'; const DEFAULT_LINE_COLOR = '#000000'; @@ -10,7 +10,8 @@ interface LineSchema extends Schema { const lineSchema: Plugin = { pdf: (arg: PDFRenderProps) => { - const { page, schema } = arg; + const { page, schema, options } = arg; + const { colorType } = options; const pageHeight = page.getHeight(); const { width, @@ -24,7 +25,7 @@ const lineSchema: Plugin = { start: rotatePoint({ x, y: y + height / 2 }, pivot, rotate.angle), end: rotatePoint({ x: x + width, y: y + height / 2 }, pivot, rotate.angle), thickness: height, - color: hex2RgbColor(schema.color ?? DEFAULT_LINE_COLOR), + color: hex2PrintingColor(schema.color ?? DEFAULT_LINE_COLOR, colorType), opacity: opacity, }); }, diff --git a/packages/schemas/src/shapes/rectAndEllipse.ts b/packages/schemas/src/shapes/rectAndEllipse.ts index a3262992..161363ca 100644 --- a/packages/schemas/src/shapes/rectAndEllipse.ts +++ b/packages/schemas/src/shapes/rectAndEllipse.ts @@ -1,6 +1,6 @@ import { Plugin, Schema, mm2pt } from '@pdfme/common'; import { HEX_COLOR_PATTERN } from '../constants.js'; -import { hex2RgbColor, convertForPdfLayoutProps } from '../utils.js'; +import { hex2PrintingColor, convertForPdfLayoutProps } from '../utils.js'; interface Shape extends Schema { type: 'ellipse' | 'rectangle'; @@ -27,7 +27,8 @@ const shape: Plugin = { rootElement.appendChild(div); }, pdf: (arg) => { - const { schema, page } = arg; + const { schema, page, options } = arg; + const { colorType } = options; const pageHeight = page.getHeight(); const cArg = { schema, pageHeight }; const { position, width, height, rotate, opacity } = convertForPdfLayoutProps(cArg); @@ -39,8 +40,8 @@ const shape: Plugin = { const drawOptions = { rotate, borderWidth, - borderColor: hex2RgbColor(schema.borderColor), - color: hex2RgbColor(schema.color), + borderColor: hex2PrintingColor(schema.borderColor, colorType), + color: hex2PrintingColor(schema.color, colorType), opacity, borderOpacity: opacity, }; diff --git a/packages/schemas/src/text/pdfRender.ts b/packages/schemas/src/text/pdfRender.ts index c60257d0..1782dc77 100644 --- a/packages/schemas/src/text/pdfRender.ts +++ b/packages/schemas/src/text/pdfRender.ts @@ -1,6 +1,13 @@ import { PDFFont, PDFDocument } from '@pdfme/pdf-lib'; import type { TextSchema, FontWidthCalcValues } from './types'; -import { PDFRenderProps, Font, getDefaultFont, getFallbackFontName, mm2pt } from '@pdfme/common'; +import { + PDFRenderProps, + Font, + getDefaultFont, + getFallbackFontName, + mm2pt, + ColorType, +} from '@pdfme/common'; import { VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, @@ -20,7 +27,7 @@ import { getSplittedLines, widthOfTextAtSize, } from './helper.js'; -import { convertForPdfLayoutProps, rotatePoint, hex2RgbColor } from '../utils.js'; +import { convertForPdfLayoutProps, rotatePoint, hex2PrintingColor } from '../utils.js'; const embedAndGetFontObj = async (arg: { pdfDoc: PDFDocument; @@ -57,17 +64,19 @@ const getFontProp = async ({ value, font, schema, + colorType, _cache, }: { value: string; font: Font; + colorType?: ColorType; schema: TextSchema; _cache: Map; }) => { const fontSize = schema.dynamicFontSize ? await calculateDynamicFontSize({ textSchema: schema, font, value, _cache }) : schema.fontSize ?? DEFAULT_FONT_SIZE; - const color = hex2RgbColor(schema.fontColor || DEFAULT_FONT_COLOR); + const color = hex2PrintingColor(schema.fontColor || DEFAULT_FONT_COLOR, colorType); return { alignment: schema.alignment ?? DEFAULT_ALIGNMENT, @@ -83,12 +92,12 @@ export const pdfRender = async (arg: PDFRenderProps) => { const { value, pdfDoc, pdfLib, page, options, schema, _cache } = arg; if (!value) return; - const { font = getDefaultFont() } = options; + const { font = getDefaultFont(), colorType } = options; const [pdfFontObj, fontKitFont, fontProp] = await Promise.all([ embedAndGetFontObj({ pdfDoc, font, _cache }), getFontKitFont(schema, font, _cache), - getFontProp({ value, font, schema, _cache }), + getFontProp({ value, font, schema, _cache, colorType }), ]); const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } = fontProp; @@ -108,7 +117,7 @@ export const pdfRender = async (arg: PDFRenderProps) => { } = convertForPdfLayoutProps({ schema, pageHeight, applyRotateTranslate: false }); if (schema.backgroundColor) { - const color = hex2RgbColor(schema.backgroundColor); + const color = hex2PrintingColor(schema.backgroundColor, colorType); page.drawRectangle({ x, y, width, height, rotate, color }); } diff --git a/packages/schemas/src/utils.ts b/packages/schemas/src/utils.ts index 94cb1e28..c5c1c548 100644 --- a/packages/schemas/src/utils.ts +++ b/packages/schemas/src/utils.ts @@ -1,6 +1,6 @@ import type * as CSS from 'csstype'; -import { degrees, degreesToRadians, rgb } from '@pdfme/pdf-lib'; -import { Schema, mm2pt, Mode, isHexValid } from '@pdfme/common'; +import { cmyk, degrees, degreesToRadians, rgb } from '@pdfme/pdf-lib'; +import { Schema, mm2pt, Mode, isHexValid, ColorType } from '@pdfme/common'; export const convertForPdfLayoutProps = ({ schema, @@ -103,6 +103,50 @@ export const hex2RgbColor = (hexString: string | undefined) => { return undefined; }; +export 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 = (hexString: string | undefined, colorType?: ColorType) => { + return colorType?.toLocaleLowerCase() == 'cmyk' + ? hex2CmykColor(hexString) + : hex2RgbColor(hexString); +}; + export const readFile = (input: File | FileList | null): Promise => new Promise((resolve, reject) => { const fileReader = new FileReader();