feature: generate pdf from actor

This commit is contained in:
Matthieu CAILLEAUX
2021-10-18 00:35:13 +02:00
parent f96372b236
commit 145207a1c5
14 changed files with 391 additions and 138 deletions

View File

@@ -2,6 +2,6 @@ export const i18n = () => (<any>game).i18n;
export const i18nLocalize = (id: string) => i18n().localize(id);
export const i18nFormat = (id: string, data?: any) => i18n().format(id, data);
export const TEXT_SIZE = 10;
export const LABEL_SIZE = 8;
export const TEXT_SIZE = 8;
export const LABEL_SIZE = 6;
export const MARGINS = { top: 10, left: 10, bottom: 10, right: 10 };

View File

@@ -20,7 +20,13 @@ export abstract class AbstractElement {
this.maxWidth = maxWidth;
}
public abstract prepareRender(doc: jsPDF, maxWidth?: number): jsPDF;
public abstract render(doc: jsPDF, maxWidth?: number): jsPDF;
public abstract getHeight(doc?: jsPDF): number;
public abstract getCheckNewPageHeight(doc?: jsPDF): number;
public abstract getElements(): AbstractElement[];
}

View File

@@ -11,6 +11,10 @@ export class Box extends AbstractElement {
this.h = h;
}
public prepareRender(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
doc.rect(this.x, this.y, this.w, this.h);
return doc;
@@ -19,4 +23,12 @@ export class Box extends AbstractElement {
public getHeight(_doc): number {
return this.h;
}
public getCheckNewPageHeight(doc?: jsPDF): number {
return this.getHeight(doc);
}
public getElements(): AbstractElement[] {
return [this];
}
}

View File

@@ -9,20 +9,24 @@ export class Column extends AbstractElement {
this.elements = elements;
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, _maxWidth?: number): jsPDF {
const elements = this.elements ?? [];
let currentY = this.y;
for (let i = 0; i < elements.length; i++) {
const element = elements[i];
element.x = this.x;
element.x = Math.max(element.x, this.x);
element.y = currentY;
element.render(doc);
element.prepareRender(doc);
currentY += element.getHeight(doc) + 2;
}
return doc;
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
public getHeight(doc): number {
return this.elements
.map((e) => e.getHeight(doc))
@@ -33,4 +37,18 @@ export class Column extends AbstractElement {
return p + c + 2;
});
}
public getCheckNewPageHeight(doc?: jsPDF): number {
return this.elements.length > 0
? this.elements[0].getCheckNewPageHeight(doc)
: 0;
}
public getElements(): AbstractElement[] {
const elements: AbstractElement[] = [];
for (const element of this.elements) {
elements.push(...element.getElements());
}
return elements;
}
}

View File

@@ -1,5 +1,6 @@
import { Box } from './box';
import jsPDF from 'jspdf';
import { AbstractElement } from './abstract-element';
export class Image extends Box {
public imageData: string;
@@ -19,4 +20,8 @@ export class Image extends Box {
});
return doc;
}
public getElements(): AbstractElement[] {
return [this];
}
}

View File

