-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to update table of content page after all sections are created? #1551
Comments
Ive run into exactly this issue. There are two ways to approach this. Ethier you can pre-calculate your PDF content per page and create the page all in one (which is the solution I took) or when creating the PDFDocument class instance, you can add the paramter As an aside, here is the code for the pdf planning class I created. It plans a 2 column layout with a few different types of content. I think ideally this is the better solution as with a large PDF with alot of pages. Having to return to the first at the end may consume alot of memory. //booklet-layout.ts
import { Container, Search, SearchItem, Sponsorship } from 'fido-common';
import { instanceConfig, tokens } from '../../../constants.js';
import PDFDocument from 'pdfkit';
import { Utils } from './utils.js';
export enum fontTypes {
title,
content,
sub,
}
export enum planColumns {
left,
right,
}
export enum planTokens {
contents,
summary,
article,
sponsor,
}
export type planColumn = { availableHeight: number; items: placedPlanItem[] };
export type planPage = Record<planColumns, planColumn>;
type planItem = { height: number } & (
| { search: Search; type: planTokens.contents }
| { summary: string; type: planTokens.summary }
| { image?: Buffer; searchItem: SearchItem; type: planTokens.article }
| { image: Buffer; sponsor: Sponsorship; type: planTokens.sponsor }
);
export type placedPlanItem = { yPos: number } & planItem;
export class BookletLayoutPlan {
static adQrSize = 50;
static columnImageMargin = 10;
static columnOffsets: Record<planColumns, number> = {
[planColumns.left]: 10,
[planColumns.right]: 302,
};
static columnWidth = 282;
private config: instanceConfig;
static fontSizes: Record<fontTypes, number> = {
[fontTypes.title]: 16,
[fontTypes.content]: 10,
[fontTypes.sub]: 8,
};
static headerOffset: Record<planColumns, number> = {
[planColumns.left]: 10,
[planColumns.right]: 345,
};
static headerWidth = 240;
static idealSpacing = 50;
static minimumSpacing = 20;
static pageHeight = 840;
static pageWidth = 595;
static pageYmargin = 40;
private plan: planPage[];
private get planPageTemplate(): planPage {
return {
[planColumns.left]: {
availableHeight: BookletLayoutPlan.columnHeight,
items: [],
},
[planColumns.right]: {
availableHeight: BookletLayoutPlan.columnHeight,
items: [],
},
};
}
constructor() {
this.plan = [this.planPageTemplate];
const container = Container.getInstance();
this.config = container.resolve<instanceConfig>(tokens.instance);
}
static get columnHeight(): number {
return this.pageHeight - this.pageYmargin - this.pageYmargin;
}
static get columnImageWidth(): number {
return this.columnWidth - this.columnImageMargin * 2;
}
async buildPlan(
search: Search,
doc: typeof PDFDocument,
articleImages: Record<string, Buffer>,
sponsorImage: Buffer | undefined,
): Promise<planPage[]> {
const planItems = await this.createPlanItems(
search,
doc,
articleImages,
sponsorImage,
);
//place items
for (const item of planItems) {
if (item.height > 0 && item.height < BookletLayoutPlan.columnHeight) {
let placed = false;
for (const page of this.plan) {
const spaceLeft = page[planColumns.left].availableHeight;
const spaceRight = page[planColumns.right].availableHeight;
const couldFit =
Math.max(spaceLeft, spaceRight) >
item.height + BookletLayoutPlan.minimumSpacing;
if (spaceLeft >= spaceRight && couldFit) {
page[planColumns.left].items.push({ yPos: 0, ...item });
page[planColumns.left].availableHeight -=
item.height + BookletLayoutPlan.minimumSpacing;
placed = true;
break;
} else if (couldFit) {
page[planColumns.right].items.push({ yPos: 0, ...item });
page[planColumns.right].availableHeight -=
item.height + BookletLayoutPlan.minimumSpacing;
placed = true;
break;
}
}
if (!placed) {
this.plan.push(this.planPageTemplate);
const lastPage = this.plan[this.plan.length - 1];
lastPage[planColumns.left].items.push({ yPos: 0, ...item });
lastPage[planColumns.left].availableHeight -=
item.height + BookletLayoutPlan.minimumSpacing;
}
}
}
//distribute items
for (const page of this.plan) {
this.spaceColumn(page[planColumns.left]);
this.spaceColumn(page[planColumns.right]);
}
return this.plan;
}
private async createPlanItems(
search: Search,
doc: typeof PDFDocument,
articleImages: Record<string, Buffer>,
sponsorImage: Buffer | undefined,
): Promise<planItem[]> {
const planItems: planItem[] = [];
const contentsHeight = Utils.estimateContentsHeight(
doc,
search.searchItems!,
);
planItems.push({
height: contentsHeight,
search,
type: planTokens.contents,
});
if (search.summary?.enabled && search.summary?.successful) {
const summaryHeight = Utils.estimateSummaryHeight(
doc,
search.summary.summary!,
);
planItems.push({
summary: search.summary!.summary!,
height: summaryHeight,
type: planTokens.summary,
});
}
const articlePlanItems = await Promise.all(
search.searchItems?.map(async (searchItem) => {
const height = await Utils.estimatArticleHeight(
searchItem,
articleImages,
doc,
);
let image: Buffer | undefined = undefined;
if (searchItem.articleImage) {
image = articleImages[searchItem.articleImage.publicBlobUrl!];
}
return {
height,
image,
searchItem,
type: planTokens.article,
};
}) as Promise<planItem>[],
);
let sponsorPlanItem: planItem | undefined = undefined;
if (search.sponsorship && sponsorImage) {
const height = await Utils.estimateSponsorHeight(sponsorImage);
sponsorPlanItem = {
height,
image: sponsorImage,
sponsor: search.sponsorship,
type: planTokens.sponsor,
};
}
for (const i in articlePlanItems) {
planItems.push(articlePlanItems[i]);
if (
sponsorPlanItem &&
Number.parseInt(i) % this.config.articlesPerSponsor == 0
) {
planItems.push(sponsorPlanItem);
}
}
return planItems;
}
private spaceColumn(column: planColumn): void {
const totalWhitespace =
BookletLayoutPlan.columnHeight -
column.items.reduce((acc, cur) => acc + cur.height, 0);
const spaceBetween = Math.min(
totalWhitespace / (column.items.length - 1),
BookletLayoutPlan.idealSpacing,
);
for (let i = 0; i < column.items.length; i++) {
if (i == 0) {
column.items[i].yPos = 0;
} else {
const bottomOfPrevious =
column.items[i - 1].yPos + column.items[i - 1].height;
column.items[i].yPos = bottomOfPrevious + spaceBetween;
}
}
}
}
//utils.ts
import { BookletLayoutPlan } from './booklet-layout-plan.js';
import PDFDocument from 'pdfkit';
import { SearchItem } from 'fido-common';
import { fontTypes } from './/booklet-layout-plan.js';
import sharp from 'sharp';
export class Utils {
static async estimatArticleHeight(
article: SearchItem,
articleImages: Record<string, Buffer>,
doc: typeof PDFDocument,
): Promise<number> {
let imgHeight = 0;
if (
article.articleImage &&
articleImages[article.articleImage.publicBlobUrl!]
) {
const articleImage = articleImages[article.articleImage.publicBlobUrl!];
const img = await sharp(articleImage);
const imageMeta = await img.metadata();
const imgRatio = (imageMeta.height ?? 1) / (imageMeta.width ?? 1);
imgHeight = imgRatio * BookletLayoutPlan.columnImageWidth;
}
return (
doc.fontSize(16).heightOfString(article.article!.title!, {
width: BookletLayoutPlan.columnWidth,
}) +
imgHeight +
doc.fontSize(10).heightOfString(article!.article!.summary!, {
width: BookletLayoutPlan.columnWidth,
}) +
doc.currentLineHeight(true) * 0.6 +
doc.fontSize(8).heightOfString(article.article!.sourceLink!, {
width: BookletLayoutPlan.columnWidth,
})
);
}
static estimateContentsHeight(
doc: typeof PDFDocument,
searchItems: SearchItem[],
): number {
let rollingHeight = this.setFont(
doc,
'black',
'Helvetica-Bold',
fontTypes.title,
).heightOfString('Contents: ', {
width: BookletLayoutPlan.columnImageWidth,
});
this.setFont(doc, 'black', 'Helvetica', fontTypes.content);
for (const item of searchItems) {
rollingHeight += doc.heightOfString('• ' + item.article!.title, {
width: BookletLayoutPlan.columnImageWidth,
});
}
return (rollingHeight += 20);
}
static async estimateSponsorHeight(sponsorImage: Buffer): Promise<number> {
const img = await sharp(sponsorImage);
const imageMeta = await img.metadata();
const imgRatio = (imageMeta.height ?? 1) / (imageMeta.width ?? 1);
return imgRatio * BookletLayoutPlan.columnImageWidth;
}
static estimateSummaryHeight(
doc: typeof PDFDocument,
summary: string,
): number {
const summaryParagraphs = summary.split('\n').filter((pg) => pg);
let rollingHeight = this.setFont(
doc,
'black',
'Helvetica',
fontTypes.title,
).heightOfString('Summary:', { width: BookletLayoutPlan.columnWidth });
this.setFont(doc, 'black', 'Helvetica', fontTypes.content);
summaryParagraphs.forEach((paragraph) => {
rollingHeight += doc.heightOfString(paragraph, {
width: BookletLayoutPlan.columnWidth,
});
});
rollingHeight +=
(summaryParagraphs.length - 1) * (doc.currentLineHeight(false) * 0.5);
return rollingHeight;
}
static setFont(
doc: typeof PDFDocument,
colour: PDFKit.Mixins.ColorValue,
font: PDFKit.Mixins.PDFFontSource,
type: fontTypes,
): PDFKit.PDFDocument {
return doc
.fillAndStroke(colour, colour)
.font(font, BookletLayoutPlan.fontSizes[type]);
}
} |
I am generating a PDF for reports that includes a table of contents and page numbers. However, we can only determine the page numbers after generating all sections. I have also attempted to use doc.switchToPage().
The text was updated successfully, but these errors were encountered: