mirror of
https://github.com/pdfme/pdfme.git
synced 2026-06-16 18:29:17 -04:00
vertical alignment support for text schema (#247)
This commit is contained in:
@@ -4,7 +4,9 @@ import type { Font as FontKitFont } from 'fontkit';
|
||||
import {
|
||||
calculateDynamicFontSize,
|
||||
checkFont,
|
||||
getBrowserVerticalFontAdjustments,
|
||||
getDefaultFont,
|
||||
getFontDescentInPt,
|
||||
getFontKitFont,
|
||||
getSplittedLines,
|
||||
} from '../src/font'
|
||||
@@ -460,3 +462,55 @@ describe('calculateDynamicFontSize with Custom font', () => {
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFontDescentInPt test', () => {
|
||||
test('it gets a descent size relative to the font size', () => {
|
||||
expect(getFontDescentInPt({ descent: -400, unitsPerEm: 1000 } as FontKitFont, 12)).toBe(
|
||||
-4.800000000000001
|
||||
);
|
||||
expect(getFontDescentInPt({ descent: 54, unitsPerEm: 1000 } as FontKitFont, 20)).toBe(1.08);
|
||||
expect(getFontDescentInPt({ descent: -512, unitsPerEm: 2048 } as FontKitFont, 54)).toBe(-13.5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrowserVerticalFontAdjustments test', () => {
|
||||
// Font with a base line-height of 1.349
|
||||
const font = { ascent: 1037, descent: -312, unitsPerEm: 1000 } as FontKitFont;
|
||||
|
||||
test('it gets a top adjustment when vertically aligning top', () => {
|
||||
expect(getBrowserVerticalFontAdjustments(font, 12, 1.0, 'top')).toEqual({
|
||||
topAdj: 2.791301999999999,
|
||||
bottomAdj: 0,
|
||||
});
|
||||
expect(getBrowserVerticalFontAdjustments(font, 36, 2.0, 'top')).toEqual({
|
||||
topAdj: 8.373906,
|
||||
bottomAdj: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('it gets a bottom adjustment when vertically aligning middle or bottom', () => {
|
||||
expect(getBrowserVerticalFontAdjustments(font, 12, 1.0, 'bottom')).toEqual({
|
||||
topAdj: 0,
|
||||
bottomAdj: 2.791302,
|
||||
});
|
||||
expect(getBrowserVerticalFontAdjustments(font, 12, 1.15, 'middle')).toEqual({
|
||||
topAdj: 0,
|
||||
bottomAdj: 1.5916020000000004,
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not get a bottom adjustment if the line height exceeds that of the font', () => {
|
||||
expect(getBrowserVerticalFontAdjustments(font, 12, 1.35, 'bottom')).toEqual({
|
||||
topAdj: 0,
|
||||
bottomAdj: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test('it does not get a bottom adjustment if the font base line-height is 1.0 or less', () => {
|
||||
const thisFont = { ascent: 900, descent: -50, unitsPerEm: 1000 } as FontKitFont;
|
||||
expect(getBrowserVerticalFontAdjustments(thisFont, 20, 1.0, 'bottom')).toEqual({
|
||||
topAdj: 0,
|
||||
bottomAdj: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { validateBarcodeInput, mm2pt, pt2mm } from '../src/helper';
|
||||
import { validateBarcodeInput, mm2pt, pt2mm, pt2px } from '../src/helper';
|
||||
import { PT_TO_PX_RATIO } from "../src";
|
||||
|
||||
describe('validateBarcodeInput test', () => {
|
||||
test('qrcode', () => {
|
||||
@@ -264,3 +265,13 @@ describe('pt2mm test', () => {
|
||||
expect(pt2mm(5322.98)).toEqual(1877.947344);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pt2px test', () => {
|
||||
it('converts points to pixels', () => {
|
||||
expect(pt2px(1)).toEqual(PT_TO_PX_RATIO);
|
||||
expect(pt2px(1)).toEqual(1.333);
|
||||
expect(pt2px(2.8346)).toEqual(3.7785218);
|
||||
expect(pt2px(10)).toEqual(13.33);
|
||||
expect(pt2px(5322.98)).toEqual(7095.532339999999);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
export const DEFAULT_FONT_NAME = 'Roboto';
|
||||
export const DEFAULT_FONT_SIZE = 13;
|
||||
export const DEFAULT_ALIGNMENT = 'left';
|
||||
export const VERTICAL_ALIGN_TOP = 'top';
|
||||
export const VERTICAL_ALIGN_MIDDLE = 'middle';
|
||||
export const VERTICAL_ALIGN_BOTTOM = 'bottom';
|
||||
export const DEFAULT_VERTICAL_ALIGNMENT = VERTICAL_ALIGN_TOP;
|
||||
export const DEFAULT_LINE_HEIGHT = 1;
|
||||
export const DEFAULT_CHARACTER_SPACING = 0;
|
||||
export const DEFAULT_FONT_COLOR = '#000';
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
FONT_SIZE_ADJUSTMENT,
|
||||
PT_TO_PX_RATIO,
|
||||
DEFAULT_DYNAMIC_FIT,
|
||||
DYNAMIC_FIT_HORIZONTAL,
|
||||
DYNAMIC_FIT_VERTICAL,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
} from './constants';
|
||||
import { mm2pt, pt2mm } from './helper';
|
||||
import { mm2pt, pt2mm, pt2px } from './helper';
|
||||
import { b64toUint8Array } from "."
|
||||
|
||||
export const getFallbackFontName = (font: Font) => {
|
||||
@@ -77,20 +77,50 @@ export const checkFont = (arg: { font: Font; template: Template }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getFontAlignmentValue = (fontKitFont: FontKitFont, fontSize: number) => {
|
||||
export const getBrowserVerticalFontAdjustments = (
|
||||
fontKitFont: FontKitFont,
|
||||
fontSize: number,
|
||||
lineHeight: number,
|
||||
verticalAlignment: string
|
||||
) => {
|
||||
const { ascent, descent, unitsPerEm } = fontKitFont;
|
||||
|
||||
const fontSizeInPx = fontSize * PT_TO_PX_RATIO;
|
||||
// Fonts have a designed line height that the browser renders when using `line-height: normal`
|
||||
const fontBaseLineHeight = (ascent - descent) / unitsPerEm;
|
||||
|
||||
// Convert ascent and descent to px values
|
||||
const ascentInPixels = (ascent / unitsPerEm) * fontSizeInPx;
|
||||
const descentInPixels = (descent / unitsPerEm) * fontSizeInPx;
|
||||
// For vertical alignment top
|
||||
// To achieve consistent positioning between browser and PDF, we apply the difference between
|
||||
// the font's actual height and the font size in pixels.
|
||||
// Browsers middle the font within this height, so we only need half of it to apply to the top.
|
||||
// This means the font renders a bit lower in the browser, but achieves PDF alignment
|
||||
const topAdjustment = (fontBaseLineHeight * fontSize - fontSize) / 2;
|
||||
|
||||
// Calculate the single line height in px
|
||||
const singleLineHeight = ((ascentInPixels + Math.abs(descentInPixels)) / fontSizeInPx);
|
||||
if (verticalAlignment === VERTICAL_ALIGN_TOP) {
|
||||
return { topAdj: pt2px(topAdjustment), bottomAdj: 0 };
|
||||
}
|
||||
|
||||
// Calculate the top margin/padding in px
|
||||
return ((singleLineHeight * fontSizeInPx) - fontSizeInPx) / 2
|
||||
// For vertical alignment bottom and middle
|
||||
// When browsers render text in a non-form element (such as a <div>), some of the text may be
|
||||
// lowered below and outside the containing element if the line height used is less than
|
||||
// the base line-height of the font.
|
||||
// This behaviour does not happen in a <textarea> though, so we need to adjust the positioning
|
||||
// for consistency between editing and viewing to stop text jumping up and down.
|
||||
// This portion of text is half of the difference between the base line height and the used
|
||||
// line height. If using the same or higher line-height than the base font, then line-height
|
||||
// takes over in the browser and this adjustment is not needed.
|
||||
// Unlike the top adjustment - this is only driven by browser behaviour, not PDF alignment.
|
||||
let bottomAdjustment = 0;
|
||||
if (lineHeight < fontBaseLineHeight) {
|
||||
bottomAdjustment = ((fontBaseLineHeight - lineHeight) * fontSize) / 2;
|
||||
}
|
||||
|
||||
return { topAdj: 0, bottomAdj: pt2px(bottomAdjustment) };
|
||||
};
|
||||
|
||||
export const getFontDescentInPt = (fontKitFont: FontKitFont, fontSize: number) => {
|
||||
const { descent, unitsPerEm } = fontKitFont;
|
||||
|
||||
return (descent / unitsPerEm) * fontSize;
|
||||
};
|
||||
|
||||
export const heightOfFontAtSize = (fontKitFont: FontKitFont, fontSize: number) => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
GenerateProps as GeneratePropsSchema,
|
||||
UIProps as UIPropsSchema,
|
||||
} from './schema';
|
||||
import { MM_TO_PT_RATIO, PT_TO_MM_RATIO } from './constants';
|
||||
import { MM_TO_PT_RATIO, PT_TO_MM_RATIO, PT_TO_PX_RATIO } from './constants';
|
||||
import { checkFont } from './font';
|
||||
|
||||
export const mm2pt = (mm: number): number => {
|
||||
@@ -21,6 +21,10 @@ export const pt2mm = (pt: number): number => {
|
||||
return pt * PT_TO_MM_RATIO;
|
||||
};
|
||||
|
||||
export const pt2px = (pt: number): number => {
|
||||
return pt * PT_TO_PX_RATIO;
|
||||
};
|
||||
|
||||
const blob2Base64Pdf = (blob: Blob) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
DEFAULT_FONT_NAME,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_ALIGNMENT,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_FONT_COLOR,
|
||||
@@ -53,6 +57,7 @@ import {
|
||||
checkDesignerProps,
|
||||
checkGenerateProps,
|
||||
mm2pt,
|
||||
pt2px,
|
||||
validateBarcodeInput,
|
||||
} from './helper.js';
|
||||
import {
|
||||
@@ -63,14 +68,19 @@ import {
|
||||
widthOfTextAtSize,
|
||||
checkFont,
|
||||
getFontKitFont,
|
||||
getFontAlignmentValue,
|
||||
getSplittedLines
|
||||
getBrowserVerticalFontAdjustments,
|
||||
getFontDescentInPt,
|
||||
getSplittedLines,
|
||||
} from './font.js';
|
||||
|
||||
export {
|
||||
DEFAULT_FONT_NAME,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_ALIGNMENT,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_FONT_COLOR,
|
||||
@@ -95,9 +105,11 @@ export {
|
||||
heightOfFontAtSize,
|
||||
widthOfTextAtSize,
|
||||
mm2pt,
|
||||
pt2px,
|
||||
checkFont,
|
||||
getBrowserVerticalFontAdjustments,
|
||||
getFontDescentInPt,
|
||||
getFontKitFont,
|
||||
getFontAlignmentValue,
|
||||
checkInputs,
|
||||
checkUIOptions,
|
||||
checkTemplate,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint dot-notation: "off"*/
|
||||
import { z } from 'zod';
|
||||
import { VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM } from "./constants";
|
||||
|
||||
const langs = ['en', 'ja', 'ar', 'th', 'pl'] as const;
|
||||
export const Lang = z.enum(langs);
|
||||
@@ -9,6 +10,13 @@ export const Size = z.object({ height: z.number(), width: z.number() });
|
||||
const alignments = ['left', 'center', 'right'] as const;
|
||||
export const Alignment = z.enum(alignments);
|
||||
|
||||
const verticalAlignments = [
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
] as const;
|
||||
export const VerticalAlignment = z.enum(verticalAlignments);
|
||||
|
||||
// prettier-ignore
|
||||
export const barcodeSchemaTypes = ['qrcode', 'japanpost', 'ean13', 'ean8', 'code39', 'code128', 'nw7', 'itf14', 'upca', 'upce', 'gs1datamatrix'] as const;
|
||||
const notBarcodeSchemaTypes = ['text', 'image'] as const;
|
||||
@@ -28,6 +36,7 @@ export const CommonSchema = z.object({
|
||||
export const TextSchema = CommonSchema.extend({
|
||||
type: z.literal(SchemaType.Enum.text),
|
||||
alignment: Alignment.optional(),
|
||||
verticalAlignment: VerticalAlignment.optional(),
|
||||
fontSize: z.number().optional(),
|
||||
fontName: z.string().optional(),
|
||||
fontColor: z.string().optional(),
|
||||
|
||||
Binary file not shown.
BIN
packages/generator/__tests__/assets/fonts/GreatVibes-Regular.ttf
Normal file
BIN
packages/generator/__tests__/assets/fonts/GreatVibes-Regular.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -42,6 +42,9 @@ const canvasPdf = require('./canvasPdf.json') as Template;
|
||||
const background = require('./background.json') as Template;
|
||||
const dynamicFontSizeHorizontal = require('./dynamicFontSizeHorizontal.json') as Template;
|
||||
const dynamicFontSizeVertical = require('./dynamicFontSizeVertical.json') as Template;
|
||||
const verticalAlignmentTop = require('./verticalAlignmentTop.json') as Template;
|
||||
const verticalAlignmentMiddle = require('./verticalAlignmentMiddle.json') as Template;
|
||||
const verticalAlignmentBottom = require('./verticalAlignmentBottom.json') as Template;
|
||||
|
||||
export default {
|
||||
test: {
|
||||
@@ -111,4 +114,7 @@ export default {
|
||||
background,
|
||||
dynamicFontSizeHorizontal,
|
||||
dynamicFontSizeVertical,
|
||||
verticalAlignmentTop,
|
||||
verticalAlignmentMiddle,
|
||||
verticalAlignmentBottom,
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -10,11 +10,17 @@ const PDFParser = require('pdf2json');
|
||||
const SauceHanSansJPData = readFileSync(path.join(__dirname, `/assets/fonts/SauceHanSansJP.ttf`));
|
||||
const SauceHanSerifJPData = readFileSync(path.join(__dirname, `/assets/fonts/SauceHanSerifJP.ttf`));
|
||||
const NotoSerifJPRegularData = readFileSync(path.join(__dirname, `/assets/fonts/NotoSerifJP-Regular.otf`));
|
||||
const GloriaHallelujahRegularData = readFileSync(path.join(__dirname, `/assets/fonts/GloriaHallelujah-Regular.ttf`));
|
||||
const GreatVibesRegularData = readFileSync(path.join(__dirname, `/assets/fonts/GreatVibes-Regular.ttf`));
|
||||
const JuliusSansOneRegularData = readFileSync(path.join(__dirname, `/assets/fonts/JuliusSansOne-Regular.ttf`));
|
||||
|
||||
const getFont = (): Font => ({
|
||||
SauceHanSansJP: { fallback: true, data: SauceHanSansJPData },
|
||||
SauceHanSerifJP: { data: SauceHanSerifJPData },
|
||||
'NotoSerifJP-Regular': { data: NotoSerifJPRegularData },
|
||||
'GloriaHallelujah-Regular': { data: GloriaHallelujahRegularData },
|
||||
'GreatVibes-Regular': { data: GreatVibesRegularData },
|
||||
'JuliusSansOne-Regular': { data: JuliusSansOneRegularData },
|
||||
});
|
||||
|
||||
const getPdf = (pdfFilePath: string) => {
|
||||
|
||||
@@ -25,13 +25,18 @@ import {
|
||||
BasePdf,
|
||||
BarCodeType,
|
||||
Alignment,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_ALIGNMENT,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_FONT_COLOR,
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
calculateDynamicFontSize,
|
||||
heightOfFontAtSize,
|
||||
getFontDescentInPt,
|
||||
getFontKitFont,
|
||||
getSplittedLines,
|
||||
mm2pt,
|
||||
@@ -168,13 +173,14 @@ const hex2RgbColor = (hexString: string | undefined) => {
|
||||
};
|
||||
|
||||
const getFontProp = async ({ input, font, schema }: { input: string, font: Font, schema: TextSchema }) => {
|
||||
const size = schema.dynamicFontSize ? await calculateDynamicFontSize({ textSchema: schema, font, input }) : schema.fontSize ?? DEFAULT_FONT_SIZE;
|
||||
const fontSize = schema.dynamicFontSize ? await calculateDynamicFontSize({ textSchema: schema, font, input }) : schema.fontSize ?? DEFAULT_FONT_SIZE;
|
||||
const color = hex2RgbColor(schema.fontColor ?? DEFAULT_FONT_COLOR);
|
||||
const alignment = schema.alignment ?? DEFAULT_ALIGNMENT;
|
||||
const verticalAlignment = schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT;
|
||||
const lineHeight = schema.lineHeight ?? DEFAULT_LINE_HEIGHT;
|
||||
const characterSpacing = schema.characterSpacing ?? DEFAULT_CHARACTER_SPACING;
|
||||
|
||||
return { size, color, alignment, lineHeight, characterSpacing };
|
||||
return { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing };
|
||||
};
|
||||
|
||||
const calcX = (x: number, alignment: Alignment, boxWidth: number, textWidth: number) => {
|
||||
@@ -188,7 +194,7 @@ const calcX = (x: number, alignment: Alignment, boxWidth: number, textWidth: num
|
||||
return mm2pt(x) + addition;
|
||||
};
|
||||
|
||||
const calcY = (y: number, height: number, itemHeight: number) => height - mm2pt(y) - itemHeight;
|
||||
const calcY = (y: number, pageHeight: number, itemHeight: number) => pageHeight - mm2pt(y) - itemHeight;
|
||||
|
||||
const drawBackgroundColor = (arg: {
|
||||
templateSchema: TextSchema;
|
||||
@@ -233,50 +239,60 @@ const drawInputByTextSchema = async (arg: {
|
||||
drawBackgroundColor({ templateSchema, page, pageHeight });
|
||||
|
||||
const { width, height, rotate } = convertSchemaDimensionsToPt(templateSchema);
|
||||
const { size, color, alignment, lineHeight, characterSpacing } = await getFontProp({
|
||||
input,
|
||||
font,
|
||||
schema: templateSchema,
|
||||
});
|
||||
const { fontSize, color, alignment, verticalAlignment, lineHeight, characterSpacing } =
|
||||
await getFontProp({
|
||||
input,
|
||||
font,
|
||||
schema: templateSchema,
|
||||
});
|
||||
|
||||
page.pushOperators(setCharacterSpacing(characterSpacing));
|
||||
|
||||
let beforeLineOver = 0;
|
||||
const firstLineTextHeight = heightOfFontAtSize(fontKitFont, fontSize);
|
||||
const descent = getFontDescentInPt(fontKitFont, fontSize);
|
||||
const halfLineHeightAdjustment = lineHeight === 0 ? 0 : ((lineHeight - 1) * fontSize) / 2;
|
||||
|
||||
const fontWidthCalcValues: FontWidthCalcValues = {
|
||||
font: fontKitFont,
|
||||
fontSize: size,
|
||||
fontSize,
|
||||
characterSpacing,
|
||||
boxWidthInPt: width,
|
||||
};
|
||||
|
||||
input.split(/\r|\n|\r\n/g).forEach((inputLine, inputLineIndex) => {
|
||||
const splitLines = getSplittedLines(inputLine, fontWidthCalcValues);
|
||||
let lines: string[] = [];
|
||||
input.split(/\r|\n|\r\n/g).forEach((inputLine) => {
|
||||
lines = lines.concat(getSplittedLines(inputLine, fontWidthCalcValues));
|
||||
});
|
||||
|
||||
const drawLine = (line: string, lineIndex: number) => {
|
||||
const textWidth = widthOfTextAtSize(line, fontKitFont, size, characterSpacing);
|
||||
const textHeight = heightOfFontAtSize(fontKitFont, size);
|
||||
// Text lines are rendered from the bottom upwards, we need to adjust the position down
|
||||
let yOffset = 0;
|
||||
if (verticalAlignment === VERTICAL_ALIGN_TOP) {
|
||||
yOffset = firstLineTextHeight + halfLineHeightAdjustment;
|
||||
} else {
|
||||
const otherLinesHeight = lineHeight * fontSize * (lines.length - 1);
|
||||
|
||||
page.drawText(line, {
|
||||
x: calcX(templateSchema.position.x, alignment, width, textWidth),
|
||||
y:
|
||||
calcY(templateSchema.position.y, pageHeight, height) +
|
||||
height -
|
||||
textHeight -
|
||||
lineHeight * size * (inputLineIndex + lineIndex + beforeLineOver) -
|
||||
(lineHeight === 0 ? 0 : ((lineHeight - 1) * size) / 2),
|
||||
rotate,
|
||||
size,
|
||||
color,
|
||||
lineHeight: lineHeight * size,
|
||||
maxWidth: width,
|
||||
font: pdfFontValue,
|
||||
wordBreaks: [''],
|
||||
});
|
||||
if (splitLines.length === lineIndex + 1) beforeLineOver += lineIndex;
|
||||
};
|
||||
if (verticalAlignment === VERTICAL_ALIGN_BOTTOM) {
|
||||
yOffset = height - otherLinesHeight + descent - halfLineHeightAdjustment;
|
||||
} else if (verticalAlignment === VERTICAL_ALIGN_MIDDLE) {
|
||||
yOffset = (height - otherLinesHeight - firstLineTextHeight + descent) / 2 + firstLineTextHeight;
|
||||
}
|
||||
}
|
||||
|
||||
splitLines.forEach(drawLine);
|
||||
lines.forEach((line, rowIndex) => {
|
||||
const textWidth = widthOfTextAtSize(line, fontKitFont, fontSize, characterSpacing);
|
||||
const rowYOffset = lineHeight * fontSize * rowIndex;
|
||||
|
||||
page.drawText(line, {
|
||||
x: calcX(templateSchema.position.x, alignment, width, textWidth),
|
||||
y: calcY(templateSchema.position.y, pageHeight, yOffset) - rowYOffset,
|
||||
rotate,
|
||||
size: fontSize,
|
||||
color,
|
||||
lineHeight: lineHeight * fontSize,
|
||||
maxWidth: width,
|
||||
font: pdfFontValue,
|
||||
wordBreaks: [''],
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -370,10 +370,10 @@ exports[`Designer snapshot 1`] = `
|
||||
title="field1"
|
||||
>
|
||||
<div
|
||||
style="position: absolute; top: 0px; padding: 0px 0px 0px 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; margin-top: 0px; font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word;"
|
||||
style="position: absolute; top: 0px; padding: 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; display: flex; flex-direction: column; justify-content: flex-start;"
|
||||
>
|
||||
<div
|
||||
style="margin-top: 0px; padding-top: 3.436640624999999px;"
|
||||
style="font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word; margin-bottom: 0px; padding-top: 3.436640625px;"
|
||||
>
|
||||
<span
|
||||
style="letter-spacing: inherit;"
|
||||
|
||||
@@ -81,13 +81,17 @@ exports[`Preview(as Form) snapshot 1`] = `
|
||||
style="position: absolute; cursor: pointer; height: 56.692913386499995px; width: 377.95275591px; top: 75.590551182px; left: 75.590551182px; outline: 1px dashed #4af;"
|
||||
title="field1"
|
||||
>
|
||||
<textarea
|
||||
placeholder="bb"
|
||||
style="position: absolute; top: 0px; padding: 3.436640624999999px 0px 0px 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; margin-top: 0px; font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word;"
|
||||
tabindex="100"
|
||||
<div
|
||||
style="position: absolute; top: 0px; padding: 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; display: flex; flex-direction: column; justify-content: flex-start;"
|
||||
>
|
||||
field1
|
||||
</textarea>
|
||||
<textarea
|
||||
placeholder="bb"
|
||||
style="padding: 3.436640625px 0px 0px 0px; resize: none; outline: none; background: none; font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word;"
|
||||
tabindex="100"
|
||||
>
|
||||
field1
|
||||
</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -227,10 +231,10 @@ exports[`Preview(as Viewer) snapshot 1`] = `
|
||||
title="field1"
|
||||
>
|
||||
<div
|
||||
style="position: absolute; top: 0px; padding: 0px 0px 0px 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; margin-top: 0px; font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word;"
|
||||
style="position: absolute; top: 0px; padding: 0px; height: 56.692913386499995px; width: 377.95275591px; resize: none; display: flex; flex-direction: column; justify-content: flex-start;"
|
||||
>
|
||||
<div
|
||||
style="margin-top: 0px; padding-top: 3.436640624999999px;"
|
||||
style="font-family: inherit; color: rgb(0, 0, 0); font-size: 30pt; letter-spacing: 0pt; line-height: 1em; text-align: left; white-space: pre-wrap; word-break: break-word; margin-bottom: 0px; padding-top: 3.436640625px;"
|
||||
>
|
||||
<span
|
||||
style="letter-spacing: inherit;"
|
||||
|
||||
@@ -6,6 +6,10 @@ import {
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_FONT_COLOR,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
DYNAMIC_FIT_VERTICAL,
|
||||
DYNAMIC_FIT_HORIZONTAL,
|
||||
DEFAULT_DYNAMIC_FIT,
|
||||
@@ -154,6 +158,7 @@ const TextPropEditor = (
|
||||
) => {
|
||||
const { changeSchemas, activeSchema } = props;
|
||||
const alignments = ['left', 'center', 'right'];
|
||||
const verticalAlignments = [VERTICAL_ALIGN_TOP, VERTICAL_ALIGN_MIDDLE, VERTICAL_ALIGN_BOTTOM];
|
||||
const dynamicFits = [DYNAMIC_FIT_HORIZONTAL, DYNAMIC_FIT_VERTICAL];
|
||||
const font = useContext(FontContext);
|
||||
const fallbackFontName = getFallbackFontName(font);
|
||||
@@ -180,13 +185,22 @@ const TextPropEditor = (
|
||||
/>
|
||||
|
||||
<SelectSet
|
||||
label={'Alignment'}
|
||||
label={'Horizontal Align'}
|
||||
value={activeSchema.alignment ?? 'left'}
|
||||
options={alignments}
|
||||
onChange={(e) =>
|
||||
changeSchemas([{ key: 'alignment', value: e.target.value, schemaId: activeSchema.id }])
|
||||
}
|
||||
/>
|
||||
|
||||
<SelectSet
|
||||
label={'Vertical Align'}
|
||||
value={activeSchema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT}
|
||||
options={verticalAlignments}
|
||||
onChange={(e) => {
|
||||
changeSchemas([{ key: 'verticalAlignment', value: e.target.value, schemaId: activeSchema.id }]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
|
||||
@@ -2,18 +2,34 @@ import React, { useContext, forwardRef, Ref, useState, useEffect } from 'react';
|
||||
import {
|
||||
DEFAULT_FONT_SIZE,
|
||||
DEFAULT_ALIGNMENT,
|
||||
VERTICAL_ALIGN_TOP,
|
||||
VERTICAL_ALIGN_MIDDLE,
|
||||
VERTICAL_ALIGN_BOTTOM,
|
||||
DEFAULT_VERTICAL_ALIGNMENT,
|
||||
DEFAULT_LINE_HEIGHT,
|
||||
DEFAULT_CHARACTER_SPACING,
|
||||
DEFAULT_FONT_COLOR,
|
||||
TextSchema,
|
||||
calculateDynamicFontSize,
|
||||
getFontKitFont,
|
||||
getFontAlignmentValue,
|
||||
getBrowserVerticalFontAdjustments,
|
||||
} from '@pdfme/common';
|
||||
import { SchemaUIProps } from './SchemaUI';
|
||||
import { ZOOM } from '../../constants';
|
||||
import { FontContext } from '../../contexts';
|
||||
|
||||
const mapVerticalAlignToFlex = (verticalAlignmentValue: string | undefined) => {
|
||||
switch (verticalAlignmentValue) {
|
||||
case VERTICAL_ALIGN_TOP:
|
||||
return 'flex-start';
|
||||
case VERTICAL_ALIGN_MIDDLE:
|
||||
return 'center';
|
||||
case VERTICAL_ALIGN_BOTTOM:
|
||||
return 'flex-end';
|
||||
}
|
||||
return 'flex-start';
|
||||
};
|
||||
|
||||
type Props = SchemaUIProps & { schema: TextSchema };
|
||||
|
||||
const TextSchemaUI = (
|
||||
@@ -22,8 +38,8 @@ const TextSchemaUI = (
|
||||
) => {
|
||||
const font = useContext(FontContext);
|
||||
const [dynamicFontSize, setDynamicFontSize] = useState<number | undefined>(undefined);
|
||||
const [fontAlignmentValue, setFontAlignmentValue] = useState<number>(0);
|
||||
|
||||
const [topAdjustment, setTopAdjustment] = useState<number>(0);
|
||||
const [bottomAdjustment, setBottomAdjustment] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (schema.dynamicFontSize && schema.data) {
|
||||
@@ -50,10 +66,31 @@ const TextSchemaUI = (
|
||||
|
||||
useEffect(() => {
|
||||
getFontKitFont(schema, font).then(fontKitFont => {
|
||||
const fav = getFontAlignmentValue(fontKitFont, dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE);
|
||||
setFontAlignmentValue(fav);
|
||||
// Depending on vertical alignment, we need to move the top or bottom of the font to keep
|
||||
// it within it's defined box and align it with the generated pdf.
|
||||
const { topAdj, bottomAdj } = getBrowserVerticalFontAdjustments(
|
||||
fontKitFont,
|
||||
dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE,
|
||||
schema.lineHeight ?? DEFAULT_LINE_HEIGHT,
|
||||
schema.verticalAlignment ?? DEFAULT_VERTICAL_ALIGNMENT
|
||||
);
|
||||
setTopAdjustment(topAdj);
|
||||
setBottomAdjustment(bottomAdj);
|
||||
});
|
||||
|
||||
if (ref && 'current' in ref) {
|
||||
const textarea = ref.current;
|
||||
|
||||
if (textarea) {
|
||||
// Textareas cannot be vertically aligned, so we need to adjust the height of the textarea
|
||||
// to exactly fit the height of it's content, whilst aligned within it's container.
|
||||
// This gives the appearance of being vertically aligned.
|
||||
textarea.style.height = 'auto'; // Reset the height to auto to ensure we get the correct height.
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
}, [
|
||||
schema.data,
|
||||
schema.width,
|
||||
schema.height,
|
||||
schema.fontName,
|
||||
@@ -63,20 +100,36 @@ const TextSchemaUI = (
|
||||
schema.dynamicFontSize?.fit,
|
||||
schema.characterSpacing,
|
||||
schema.lineHeight,
|
||||
schema.verticalAlignment,
|
||||
font,
|
||||
dynamicFontSize
|
||||
dynamicFontSize,
|
||||
editable,
|
||||
]);
|
||||
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
padding: 0,
|
||||
height: fontAlignmentValue < 0 ? schema.height * ZOOM + Math.abs(fontAlignmentValue) : schema.height * ZOOM,
|
||||
height: schema.height * ZOOM,
|
||||
width: schema.width * ZOOM,
|
||||
resize: 'none',
|
||||
marginTop: fontAlignmentValue < 0 ? fontAlignmentValue : 0,
|
||||
paddingTop: fontAlignmentValue >= 0 ? fontAlignmentValue : 0,
|
||||
backgroundColor: schema.data && schema.backgroundColor ? schema.backgroundColor : 'rgb(242 244 255 / 75%)',
|
||||
border: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: mapVerticalAlignToFlex(schema.verticalAlignment),
|
||||
};
|
||||
|
||||
const textareaStyle: React.CSSProperties = {
|
||||
padding: 0,
|
||||
resize: 'none',
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
paddingTop: topAdjustment,
|
||||
background: 'none',
|
||||
};
|
||||
|
||||
const fontStyles: React.CSSProperties = {
|
||||
fontFamily: schema.fontName ? `'${schema.fontName}'` : 'inherit',
|
||||
color: schema.fontColor ? schema.fontColor : DEFAULT_FONT_COLOR,
|
||||
fontSize: `${dynamicFontSize ?? schema.fontSize ?? DEFAULT_FONT_SIZE}pt`,
|
||||
@@ -85,27 +138,32 @@ const TextSchemaUI = (
|
||||
textAlign: schema.alignment ?? DEFAULT_ALIGNMENT,
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
backgroundColor:
|
||||
schema.data && schema.backgroundColor ? schema.backgroundColor : 'rgb(242 244 255 / 75%)',
|
||||
border: 'none',
|
||||
};
|
||||
|
||||
return editable ? (
|
||||
<textarea
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
tabIndex={tabIndex}
|
||||
style={style}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onStopEditing}
|
||||
value={schema.data}
|
||||
></textarea>
|
||||
<div style={containerStyle}>
|
||||
<textarea
|
||||
ref={ref}
|
||||
placeholder={placeholder}
|
||||
tabIndex={tabIndex}
|
||||
style={{ ...textareaStyle, ...fontStyles }}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onBlur={onStopEditing}
|
||||
value={schema.data}
|
||||
></textarea>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ ...style, height: schema.height * ZOOM, marginTop: 0, paddingTop: 0 }}>
|
||||
<div style={{ marginTop: style.marginTop, paddingTop: style.paddingTop }}>
|
||||
<div style={containerStyle}>
|
||||
<div
|
||||
style={{
|
||||
...fontStyles,
|
||||
marginBottom: bottomAdjustment,
|
||||
paddingTop: topAdjustment,
|
||||
}}
|
||||
>
|
||||
{/* Set the letterSpacing of the last character to 0. */}
|
||||
{schema.data.split('').map((l, i) => (
|
||||
<span key={i} style={{ letterSpacing: String(schema.data).length === i + 1 ? 0 : 'inherit', }} >
|
||||
<span key={i} style={{ letterSpacing: String(schema.data).length === i + 1 ? 0 : 'inherit' }}>
|
||||
{l}
|
||||
</span>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user