@@ -1,6 +1,7 @@
import jsPDF, { TextOptionsLight } from 'jspdf';
import { Text } from './text';
import { i18nLocalize, LABEL_SIZE, TEXT_SIZE } from '../constants';
import { AbstractElement } from './abstract-element';
export class LabelledText extends Text {
public label: string;
@@ -23,8 +24,12 @@ export class LabelledText extends Text {
this.updateMaxWidth(maxWidth);
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, maxWidth?: number): jsPDF {
this.updateMaxWidth(maxWidth);
return super.prepareRender(doc, maxWidth);
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
const yLabel = this.y + this.getHeightFromPx(doc, LABEL_SIZE);
const yText = yLabel + this.getHeightFromPx(doc, TEXT_SIZE) + 1;
doc
@@ -50,4 +55,12 @@ export class LabelledText extends Text {
public getHeight(doc): number {
return this.getHeightFromPx(doc, TEXT_SIZE + LABEL_SIZE) + 1;
}
public getCheckNewPageHeight(doc?: jsPDF): number {
return this.getHeight(doc);
}
public getElements(): AbstractElement[] {
return [this];
}
}

View File

@@ -69,7 +69,7 @@ export class LabelledValues extends Row {
}
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, maxWidth?: number): jsPDF {
const pageWidth = doc.internal.pageSize.width;
const rowWidth = pageWidth - this.x - MARGINS.right;
for (const column of this.elements) {
@@ -77,6 +77,10 @@ export class LabelledValues extends Row {
labelledValue.maxWidth = rowWidth / this.nbrOfCol;
}
}
return super.render(doc, maxWidth);
return super.prepareRender(doc, maxWidth);
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
}

View File

@@ -21,7 +21,7 @@ export class Row extends AbstractElement {
this.maxWidths = maxWidths ?? [];
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, maxWidth?: number): jsPDF {
const elements = this.elements ?? [];
let maxWidths = this.maxWidths ?? [];
let widthPercents = this.widthPercents ?? [];
@@ -50,12 +50,16 @@ export class Row extends AbstractElement {
const maxChildWidth = maxWidths[i] ?? percentWidth;
element.x = currentX;
element.y = this.y;
element.render(doc, maxChildWidth);
element.prepareRender(doc, maxChildWidth);
currentX += percentWidth;
}
return doc;
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
public getHeight(doc?: jsPDF): number {
let maxHeight = 0;
for (const element of this.elements) {
@@ -63,4 +67,20 @@ export class Row extends AbstractElement {
}
return maxHeight;
}
public getCheckNewPageHeight(doc?: jsPDF): number {
let maxHeight = 0;
for (const element of this.elements) {
maxHeight = Math.max(maxHeight, element.getCheckNewPageHeight(doc));
}
return maxHeight;
}
public getElements(): AbstractElement[] {
const elements: AbstractElement[] = [];
for (const element of this.elements) {
elements.push(...element.getElements());
}
return elements;
}
}

39
src/elements/separator.ts Normal file
View File

@@ -0,0 +1,39 @@
import { AbstractElement } from './abstract-element';
import jsPDF from 'jspdf';
import { MARGINS } from '../constants';
export class Separator extends AbstractElement {
constructor(x: number, y: number, maxWidth?: number | undefined) {
super(x, y, maxWidth);
}
public getHeight(_doc?: jsPDF): number {
return 0.5;
}
public getCheckNewPageHeight(doc?: jsPDF): number {
return this.getHeight(doc);
}
public prepareRender(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
const pageWidth = doc.internal.pageSize.width;
const maxPageWidth = pageWidth - MARGINS.left - MARGINS.right;
const finalWidth = Math.min(
maxWidth ?? this.maxWidth ?? maxPageWidth,
maxPageWidth
);
doc.setLineWidth(0.25);
doc.line(this.x, this.y, this.x + finalWidth, this.y);
return doc;
}
public getElements(): AbstractElement[] {
return [this];
}
}

View File

@@ -17,11 +17,18 @@ export class Text extends AbstractElement {
this.textOptions = textOptions;
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, maxWidth?: number): jsPDF {
this.updateMaxWidth(maxWidth);
let finalText = [i18nLocalize(this.text)];
return doc;
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
let finalText: string[] = [i18nLocalize(this.text)];
if (this.maxWidth != null) {
finalText = doc.splitTextToSize(finalText[0], maxWidth ?? 0);
finalText = doc.splitTextToSize(finalText[0], this.maxWidth ?? 0);
}
if (finalText.length > 1) {
finalText[0] = finalText[0].replace(/(.){3}$/, '...');
}
const yText = this.y + this.getHeightFromPx(doc, TEXT_SIZE);
doc
@@ -42,4 +49,12 @@ export class Text extends AbstractElement {
public getHeight(doc): number {
return this.getHeightFromPx(doc, TEXT_SIZE);
}
public getCheckNewPageHeight(doc?: jsPDF): number {
return this.getHeight(doc);
}
public getElements(): AbstractElement[] {
return [this];
}
}

View File

@@ -53,7 +53,7 @@ export class Texts extends Row {
}
}
public render(doc: jsPDF, maxWidth?: number): jsPDF {
public prepareRender(doc: jsPDF, maxWidth?: number): jsPDF {
const pageWidth = doc.internal.pageSize.width;
const rowWidth = pageWidth - this.x - MARGINS.right;
for (const column of this.elements) {
@@ -61,6 +61,10 @@ export class Texts extends Row {
labelledValue.maxWidth = rowWidth / this.nbrOfCol;
}
}
return super.render(doc, maxWidth);
return super.prepareRender(doc, maxWidth);
}
public render(doc: jsPDF, _maxWidth?: number): jsPDF {
return doc;
}
}

View File

@@ -9,6 +9,8 @@ import { ItemData } from '@league-of-foundry-developers/foundry-vtt-types/src/fo
import { LabelledValues } from './elements/labelled-values';
import { Text } from './elements/text';
import { Texts } from './elements/texts';
import { Column } from './elements/column';
import { Separator } from './elements/separator';
Hooks.on(
'renderActorSheetWfrp4eCharacter',
@@ -37,18 +39,10 @@ Hooks.on(
const labelledRowHeight =
Util.getHeightFromPx(docBuilder.doc, TEXT_SIZE + LABEL_SIZE) + 1;
const textRowHeight = Util.getHeightFromPx(docBuilder.doc, TEXT_SIZE);
const row2Y = labelledRowHeight + MARGINS.top + 2;
const row3Y = row2Y + labelledRowHeight + 2;
const row4Y = row3Y + labelledRowHeight + 2;
const row5Y = row4Y + labelledRowHeight + 2;
const row6Y = row5Y + labelledRowHeight + 2;
const row7Y = row6Y + labelledRowHeight + 2;
const row8Y = row7Y + textRowHeight + 2;
const skills = new LabelledValues(
0,
row8Y,
0,
actor.itemCategories.skill
.map((item) => {
return {
@@ -59,12 +53,9 @@ Hooks.on(
.sort((a, b) => a.label.localeCompare(b.label))
);
const row9Y = row8Y + skills.getHeight(docBuilder.doc) + 2;
const row10Y = row9Y + textRowHeight + 2;
const talents = new LabelledValues(
0,
row10Y,
0,
actor.itemCategories.talent
.map((item) => {
return {
@@ -79,12 +70,9 @@ Hooks.on(
1
);
const row11Y = row10Y + talents.getHeight(docBuilder.doc) + 2;
const row12Y = row11Y + textRowHeight + 2;
const traits = new Texts(
0,
row12Y,
0,
actor.itemCategories.trait
.map((item) => {
return item.name;
@@ -93,114 +81,201 @@ Hooks.on(
4
);
const weaponsMelee = new Texts(
0,
0,
actor.itemCategories.weapon
.filter((w) => w.isMelee)
.map((item) => {
return `${item.name} : ${item.data.data.damage.meleeValue} (${
item.mountDamage
}), ${item.OriginalQualities.concat(item.OriginalFlaws).join(
', '
)}`;
})
.sort((a, b) => a.localeCompare(b)),
1
);
const weaponsRanged = new Texts(
0,
0,
actor.itemCategories.weapon
.filter((w) => w.isRanged)
.map((item) => {
return `${item.name} : ${item.data.data.damage.rangedValue}, ${
item.data.data.range.value
}, ${item.OriginalQualities.concat(item.OriginalFlaws).join(', ')}`;
})
.sort((a, b) => a.localeCompare(b)),
1
);
const ammunitions = new Texts(
0,
0,
actor.itemCategories.ammunition
.map((item) => {
return `${item.data.data.quantity.value} ${item.name} : ${
item.data.data.damage.value.length > 0
? item.data.data.damage.value
: '+0'
}, (${item.data.data.range.value}), ${item.OriginalQualities.concat(
item.OriginalFlaws
).join(', ')}`;
})
.sort((a, b) => a.localeCompare(b)),
2
);
const imageWidth = 25;
const imageY = labelledRowHeight + MARGINS.top + 2;
const actorImageElement =
actorImageData != null
? new Image(0, row2Y, imageWidth, imageWidth, actorImageData)
: new Box(0, row2Y, imageWidth, imageWidth);
? new Image(0, imageY, imageWidth, imageWidth, actorImageData)
: new Box(0, imageY, imageWidth, imageWidth);
docBuilder.build([
new Row(0, 0, [
new LabelledText(0, 0, 'Name', `${actor.name}`),
new LabelledText(0, 0, 'Species', `${actorDetails?.species?.value}`),
new LabelledText(0, 0, 'Gender', `${actorDetails?.gender?.value}`),
]),
actorImageElement,
new Row(imageWidth + MARGINS.left + 1, row2Y, [
new LabelledText(0, 0, 'Class', `${careerDetail?.class?.value}`),
new LabelledText(
0,
0,
'Career Group',
`${careerDetail?.careergroup?.value}`
),
new LabelledText(0, 0, 'Career', `${currentCareer?.name}`),
new Column(0, 0, [
new Row(0, 0, [
new LabelledText(0, 0, 'Name', `${actor.name}`),
new LabelledText(
0,
0,
'Species',
`${actorDetails?.species?.value}`
),
new LabelledText(0, 0, 'Gender', `${actorDetails?.gender?.value}`),
]),
new Row(imageWidth + MARGINS.left + 1, 0, [
new LabelledText(0, 0, 'Class', `${careerDetail?.class?.value}`),
new LabelledText(
0,
0,
'Career Group',
`${careerDetail?.careergroup?.value}`
),
new LabelledText(0, 0, 'Career', `${currentCareer?.name}`),
]),
new Row(imageWidth + MARGINS.left + 1, 0, [
new LabelledText(0, 0, 'Status', `${actorDetails?.status?.value}`),
new LabelledText(0, 0, 'Age', `${actorDetails?.age?.value}`),
new LabelledText(0, 0, 'Height', `${actorDetails?.height?.value}`),
new LabelledText(0, 0, 'Weight', `${actorDetails?.weight?.value}`),
new LabelledText(
0,
0,
'Hair Colour',
`${actorDetails?.haircolour?.value}`
),
]),
new Row(imageWidth + MARGINS.left + 1, 0, [
new LabelledText(
0,
0,
'Eye Colour',
`${actorDetails?.eyecolour?.value}`
),
new LabelledText(
0,
0,
'Distinguishing Mark',
`${actorDetails?.distinguishingmark?.value}`
),
new LabelledText(
0,
0,
'Star Sign',
`${actorDetails?.starsign?.value}`
),
]),
new Row(0, 0, [
new LabelledText(
0,
0,
'CHARAbbrev.WS',
`${actorCharacs?.ws?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.BS',
`${actorCharacs?.bs?.value}`
),
new LabelledText(0, 0, 'CHARAbbrev.S', `${actorCharacs?.s?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.T', `${actorCharacs?.t?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.I', `${actorCharacs?.i?.value}`),
new LabelledText(
0,
0,
'CHARAbbrev.Ag',
`${actorCharacs?.ag?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.Dex',
`${actorCharacs?.dex?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.Int',
`${actorCharacs?.int?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.WP',
`${actorCharacs?.wp?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.Fel',
`${actorCharacs?.fel?.value}`
),
]),
new Row(0, 0, [
new LabelledText(0, 0, 'Move', `${actorDetails?.move?.value}`),
new LabelledText(0, 0, 'Walk', `${actorDetails?.move?.walk}`),
new LabelledText(0, 0, 'Run', `${actorDetails?.move?.run}`),
new LabelledText(0, 0, 'Fortune', `${actorStatus?.fortune?.value}`),
new LabelledText(0, 0, 'Fate', `${actorStatus?.fate?.value}`),
new LabelledText(0, 0, 'Resolve', `${actorStatus?.resolve?.value}`),
new LabelledText(
0,
0,
'Resilience',
`${actorStatus?.resilience?.value}`
),
new LabelledText(
0,
0,
'Wounds',
`${actorStatus?.wounds?.value}/${actorStatus?.wounds?.max}`
),
]),
new Separator(0, 0),
new Text(0, 0, 'Skills'),
skills,
new Separator(0, 0),
new Text(0, 0, 'Talents'),
talents,
new Separator(0, 0),
new Text(0, 0, 'Traits'),
traits,
new Separator(0, 0),
new Text(0, 0, 'SHEET.MeleeWeaponHeader'),
weaponsMelee,
new Separator(0, 0),
new Text(0, 0, 'SHEET.RangedWeaponHeader'),
weaponsRanged,
new Separator(0, 0),
new Text(0, 0, 'Ammunition'),
ammunitions,
]),
new Row(imageWidth + MARGINS.left + 1, row3Y, [
new LabelledText(0, 0, 'Status', `${actorDetails?.status?.value}`),
new LabelledText(0, 0, 'Age', `${actorDetails?.age?.value}`),
new LabelledText(0, 0, 'Height', `${actorDetails?.height?.value}`),
new LabelledText(0, 0, 'Weight', `${actorDetails?.weight?.value}`),
new LabelledText(
0,
0,
'Hair Colour',
`${actorDetails?.haircolour?.value}`
),
]),
new Row(imageWidth + MARGINS.left + 1, row4Y, [
new LabelledText(
0,
0,
'Eye Colour',
`${actorDetails?.eyecolour?.value}`
),
new LabelledText(
0,
0,
'Distinguishing Mark',
`${actorDetails?.distinguishingmark?.value}`
),
new LabelledText(
0,
0,
'Star Sign',
`${actorDetails?.starsign?.value}`
),
]),
new Row(0, row5Y, [
new LabelledText(0, 0, 'CHARAbbrev.WS', `${actorCharacs?.ws?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.BS', `${actorCharacs?.bs?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.S', `${actorCharacs?.s?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.T', `${actorCharacs?.t?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.I', `${actorCharacs?.i?.value}`),
new LabelledText(0, 0, 'CHARAbbrev.Ag', `${actorCharacs?.ag?.value}`),
new LabelledText(
0,
0,
'CHARAbbrev.Dex',
`${actorCharacs?.dex?.value}`
),
new LabelledText(
0,
0,
'CHARAbbrev.Int',
`${actorCharacs?.int?.value}`
),
new LabelledText(0, 0, 'CHARAbbrev.WP', `${actorCharacs?.wp?.value}`),
new LabelledText(
0,
0,
'CHARAbbrev.Fel',
`${actorCharacs?.fel?.value}`
),
]),
new Row(0, row6Y, [
new LabelledText(0, 0, 'Move', `${actorDetails?.move?.value}`),
new LabelledText(0, 0, 'Walk', `${actorDetails?.move?.walk}`),
new LabelledText(0, 0, 'Run', `${actorDetails?.move?.run}`),
new LabelledText(0, 0, 'Fortune', `${actorStatus?.fortune?.value}`),
new LabelledText(0, 0, 'Fate', `${actorStatus?.fate?.value}`),
new LabelledText(0, 0, 'Resolve', `${actorStatus?.resolve?.value}`),
new LabelledText(
0,
0,
'Resilience',
`${actorStatus?.resilience?.value}`
),
new LabelledText(
0,
0,
'Wounds',
`${actorStatus?.wounds?.value}/${actorStatus?.wounds?.max}`
),
]),
new Text(0, row7Y, 'Skills'),
skills,
new Text(0, row9Y, 'Talents'),
talents,
new Text(0, row11Y, 'Traits'),
traits,
]);
docBuilder.doc.save(`${app.actor.name}.pdf`);
});

View File

@@ -1,18 +1,61 @@
import jsPDF, { jsPDFOptions } from 'jspdf';
import { AbstractElement } from './elements/abstract-element';
import { MARGINS } from './constants';
export class PdfBuilder {
public doc: jsPDF;
constructor(options: jsPDFOptions) {
this.doc = new jsPDF(options);
this.doc.advancedAPI();
console.dir(this.doc.internal);
}
public build(elements: AbstractElement[]) {
const finalElements: AbstractElement[] = [];
for (const element of elements) {
element.render(this.doc);
element.prepareRender(this.doc);
finalElements.push(...element.getElements());
}
finalElements.sort((a, b) => {
return a.y - b.y;
});
const pageHeight = this.doc.internal.pageSize.height;
const yMax = pageHeight - MARGINS.bottom;
const pages: AbstractElement[][] = [];
for (const element of finalElements) {
let indexPage = 0;
let currentY = element.y;
const height = element.getCheckNewPageHeight(this.doc);
if (currentY + height > yMax) {
while (currentY + height > yMax) {
indexPage++;
currentY = currentY - yMax + MARGINS.bottom;
if (currentY + height <= yMax) {
if (pages[indexPage] == null) {
pages[indexPage] = [];
}
element.y = currentY;
pages[indexPage].push(element);
}
}
} else {
if (pages[indexPage] == null) {
pages[indexPage] = [];
}
pages[indexPage].push(element);
}
}
let i = 0;
for (const page of pages) {
i++;
for (const element of page) {
element.render(this.doc);
}
if (i < pages.length) {
this.doc.addPage();
}
}
}
}

View File

@@ -2,8 +2,7 @@ const path = require('path');
module.exports = {
entry: './src/main.ts',
devtool: 'inline-source-map',
devtool: 'eval-source-map',
module: {
rules: [
{