PDF Designer Variablen Plugin

This commit is contained in:
Moritz Utcke
2024-02-23 13:25:57 +07:00
parent 74da57fc41
commit 977588f51b
7 changed files with 403 additions and 18 deletions

View File

@@ -41,6 +41,7 @@
"esbuild": "^0.18.17",
"express": "^4.18.2",
"flag-icons": "^6.9.2",
"fontkit": "^2.0.2",
"i18next": "^23.4.1",
"i18next-fs-backend": "^2.1.5",
"i18next-http-backend": "^2.2.1",
@@ -68,6 +69,7 @@
"@faker-js/faker": "^8.3.1",
"@tailwindcss/typography": "^0.5.10",
"@types/body-scroll-lock": "^3.1.2",
"@types/fontkit": "^2.0.6",
"@types/js-cookie": "^3.0.6",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.59.6",

View File

@@ -78,7 +78,12 @@
</li>
<li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-designer">
PDF Erstellen
PDF Designer
</a>
</li>
<li>
<a use:ripple={rippleOptions} class="button-tab" href="/dashboard/admin/pdf-viewer">
PDF Viewer
</a>
</li>
</ul>

View File

@@ -0,0 +1,26 @@
import { ALIGNMENT, VERTICAL_ALIGNMENT, DYNAMIC_FONT_SIZE_FIT } from './types';
export const DEFAULT_FONT_SIZE = 13;
export const ALIGN_LEFT = 'left' as ALIGNMENT;
export const ALIGN_CENTER = 'center' as ALIGNMENT;
export const ALIGN_RIGHT = 'right' as ALIGNMENT;
export const DEFAULT_ALIGNMENT = ALIGN_LEFT;
export const VERTICAL_ALIGN_TOP = 'top' as VERTICAL_ALIGNMENT;
export const VERTICAL_ALIGN_MIDDLE = 'middle' as VERTICAL_ALIGNMENT;
export const VERTICAL_ALIGN_BOTTOM = 'bottom' as VERTICAL_ALIGNMENT;
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 = '#000000';
export const PLACEHOLDER_FONT_COLOR = '#A0A0A0';
export const DYNAMIC_FIT_VERTICAL = 'vertical' as DYNAMIC_FONT_SIZE_FIT;
export const DYNAMIC_FIT_HORIZONTAL = 'horizontal' as DYNAMIC_FONT_SIZE_FIT;
export const DEFAULT_DYNAMIC_FIT = DYNAMIC_FIT_VERTICAL;
export const DEFAULT_DYNAMIC_MIN_FONT_SIZE = 4;
export const DEFAULT_DYNAMIC_MAX_FONT_SIZE = 72;
export const FONT_SIZE_ADJUSTMENT = 0.25;
export const DEFAULT_OPACITY = 1;
export const HEX_COLOR_PATTERN = '^#(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$';

View File

