Skip to content

Commit

Permalink
Improve pdf generation (#177)
Browse files Browse the repository at this point in the history
* add options for rendering pdfs
  • Loading branch information
Miłosz Skaza authored Sep 7, 2023
1 parent 2541fc1 commit 560c6d9
Show file tree
Hide file tree
Showing 13 changed files with 1,232 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker-next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push next
uses: docker/build-push-action@v3
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docker-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.CR_PAT }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version
run: |
export VERSION=`node -e "console.log(require('./package').version)"`
Expand Down
22 changes: 21 additions & 1 deletion docs/03-dispatching-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,27 @@ The payload for both types of jobs is exactly the same:
"SCREENSHOT",
// render a pdf after the actions are finished
"PDF"
]
],
// additional PDF generation options - will be ignored if PDF option is not selected
"pdf": {
"media": "screen", // media emulation - optional, defaults to screen
"format": "A4", // page format - optional, defaults to A4 (for a full list of supported formats, see the playwright docs on PDF rendering)
"landscape": false, // whether the page should be rendered in landscape mode (horizontal) - optional, defaults to false (portrait / vertical)
"background": true, // whether the page should be rendered with background color / images for elements - optional, defaults to true
"margin": { // page margins - optional, default to 0
"top": "10px", // values can be as supported by CSS, either numbers or strings with units
"right": "10px",
"bottom": "10px",
"left": "10px"
},
"size": { // page size - optional, defaults to an empty object. Format will take precedence over size.
"width": "10px",
"height": "10px"
},
"js": true, // whether to allow JS execution on the page - optional, defaults to true
"delay": 1000, // delay in ms before rendering the page (to allow js execution) - optional, defaults to 0 - will be ignored if js is false, max 30 seconds
"scale": 1.0, // page scale - optional, defaults to 1.0, max 2.0
}
}
```

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"license": "Apache-2.0",
"scripts": {
"cmd:get-issuer-token": "ts-node ./src/cli.ts get-issuer-token",
"cmd:get-visit-token": "ts-node ./src/cli.ts get-visit-token",
"build": "rm -rf ./out && tsc -p tsconfig.production.json",
"start:docker": "node ./src/index.js | pino-pretty",
"start:serve": "node ./out/index.js | pino-pretty",
Expand Down
15 changes: 15 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import config from "./config";
import { getIssuerToken } from "./utils/auth";
import { parseBool } from "./utils/config";
import jwt from "jsonwebtoken";

const args = process.argv.slice(2);
const cmd = args[0];
Expand All @@ -9,6 +11,19 @@ switch (cmd) {
console.log(getIssuerToken(config.SECRET));
break;

case "get-visit-token":
const scope = args[1];
const validity = parseInt(args[2]) || 3600;
const strict = parseBool(args[3]);

const payload = {
scope,
strict,
};

console.log(jwt.sign(payload, config.SECRET, { expiresIn: validity }));
break;

default:
console.log(`Invalid command '${cmd}'`);
process.exit(1);
Expand Down
9 changes: 8 additions & 1 deletion src/jobs/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import { Job } from "bull";
import * as Sentry from "@sentry/node";

import config from "../config";
import { JobBrowser, JobCookieType, JobOptions, JobStepType } from "../schemas/api";
import {
JobBrowser,
JobCookieType,
JobOptions,
JobStepType,
PDFOptionsType,
} from "../schemas/api";
import { PlaywrightRunner } from "../utils/runner";

export type VisitJobData = {
browser: JobBrowser;
steps: JobStepType[];
cookies: JobCookieType[];
options: JobOptions[];
pdf?: PDFOptionsType;
};

export const asyncVisitJob = async (job: Job<VisitJobData>) => {
Expand Down
2 changes: 1 addition & 1 deletion src/loaders/error-handling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const initErrorHandler = (app: FastifyInstance) => {
Sentry.captureException(error);
}

return reply.code(500).send(new Error("Internal Server Error"));
return reply.code(500).send({ status: "error", error: "Internal Server Error" });
},
);
};
72 changes: 72 additions & 0 deletions src/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,77 @@ export enum CookieSameSite {
STRICT = "Strict",
}

export enum PageFormats {
A0 = "A0", // 841 x 1189 mm
A1 = "A1", // 594 x 841 mm
A2 = "A2", // 420 x 594 mm
A3 = "A3", // 297 x 420 mm
A4 = "A4", // 210 x 297 mm
A5 = "A5", // 148.5 x 210 mm
A6 = "A6", // 105 x 148.5 mm
Letter = "Letter", // 8.5in x 11in
Legal = "Legal", // 8.5in x 14in
Tabloid = "Tabloid", // 11in x 17in
Ledger = "Ledger", // 17in x 11in
}

export enum MediaOptions {
SCREEN = "screen",
PRINT = "print",
}

export const PDFOptions = Type.Object(
{
// media emulation (defaults to screen)
media: Type.Enum(MediaOptions, { default: MediaOptions.SCREEN }),

// format defaults to A4
format: Type.Enum(PageFormats, { default: PageFormats.A4 }),

// page orientation (defaults to portrait)
landscape: Type.Boolean({ default: false }),

// print background graphics (defaults to true)
background: Type.Boolean({ default: true }),

// margins can be numbers or strings with units, default to 0 in playwright
margin: Type.Optional(
Type.Object(
{
top: Type.Optional(Type.Union([Type.Number(), Type.String()])),
right: Type.Optional(Type.Union([Type.Number(), Type.String()])),
bottom: Type.Optional(Type.Union([Type.Number(), Type.String()])),
left: Type.Optional(Type.Union([Type.Number(), Type.String()])),
},
{ additionalProperties: false },
),
),

// format takes precedence over width and height
size: Type.Optional(
Type.Object(
{
width: Type.Optional(Type.Union([Type.Number(), Type.String()])),
height: Type.Optional(Type.Union([Type.Number(), Type.String()])),
},
{ additionalProperties: false },
),
),

// whether to enable javascript (defaults to true)
js: Type.Boolean({ default: true }),

// delay in milliseconds to wait for javascript execution (defaults to 0)
delay: Type.Number({ default: 0, maximum: 10000 }),

// scale of the webpage rendering (defaults to 0)
scale: Type.Number({ default: 1.0, minimum: 0.1, maximum: 2.0 }),
},
{ additionalProperties: false },
);

export type PDFOptionsType = Static<typeof PDFOptions>;

export const JobStep = Type.Object(
{
url: Type.String(),
Expand Down Expand Up @@ -64,6 +135,7 @@ export const JobDispatchRequest = Type.Object(
steps: Type.Array(JobStep),
cookies: Type.Array(JobCookie, { default: [] }),
options: Type.Array(Type.Enum(JobOptions), { default: [] }),
pdf: Type.Optional(PDFOptions),
},
{ additionalProperties: false },
);
Expand Down
31 changes: 30 additions & 1 deletion src/utils/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import {
JobOptions,
JobResultType,
JobStepType,
PDFOptionsType,
} from "../schemas/api";

export type PlaywrightRunnerData = {
browser: JobBrowser;
steps: JobStepType[];
cookies: JobCookieType[];
options: JobOptions[];
pdf?: PDFOptionsType;
};

// PlaywrightRunner executes steps and actions in an isolated context
Expand All @@ -30,6 +32,7 @@ export class PlaywrightRunner {
public readonly browserKind: JobBrowser;
public readonly steps: JobStepType[];
public readonly cookies: JobCookieType[];
public readonly pdf?: PDFOptionsType;

private _dialogMessages: string[] = [];
private readonly _debug: boolean;
Expand All @@ -39,6 +42,11 @@ export class PlaywrightRunner {
this.steps = data.steps;
this.cookies = data.cookies;
this.options = data.options;

if (data.pdf) {
this.pdf = data.pdf;
}

this._debug = debug;
}

Expand Down Expand Up @@ -227,7 +235,28 @@ export class PlaywrightRunner {
}

if (this.options.includes(JobOptions.PDF)) {
const pdfBuffer = await this.page!.pdf();
let pdfBuffer;

if (this.pdf) {
await this.page.emulateMedia({ media: this.pdf.media });

if (this.pdf.js && this.pdf.delay > 0) {
await this.page.waitForTimeout(this.pdf.delay);
}

pdfBuffer = await this.page.pdf({
format: this.pdf.format,
landscape: this.pdf.landscape,
margin: this.pdf.margin,
scale: this.pdf.scale,
width: this.pdf.size?.width,
height: this.pdf.size?.height,
printBackground: this.pdf.background,
});
} else {
pdfBuffer = await this.page.pdf();
}

result.pdf = pdfBuffer.toString("base64");
}

Expand Down
Loading

0 comments on commit 560c6d9

Please sign in to comment.