mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-23 16:19:49 +00:00
feat: optimize documents printing
This commit is contained in:
@@ -49,4 +49,7 @@ SIGNUP_ALLOWED_DOMAINS=
|
|||||||
SIGNUP_ALLOWED_EMAILS=
|
SIGNUP_ALLOWED_EMAILS=
|
||||||
|
|
||||||
# API rate limit (points,duration,block duration).
|
# API rate limit (points,duration,block duration).
|
||||||
API_RATE_LIMIT=120,60,600
|
API_RATE_LIMIT=120,60,600
|
||||||
|
|
||||||
|
# Gotenberg API for PDF printing.
|
||||||
|
GOTENBERG_url=http://localhost:9000
|
||||||
@@ -47,6 +47,11 @@ services:
|
|||||||
restart_policy:
|
restart_policy:
|
||||||
condition: unless-stopped
|
condition: unless-stopped
|
||||||
|
|
||||||
|
gotenberg:
|
||||||
|
image: gotenberg/gotenberg:7
|
||||||
|
ports:
|
||||||
|
- "9000:3000"
|
||||||
|
|
||||||
# Volumes
|
# Volumes
|
||||||
volumes:
|
volumes:
|
||||||
mysql:
|
mysql:
|
||||||
|
|||||||
23
packages/server/src/lib/Chromiumly/Chromiumly.ts
Normal file
23
packages/server/src/lib/Chromiumly/Chromiumly.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ChromiumRoute, LibreOfficeRoute, PdfEngineRoute } from './_types';
|
||||||
|
|
||||||
|
export class Chromiumly {
|
||||||
|
public static readonly GOTENBERG_ENDPOINT = '';
|
||||||
|
|
||||||
|
public static readonly CHROMIUM_PATH = 'forms/chromium/convert';
|
||||||
|
public static readonly PDF_ENGINES_PATH = 'forms/pdfengines';
|
||||||
|
public static readonly LIBRE_OFFICE_PATH = 'forms/libreoffice';
|
||||||
|
|
||||||
|
public static readonly CHROMIUM_ROUTES = {
|
||||||
|
url: ChromiumRoute.URL,
|
||||||
|
html: ChromiumRoute.HTML,
|
||||||
|
markdown: ChromiumRoute.MARKDOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly PDF_ENGINE_ROUTES = {
|
||||||
|
merge: PdfEngineRoute.MERGE,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly LIBRE_OFFICE_ROUTES = {
|
||||||
|
convert: LibreOfficeRoute.CONVERT,
|
||||||
|
};
|
||||||
|
}
|
||||||
66
packages/server/src/lib/Chromiumly/ConvertUtils.ts
Normal file
66
packages/server/src/lib/Chromiumly/ConvertUtils.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import FormData from 'form-data';
|
||||||
|
import { GotenbergUtils } from './GotenbergUtils';
|
||||||
|
import { PageProperties } from './_types';
|
||||||
|
|
||||||
|
export class ConverterUtils {
|
||||||
|
public static injectPageProperties(
|
||||||
|
data: FormData,
|
||||||
|
pageProperties: PageProperties
|
||||||
|
): void {
|
||||||
|
if (pageProperties.size) {
|
||||||
|
GotenbergUtils.assert(
|
||||||
|
pageProperties.size.width >= 1.0 && pageProperties.size.height >= 1.5,
|
||||||
|
'size is smaller than the minimum printing requirements (i.e. 1.0 x 1.5 in)'
|
||||||
|
);
|
||||||
|
|
||||||
|
data.append('paperWidth', pageProperties.size.width);
|
||||||
|
data.append('paperHeight', pageProperties.size.height);
|
||||||
|
}
|
||||||
|
if (pageProperties.margins) {
|
||||||
|
GotenbergUtils.assert(
|
||||||
|
pageProperties.margins.top >= 0 &&
|
||||||
|
pageProperties.margins.bottom >= 0 &&
|
||||||
|
pageProperties.margins.left >= 0 &&
|
||||||
|
pageProperties.margins.left >= 0,
|
||||||
|
'negative margins are not allowed'
|
||||||
|
);
|
||||||
|
data.append('marginTop', pageProperties.margins.top);
|
||||||
|
data.append('marginBottom', pageProperties.margins.bottom);
|
||||||
|
data.append('marginLeft', pageProperties.margins.left);
|
||||||
|
data.append('marginRight', pageProperties.margins.right);
|
||||||
|
}
|
||||||
|
if (pageProperties.preferCssPageSize) {
|
||||||
|
data.append(
|
||||||
|
'preferCssPageSize',
|
||||||
|
String(pageProperties.preferCssPageSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (pageProperties.printBackground) {
|
||||||
|
data.append('printBackground', String(pageProperties.printBackground));
|
||||||
|
}
|
||||||
|
if (pageProperties.landscape) {
|
||||||
|
data.append('landscape', String(pageProperties.landscape));
|
||||||
|
}
|
||||||
|
if (pageProperties.scale) {
|
||||||
|
GotenbergUtils.assert(
|
||||||
|
pageProperties.scale >= 0.1 && pageProperties.scale <= 2.0,
|
||||||
|
'scale is outside of [0.1 - 2] range'
|
||||||
|
);
|
||||||
|
data.append('scale', pageProperties.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageProperties.nativePageRanges) {
|
||||||
|
GotenbergUtils.assert(
|
||||||
|
pageProperties.nativePageRanges.from > 0 &&
|
||||||
|
pageProperties.nativePageRanges.to > 0 &&
|
||||||
|
pageProperties.nativePageRanges.to >=
|
||||||
|
pageProperties.nativePageRanges.from,
|
||||||
|
'page ranges syntax error'
|
||||||
|
);
|
||||||
|
data.append(
|
||||||
|
'nativePageRanges',
|
||||||
|
`${pageProperties.nativePageRanges.from}-${pageProperties.nativePageRanges.to}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/server/src/lib/Chromiumly/Converter.ts
Normal file
9
packages/server/src/lib/Chromiumly/Converter.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { Chromiumly, ChromiumRoute } from '../../main.config';
|
||||||
|
|
||||||
|
export abstract class Converter {
|
||||||
|
readonly endpoint: string;
|
||||||
|
|
||||||
|
constructor(route: ChromiumRoute) {
|
||||||
|
this.endpoint = `${Chromiumly.GOTENBERG_ENDPOINT}/${Chromiumly.CHROMIUM_PATH}/${Chromiumly.CHROMIUM_ROUTES[route]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
packages/server/src/lib/Chromiumly/GotenbergUtils.ts
Normal file
25
packages/server/src/lib/Chromiumly/GotenbergUtils.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import FormData from 'form-data';
|
||||||
|
import fetch from 'node-fetch';
|
||||||
|
|
||||||
|
export class GotenbergUtils {
|
||||||
|
public static assert(condition: boolean, message: string): asserts condition {
|
||||||
|
if (!condition) {
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async fetch(endpoint: string, data: FormData): Promise<Buffer> {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'post',
|
||||||
|
body: data,
|
||||||
|
headers: {
|
||||||
|
...data.getHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.buffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/server/src/lib/Chromiumly/HTMLConvert.ts
Normal file
38
packages/server/src/lib/Chromiumly/HTMLConvert.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { constants, createReadStream, PathLike, promises } from 'fs';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import { GotenbergUtils } from './GotenbergUtils';
|
||||||
|
import { IConverter, PageProperties } from './_types';
|
||||||
|
import { PdfFormat, ChromiumRoute } from './_types';
|
||||||
|
import { ConverterUtils } from './ConvertUtils';
|
||||||
|
import { Converter } from './Converter';
|
||||||
|
|
||||||
|
export class HtmlConverter extends Converter implements IConverter {
|
||||||
|
constructor() {
|
||||||
|
super(ChromiumRoute.HTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert({
|
||||||
|
html,
|
||||||
|
properties,
|
||||||
|
pdfFormat,
|
||||||
|
}: {
|
||||||
|
html: PathLike;
|
||||||
|
properties?: PageProperties;
|
||||||
|
pdfFormat?: PdfFormat;
|
||||||
|
}): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
await promises.access(html, constants.R_OK);
|
||||||
|
const data = new FormData();
|
||||||
|
if (pdfFormat) {
|
||||||
|
data.append('pdfFormat', pdfFormat);
|
||||||
|
}
|
||||||
|
data.append('index.html', createReadStream(html));
|
||||||
|
if (properties) {
|
||||||
|
ConverterUtils.injectPageProperties(data, properties);
|
||||||
|
}
|
||||||
|
return GotenbergUtils.fetch(this.endpoint, data);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
packages/server/src/lib/Chromiumly/UrlConvert.ts
Normal file
38
packages/server/src/lib/Chromiumly/UrlConvert.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import FormData from 'form-data';
|
||||||
|
import { IConverter, PageProperties, PdfFormat, ChromiumRoute } from './_types';
|
||||||
|
import { ConverterUtils } from './ConvertUtils';
|
||||||
|
import { Converter } from './Converter';
|
||||||
|
import { GotenbergUtils } from './GotenbergUtils';
|
||||||
|
|
||||||
|
export class UrlConverter extends Converter implements IConverter {
|
||||||
|
constructor() {
|
||||||
|
super(ChromiumRoute.URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert({
|
||||||
|
url,
|
||||||
|
properties,
|
||||||
|
pdfFormat,
|
||||||
|
}: {
|
||||||
|
url: string;
|
||||||
|
properties?: PageProperties;
|
||||||
|
pdfFormat?: PdfFormat;
|
||||||
|
}): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
const _url = new URL(url);
|
||||||
|
const data = new FormData();
|
||||||
|
|
||||||
|
if (pdfFormat) {
|
||||||
|
data.append('pdfFormat', pdfFormat);
|
||||||
|
}
|
||||||
|
data.append('url', _url.href);
|
||||||
|
|
||||||
|
if (properties) {
|
||||||
|
ConverterUtils.injectPageProperties(data, properties);
|
||||||
|
}
|
||||||
|
return GotenbergUtils.fetch(this.endpoint, data);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/server/src/lib/Chromiumly/_types.ts
Normal file
51
packages/server/src/lib/Chromiumly/_types.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { PathLike } from 'fs';
|
||||||
|
|
||||||
|
export type PageSize = {
|
||||||
|
width: number; // Paper width, in inches (default 8.5)
|
||||||
|
height: number; //Paper height, in inches (default 11)
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageMargins = {
|
||||||
|
top: number; // Top margin, in inches (default 0.39)
|
||||||
|
bottom: number; // Bottom margin, in inches (default 0.39)
|
||||||
|
left: number; // Left margin, in inches (default 0.39)
|
||||||
|
right: number; // Right margin, in inches (default 0.39)
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PageProperties = {
|
||||||
|
size?: PageSize;
|
||||||
|
margins?: PageMargins;
|
||||||
|
preferCssPageSize?: boolean; // Define whether to prefer page size as defined by CSS (default false)
|
||||||
|
printBackground?: boolean; // Print the background graphics (default false)
|
||||||
|
landscape?: boolean; // Set the paper orientation to landscape (default false)
|
||||||
|
scale?: number; // The scale of the page rendering (default 1.0)
|
||||||
|
nativePageRanges?: { from: number; to: number }; // Page ranges to print
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IConverter {
|
||||||
|
convert({
|
||||||
|
...args
|
||||||
|
}: {
|
||||||
|
[x: string]: string | PathLike | PageProperties | PdfFormat;
|
||||||
|
}): Promise<Buffer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfFormat {
|
||||||
|
A_1a = 'PDF/A-1a',
|
||||||
|
A_2b = 'PDF/A-2b',
|
||||||
|
A_3b = 'PDF/A-3b',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ChromiumRoute {
|
||||||
|
URL = 'url',
|
||||||
|
HTML = 'html',
|
||||||
|
MARKDOWN = 'markdown',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PdfEngineRoute {
|
||||||
|
MERGE = 'merge',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibreOfficeRoute {
|
||||||
|
CONVERT = 'convert',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user