@@ -0,0 +1,311 @@
import {
ZOOM,
Plugin,
Schema,
PropPanel,
DEFAULT_FONT_NAME,
getFallbackFontName,
PropPanelSchema,
PropPanelWidgetProps,
} from "@pdfme/common";
import { image, text } from "@pdfme/schemas";
import type { TextSchema } from "@pdfme/schemas/dist/types/src/text/types";
import {
DEFAULT_FONT_SIZE,
DEFAULT_ALIGNMENT,
DEFAULT_VERTICAL_ALIGNMENT,
DEFAULT_CHARACTER_SPACING,
DEFAULT_LINE_HEIGHT,
VERTICAL_ALIGN_TOP,
VERTICAL_ALIGN_MIDDLE,
VERTICAL_ALIGN_BOTTOM,
DEFAULT_FONT_COLOR,
DYNAMIC_FIT_VERTICAL,
DYNAMIC_FIT_HORIZONTAL,
DEFAULT_DYNAMIC_FIT,
DEFAULT_DYNAMIC_MIN_FONT_SIZE,
DEFAULT_DYNAMIC_MAX_FONT_SIZE,
ALIGN_RIGHT,
ALIGN_CENTER,
DEFAULT_OPACITY,
HEX_COLOR_PATTERN,
} from "./constants";
import {
GebaeudeStammdaten,
Rechnungen,
VerbrauchsausweisWohnen,
} from "@ibcornelsen/database/client";
const UseDynamicFontSize = (props: PropPanelWidgetProps) => {
const { rootElement, changeSchemas, activeSchema, i18n } = props;
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = Boolean((activeSchema as any)?.dynamicFontSize);
checkbox.onchange = (e: any) => {
const val = e.target.checked
? {
min: DEFAULT_DYNAMIC_MIN_FONT_SIZE,
max: DEFAULT_DYNAMIC_MAX_FONT_SIZE,
fit: DEFAULT_DYNAMIC_FIT,
}
: undefined;
changeSchemas([
{ key: "dynamicFontSize", value: val, schemaId: activeSchema.id },
]);
};
const label = document.createElement("label");
label.innerText = i18n("schemas.text.dynamicFontSize") || "";
label.style.cssText = "display: flex; width: 100%;";
label.appendChild(checkbox);
rootElement.appendChild(label);
};
type AusweisIndex = keyof VerbrauchsausweisWohnen
| `gebaeude_stammdaten.${keyof GebaeudeStammdaten}`
| `rechnung.${keyof Rechnungen}`;
const sampleData: Partial<Record<AusweisIndex , string>> = {
"gebaeude_stammdaten.adresse": "Musterstraße 123",
"gebaeude_stammdaten.plz": "12345",
"gebaeude_stammdaten.ort": "Musterstadt",
"gebaeude_stammdaten.baujahr_gebaeude": "1990",
"gebaeude_stammdaten.baujahr_heizung": "2000",
}
const variableOptions: {
label: string;
value: AusweisIndex;
}[] = [
{
label: "Gebäude -> Adresse",
value: "gebaeude_stammdaten.adresse",
},
{
label: "Gebäude -> PLZ",
value: "gebaeude_stammdaten.plz",
},
{
label: "Gebaeude -> Ort",
value: "gebaeude_stammdaten.ort",
},
{
label: "Gebäude -> Baujahr Gebäude",
value: "gebaeude_stammdaten.baujahr_gebaeude",
},
{
label: "Gebäude -> Baujahr Heizung",
value: "gebaeude_stammdaten.baujahr_heizung",
},
{
label: "Gebäude -> Fläche",
value: "gebaeude_stammdaten.flaeche"
},
{
label: "Gebäude -> Einheiten",
value: "gebaeude_stammdaten.einheiten"
}
];
interface VariableSchema extends TextSchema {
variable: AusweisIndex | undefined
}
const propPanel: PropPanel<VariableSchema> = {
schema: ({ options, activeSchema, i18n }) => {
const font = options.font || {
[DEFAULT_FONT_NAME]: { data: "", fallback: true },
};
const fontNames = Object.keys(font);
const fallbackFontName = getFallbackFontName(font);
const enableDynamicFont = Boolean(
(activeSchema as any)?.dynamicFontSize
);
const textSchema: Record<string, PropPanelSchema> = {
variable: {
title: "Variable",
type: "string",
widget: "select",
props: { options: variableOptions },
span: 24,
},
fontName: {
title: i18n("schemas.text.fontName"),
type: "string",
widget: "select",
default: fallbackFontName,
props: {
options: fontNames.map((name) => ({
label: name,
value: name,
})),
},
span: 12,
},
fontSize: {
title: i18n("schemas.text.size"),
type: "number",
widget: "inputNumber",
span: 6,
disabled: enableDynamicFont,
},
characterSpacing: {
title: i18n("schemas.text.spacing"),
type: "number",
widget: "inputNumber",
span: 6,
},
alignment: {
title: i18n("schemas.text.textAlign"),
type: "string",
widget: "select",
props: {
options: [
{
label: i18n("schemas.left"),
value: DEFAULT_ALIGNMENT,
},
{ label: i18n("schemas.center"), value: ALIGN_CENTER },
{ label: i18n("schemas.right"), value: ALIGN_RIGHT },
],
},
span: 8,
},
verticalAlignment: {
title: i18n("schemas.text.verticalAlign"),
type: "string",
widget: "select",
props: {
options: [
{
label: i18n("schemas.top"),
value: VERTICAL_ALIGN_TOP,
},
{
label: i18n("schemas.middle"),
value: VERTICAL_ALIGN_MIDDLE,
},
{
label: i18n("schemas.bottom"),
value: VERTICAL_ALIGN_BOTTOM,
},
],
},
span: 8,
},
lineHeight: {
title: i18n("schemas.text.lineHeight"),
type: "number",
widget: "inputNumber",
props: {
step: 0.1,
},
span: 8,
},
useDynamicFontSize: {
type: "boolean",
widget: "UseDynamicFontSize",
bind: false,
span: 16,
},
dynamicFontSize: {
type: "object",
widget: "card",
column: 3,
properties: {
min: {
title: i18n("schemas.text.min"),
type: "number",
widget: "inputNumber",
hidden: !enableDynamicFont,
},
max: {
title: i18n("schemas.text.max"),
type: "number",
widget: "inputNumber",
hidden: !enableDynamicFont,
},
fit: {
title: i18n("schemas.text.fit"),
type: "string",
widget: "select",
hidden: !enableDynamicFont,
props: {
options: [
{
label: i18n("schemas.horizontal"),
value: DYNAMIC_FIT_HORIZONTAL,
},
{
label: i18n("schemas.vertical"),
value: DYNAMIC_FIT_VERTICAL,
},
],
},
},
},
},
fontColor: {
title: i18n("schemas.textColor"),
type: "string",
widget: "color",
rules: [
{
pattern: HEX_COLOR_PATTERN,
message: i18n("hexColorPrompt"),
},
],
},
backgroundColor: {
title: i18n("schemas.bgColor"),
type: "string",
widget: "color",
rules: [
{
pattern: HEX_COLOR_PATTERN,
message: i18n("hexColorPrompt"),
},
],
},
};
return textSchema;
},
widgets: { UseDynamicFontSize },
defaultValue: "Type Something...",
defaultSchema: {
type: "variable",
position: { x: 0, y: 0 },
width: 45,
height: 10,
rotate: 0,
alignment: DEFAULT_ALIGNMENT,
verticalAlignment: DEFAULT_VERTICAL_ALIGNMENT,
fontSize: DEFAULT_FONT_SIZE,
lineHeight: DEFAULT_LINE_HEIGHT,
characterSpacing: DEFAULT_CHARACTER_SPACING,
dynamicFontSize: undefined,
fontColor: DEFAULT_FONT_COLOR,
fontName: undefined,
backgroundColor: "",
opacity: DEFAULT_OPACITY,
variable: undefined
},
};
export const variable: Plugin<VariableSchema> = {
ui: (props) => {
if (props.schema.variable) {
props.value = sampleData[props.schema.variable] as string;
}
return text.ui(props);
},
pdf: (props) => {
props.value = props.schema.variable as string;
text.pdf(props);
},
propPanel,
};

View File

@@ -0,0 +1,29 @@
import type { Schema } from '@pdfme/common';
import type { Font as FontKitFont } from 'fontkit';
export type ALIGNMENT = 'left' | 'center' | 'right';
export type VERTICAL_ALIGNMENT = 'top' | 'middle' | 'bottom';
export type DYNAMIC_FONT_SIZE_FIT = 'horizontal' | 'vertical';
export type FontWidthCalcValues = {
font: FontKitFont;
fontSize: number;
characterSpacing: number;
boxWidthInPt: number;
};
export interface TextSchema extends Schema {
fontName?: string;
alignment: ALIGNMENT;
verticalAlignment: VERTICAL_ALIGNMENT;
fontSize: number;
lineHeight: number;
characterSpacing: number;
dynamicFontSize?: {
min: number;
max: number;
fit: DYNAMIC_FONT_SIZE_FIT;
};
fontColor: string;
backgroundColor: string;
}

View File

@@ -1,5 +1,7 @@
<script lang="ts">
import { Designer } from "@pdfme/ui";
import { image, text } from "@pdfme/schemas";
import { variable } from "../../lib/pdf/plugins/variables/index"
import { onMount } from "svelte";
import { Template, BLANK_PDF } from "@pdfme/common";
@@ -12,12 +14,14 @@
],
};
let plugins = { Bild: image, Text: text, Variablen: variable }
let container: HTMLDivElement;
let designer: Designer;
onMount(() => {
designer = new Designer({ domContainer: container, template });
designer = new Designer({ domContainer: container, template, plugins });
});
function loadBasePDF() {
@@ -31,7 +35,7 @@
const basePdf = e.target.result as string;
const newTemplate = { ...template, basePdf };
designer = new Designer({ domContainer: container, template: newTemplate });
designer = new Designer({ domContainer: container, template: newTemplate, plugins });
};
reader.readAsDataURL(file);
@@ -74,7 +78,7 @@
if (!e.target) return;
const template = JSON.parse(e.target.result as string) as Template;
designer = new Designer({ domContainer: container, template });
designer = new Designer({ domContainer: container, template, plugins });
};
reader.readAsText(file);
@@ -86,7 +90,7 @@
let loadTemplateInput: HTMLInputElement;
</script>
<header>
<header class="mb-4">
<button class="btn btn-secondary" on:click={() => loadTemplateInput.click()}>Change base PDF</button>
<button class="btn btn-secondary" on:click={addNewField}>Add new Field</button>
<button class="btn btn-secondary" on:click={exportTemplate}>Export</button>

View File

@@ -1,7 +1,10 @@
<script lang="ts">
import { variable } from "#lib/pdf/plugins/variables";
import { Benutzer, GebaeudeStammdaten, Rechnungen, VerbrauchsausweisWohnen } from "@ibcornelsen/database/client";
import { Template } from "@pdfme/common";
import { Viewer } from "@pdfme/ui";
import { Check } from "radix-svelte-icons";
import { image, text } from "@pdfme/schemas";
type AusweisData = VerbrauchsausweisWohnen & { benutzer: Benutzer, gebaeude_stammdaten: GebaeudeStammdaten, rechnungen: Rechnungen }
@@ -47,7 +50,8 @@
viewer = new Viewer({
domContainer: pdfViewerContainer,
template,
inputs: [convertAusweisData(pdfInputs)]
inputs: [convertAusweisData(pdfInputs)],
plugins: { text, image, variable}
})
};
@@ -64,10 +68,13 @@
return
}
if (viewer) viewer.destroy();
viewer = new Viewer({
domContainer: pdfViewerContainer,
template,
inputs: [convertAusweisData(pdfInputs)]
inputs: [convertAusweisData(pdfInputs)],
plugins: { text, image, variable}
})
}
@@ -76,20 +83,21 @@
<header>
<div>
<button class="btn btn-secondary" on:click={loadTemplate}>Load Template</button>
<button class="btn btn-secondary mb-4" on:click={loadTemplate}>Load Template</button>
</div>
</header>
<main class="grid grid-cols-[1fr_4fr]">
<main class="grid grid-cols-[1fr_4fr] gap-4">
<div>
<h1>Ausweise</h1>
{#each ausweise as ausweis}
<div>
<h2 class="text-black">{ausweis.gebaeude_stammdaten.adresse}</h2>
<button class="btn btn-secondary" on:click={() => {
changeInputs(ausweis)
}}>Diesen Ausweis als Basis nehmen</button>
</div>
{/each}
<div class="flex flex-col gap-4">
{#each ausweise as ausweis}
<div class="rounded-lg border p-2 flex flex-row items-center justify-between">
<h2 class="text-black">{ausweis.gebaeude_stammdaten.adresse}</h2>
<button class="btn btn-square btn-ghost p-1.5" on:click={() => {
changeInputs(ausweis)
}}><Check size={20}/></button>
</div>
{/each}
</div>
</div>
<div bind:this={pdfViewerContainer}></div>
</main>