2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Merge pull request #661 from frappe/report-print

feat: print reports to PDF
This commit is contained in:
Alan 2023-06-13 01:45:06 -07:00 committed by GitHub
commit b1849928d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 610 additions and 220 deletions

View File

@ -19,6 +19,8 @@ import * as errors from './utils/errors';
import { format } from './utils/format';
import { t, T } from './utils/translation';
import { ErrorLog } from './utils/types';
import type { reports } from 'reports/index';
import type { Report } from 'reports/Report';
export class Fyo {
t = t;
@ -234,6 +236,7 @@ export class Fyo {
deviceId: '',
openCount: -1,
appFlags: {} as Record<string, boolean>,
reports: {} as Record<keyof typeof reports, Report | undefined>,
};
}

View File

@ -1,6 +1,15 @@
import { Attachment } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { HiddenMap } from 'fyo/model/types';
export class PrintSettings extends Doc {
logo?: Attachment;
email?: string;
phone?: string;
address?: string;
companyName?: string;
color?: string;
font?: string;
displayLogo?: boolean;
override hidden: HiddenMap = {};
}

View File

@ -2,11 +2,10 @@ import { BalanceSheet } from './BalanceSheet/BalanceSheet';
import { GeneralLedger } from './GeneralLedger/GeneralLedger';
import { GSTR1 } from './GoodsAndServiceTax/GSTR1';
import { GSTR2 } from './GoodsAndServiceTax/GSTR2';
import { StockLedger } from './inventory/StockLedger';
import { StockBalance } from './inventory/StockBalance';
import { ProfitAndLoss } from './ProfitAndLoss/ProfitAndLoss';
import { Report } from './Report';
import { TrialBalance } from './TrialBalance/TrialBalance';
import { StockBalance } from './inventory/StockBalance';
import { StockLedger } from './inventory/StockLedger';
export const reports = {
GeneralLedger,
@ -17,4 +16,4 @@ export const reports = {
GSTR2,
StockLedger,
StockBalance,
} as Record<string, typeof Report>;
} as const;

View File

@ -1,47 +1,45 @@
<template>
<div class="flex">
<div class="flex flex-col flex-1 bg-gray-25">
<PageHeader :border="true">
<template #left>
<AutoComplete
v-if="templateList.length"
:df="{
fieldtype: 'AutoComplete',
fieldname: 'templateName',
label: t`Template Name`,
options: templateList.map((n) => ({ label: n, value: n })),
}"
input-class="text-base py-0 h-8"
class="w-56"
:border="true"
:value="templateName ?? ''"
@change="onTemplateNameChange"
/>
</template>
<DropdownWithActions :actions="actions" :title="t`More`" />
<Button class="text-xs" type="primary" @click="savePDF">
{{ t`Save as PDF` }}
</Button>
</PageHeader>
<!-- Template Display Area -->
<div class="overflow-auto custom-scroll p-4">
<!-- Display Hints -->
<div v-if="helperMessage" class="text-sm text-gray-700">
{{ helperMessage }}
</div>
<!-- Template Container -->
<PrintContainer
ref="printContainer"
v-if="printProps"
:template="printProps.template"
:values="printProps.values"
:scale="scale"
:width="templateDoc?.width"
:height="templateDoc?.height"
<div class="flex flex-col flex-1 bg-gray-25">
<PageHeader :border="true">
<template #left>
<AutoComplete
v-if="templateList.length"
:df="{
fieldtype: 'AutoComplete',
fieldname: 'templateName',
label: t`Template Name`,
options: templateList.map((n) => ({ label: n, value: n })),
}"
input-class="text-base py-0 h-8"
class="w-56"
:border="true"
:value="templateName ?? ''"
@change="onTemplateNameChange"
/>
</template>
<DropdownWithActions :actions="actions" :title="t`More`" />
<Button class="text-xs" type="primary" @click="savePDF">
{{ t`Save as PDF` }}
</Button>
</PageHeader>
<!-- Template Display Area -->
<div class="overflow-auto custom-scroll p-4">
<!-- Display Hints -->
<div v-if="helperMessage" class="text-sm text-gray-700">
{{ helperMessage }}
</div>
<!-- Template Container -->
<PrintContainer
ref="printContainer"
v-if="printProps"
:template="printProps.template"
:values="printProps.values"
:scale="scale"
:width="templateDoc?.width"
:height="templateDoc?.height"
/>
</div>
</div>
</template>
@ -61,6 +59,7 @@ import { PrintValues } from 'src/utils/types';
import { getFormRoute, openSettings, routeTo } from 'src/utils/ui';
import { defineComponent } from 'vue';
import PrintContainer from '../TemplateBuilder/PrintContainer.vue';
import { showSidebar } from 'src/utils/refs';
export default defineComponent({
name: 'PrintView',
@ -204,11 +203,22 @@ export default defineComponent({
this.values = await getPrintTemplatePropValues(this.doc as Doc);
}
},
setScale() {
this.scale = 1;
const width = (this.templateDoc?.width ?? 21) * 37.8;
let containerWidth = window.innerWidth - 32;
if (showSidebar.value) {
containerWidth -= 12 * 16;
}
this.scale = Math.min(containerWidth / width, 1);
},
reset() {
this.doc = null;
this.values = null;
this.templateList = [];
this.templateDoc = null;
this.scale = 1;
},
async onTemplateNameChange(value: string | null): Promise<void> {
if (!value) {
@ -225,6 +235,7 @@ export default defineComponent({
} catch (error) {
await handleErrorWithDialog(error);
}
this.setScale();
},
async setTemplateList(): Promise<void> {
const list = (await this.fyo.db.getAllRaw(ModelNameEnum.PrintTemplate, {

View File

@ -0,0 +1,310 @@
<template>
<div class="flex flex-col w-full h-full">
<PageHeader :title="t`Print ${title}`">
<Button class="text-xs" type="primary" @click="savePDF">
{{ t`Save as PDF` }}
</Button>
</PageHeader>
<div class="outer-container">
<!-- Report Print Display Area -->
<div
class="p-4 bg-gray-25 overflow-auto flex justify-center custom-scroll"
>
<!-- Report Print Display Container -->
<ScaledContainer
class="shadow-lg border bg-white"
ref="scaledContainer"
:scale="scale"
:width="size.width"
:height="size.height"
:show-overflow="true"
>
<div class="bg-white mx-auto">
<div class="p-2">
<div class="font-semibold text-xl w-full flex justify-between">
<h1>
{{ `${fyo.singles.PrintSettings?.companyName}` }}
</h1>
<p class="text-gray-600">
{{ title }}
</p>
</div>
</div>
<!-- Report Data -->
<div class="grid" :style="rowStyles">
<template v-for="(row, r) of matrix" :key="`row-${r}`">
<div
v-for="(cell, c) of row"
:key="`cell-${r}.${c}`"
:class="cellClasses(cell.idx, r)"
class="text-sm p-2"
style="min-height: 2rem"
>
{{ cell.value }}
</div>
</template>
</div>
<div class="border-t p-2">
<p class="text-xs text-right w-full">
{{ fyo.format(new Date(), 'Datetime') }}
</p>
</div>
</div>
</ScaledContainer>
</div>
<!-- Report Print Settings -->
<div class="border-l flex flex-col" v-if="report">
<p class="p-4 text-sm text-gray-600">
{{
[
t`Hidden values will be visible on Print on.`,
t`Report will use more than one page if required.`,
].join(' ')
}}
</p>
<!-- Row Selection -->
<div class="p-4 border-t">
<Int
:show-label="true"
:border="true"
:df="{
label: t`Start From Row Index`,
fieldtype: 'Int',
fieldname: 'numRows',
minvalue: 1,
maxvalue: report?.reportData.length ?? 1000,
}"
:value="start"
@change="(v) => (start = v)"
/>
<Int
class="mt-4"
:show-label="true"
:border="true"
:df="{
label: t`Number of Rows`,
fieldtype: 'Int',
fieldname: 'numRows',
minvalue: 0,
maxvalue: report?.reportData.length ?? 1000,
}"
:value="limit"
@change="(v) => (limit = v)"
/>
</div>
<!-- Size Selection -->
<div class="border-t p-4">
<Select
:show-label="true"
:border="true"
:df="printSizeDf"
:value="printSize"
@change="(v) => (printSize = v)"
/>
<Check
class="mt-4"
:show-label="true"
:border="true"
:df="{
label: t`Is Landscape`,
fieldname: 'isLandscape',
fieldtype: 'Check',
}"
:value="isLandscape"
@change="(v) => (isLandscape = v)"
/>
</div>
<!-- Pick Columns -->
<div class="border-t p-4">
<h2 class="text-sm text-gray-600">
{{ t`Pick Columns` }}
</h2>
<div class="border rounded grid grid-cols-2 mt-1">
<Check
v-for="(col, i) of report?.columns"
:show-label="true"
:key="col.fieldname"
:df="{
label: col.label,
fieldname: col.fieldname,
fieldtype: 'Check',
}"
:value="columnSelection[i]"
@change="(v) => (columnSelection[i] = v)"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Report } from 'reports/Report';
import { reports } from 'reports/index';
import Button from 'src/components/Button.vue';
import Check from 'src/components/Controls/Check.vue';
import Int from 'src/components/Controls/Int.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { getReport } from 'src/utils/misc';
import { PropType, defineComponent } from 'vue';
import ScaledContainer from '../TemplateBuilder/ScaledContainer.vue';
import { getPathAndMakePDF } from 'src/utils/printTemplates';
import { OptionField } from 'schemas/types';
import { paperSizeMap, printSizes } from 'src/utils/ui';
import Select from 'src/components/Controls/Select.vue';
import { showSidebar } from 'src/utils/refs';
export default defineComponent({
props: {
reportName: {
type: String as PropType<keyof typeof reports>,
required: true,
},
},
data() {
return {
start: 1,
limit: 0,
printSize: 'A4' as typeof printSizes[number],
isLandscape: false,
scale: 0.65,
report: null as null | Report,
columnSelection: [] as boolean[],
};
},
async mounted() {
this.report = await getReport(this.reportName);
this.limit = this.report.reportData.length;
this.columnSelection = this.report.columns.map(() => true);
this.setScale();
},
watch: {
size() {
this.setScale();
},
},
computed: {
title(): string {
return reports[this.reportName]?.title ?? this.t`Report`;
},
printSizeDf(): OptionField {
return {
label: 'Print Size',
fieldname: 'printSize',
fieldtype: 'Select',
options: printSizes
.filter((p) => p !== 'Custom')
.map((name) => ({ value: name, label: name })),
};
},
matrix(): { value: string; idx: number }[][] {
if (!this.report) {
return [];
}
const columns = this.report.columns
.map((col, idx) => ({ value: col.label, idx }))
.filter((_, i) => this.columnSelection[i]);
const matrix: { value: string; idx: number }[][] = [columns];
const start = Math.max(this.start - 1, 1);
const end = Math.min(start + this.limit, this.report.reportData.length);
for (const i in this.report.reportData.slice(start, end)) {
const row = this.report.reportData[Number(i) + start];
matrix.push([]);
for (const j in row.cells) {
if (!this.columnSelection[j]) {
continue;
}
const value = row.cells[j].value;
matrix.at(-1)?.push({ value, idx: Number(j) });
}
}
return matrix;
},
rowStyles(): Record<string, string> {
const style: Record<string, string> = {};
const numColumns = this.columnSelection.filter(Boolean).length;
style['grid-template-columns'] = `repeat(${numColumns}, minmax(0, auto))`;
return style;
},
size(): { width: number; height: number } {
const size = paperSizeMap[this.printSize];
const long = size.width > size.height ? size.width : size.height;
const short = size.width <= size.height ? size.width : size.height;
if (this.isLandscape) {
return { width: long, height: short };
}
return { width: short, height: long };
},
},
methods: {
setScale() {
const width = this.size.width * 37.2;
let containerWidth = window.innerWidth - 26 * 16;
if (showSidebar.value) {
containerWidth -= 12 * 16;
}
this.scale = Math.min(containerWidth / width, 1);
},
async savePDF(): Promise<void> {
// @ts-ignore
const innerHTML = this.$refs.scaledContainer.$el.children[0].innerHTML;
if (typeof innerHTML !== 'string') {
return;
}
const name = this.title + ' - ' + this.fyo.format(new Date(), 'Date');
await getPathAndMakePDF(
name,
innerHTML,
this.size.width,
this.size.height
);
},
cellClasses(cIdx: number, rIdx: number): string[] {
const classes: string[] = [];
if (!this.report) {
return classes;
}
const col = this.report.columns[cIdx];
const isFirst = cIdx === 0;
if (col.align) {
classes.push(`text-${col.align}`);
}
if (rIdx === 0) {
classes.push('font-semibold');
}
classes.push('border-t');
if (!isFirst) {
classes.push('border-l');
}
return classes;
},
},
components: { PageHeader, Button, Check, Int, ScaledContainer, Select },
});
</script>
<style scoped>
.outer-container {
display: grid;
grid-template-columns: auto var(--w-quick-edit);
@apply h-full overflow-auto;
}
</style>

View File

@ -11,6 +11,14 @@
>
{{ group.group }}
</DropdownWithActions>
<Button
ref="printButton"
:icon="true"
:title="t`Open Report Print View`"
@click="routeTo(`/report-print/${reportClassName}`)"
>
<feather-icon name="printer" class="w-4 h-4"></feather-icon>
</Button>
</PageHeader>
<!-- Filters -->
@ -28,7 +36,7 @@
:df="field"
:value="report.get(field.fieldname)"
:read-only="loading"
@change="async (value) => await report.set(field.fieldname, value)"
@change="async (value) => await report?.set(field.fieldname, value)"
/>
</div>
@ -36,22 +44,33 @@
<ListReport v-if="report" :report="report" class="" />
</div>
</template>
<script>
<script lang="ts">
import { computed } from '@vue/reactivity';
import { t } from 'fyo';
import { reports } from 'reports';
import { Report } from 'reports/Report';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import PageHeader from 'src/components/PageHeader.vue';
import ListReport from 'src/components/Report/ListReport.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { shortcutsKey } from 'src/utils/injectionKeys';
import { docsPathMap, getReport } from 'src/utils/misc';
import { docsPathRef } from 'src/utils/refs';
import { defineComponent } from 'vue';
import { ActionGroup } from 'src/utils/types';
import { routeTo } from 'src/utils/ui';
import { PropType, defineComponent, inject } from 'vue';
export default defineComponent({
setup() {
return { shortcuts: inject(shortcutsKey) };
},
props: {
reportClassName: String,
reportClassName: {
type: String as PropType<keyof typeof reports>,
required: true,
},
defaultFilters: {
type: String,
default: '{}',
@ -60,7 +79,7 @@ export default defineComponent({
data() {
return {
loading: false,
report: null,
report: null as null | Report,
};
},
provide() {
@ -68,27 +87,40 @@ export default defineComponent({
report: computed(() => this.report),
};
},
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
components: {
PageHeader,
FormControl,
ListReport,
DropdownWithActions,
Button,
},
async activated() {
docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
docsPathRef.value =
docsPathMap[this.reportClassName] ?? docsPathMap.Reports!;
await this.setReportData();
const filters = JSON.parse(this.defaultFilters);
const filterKeys = Object.keys(filters);
for (const key of filterKeys) {
await this.report.set(key, filters[key]);
await this.report?.set(key, filters[key]);
}
if (filterKeys.length) {
await this.report.updateData();
await this.report?.updateData();
}
if (fyo.store.isDevelopment) {
// @ts-ignore
window.rep = this;
}
this.shortcuts?.pmod.set(this.reportClassName, ['KeyP'], () => {
routeTo(`/report-print/${this.reportClassName}`);
});
},
deactivated() {
docsPathRef.value = '';
this.shortcuts?.delete(this.reportClassName);
},
computed: {
title() {
@ -104,24 +136,22 @@ export default defineComponent({
acc[ac.group] ??= {
group: ac.group,
label: ac.label ?? '',
e: ac.type,
type: ac.type ?? 'secondary',
actions: [],
};
acc[ac.group].actions.push(ac);
return acc;
}, {});
}, {} as Record<string, ActionGroup>);
return Object.values(actionsMap);
},
},
methods: {
routeTo,
async setReportData() {
const Report = reports[this.reportClassName];
if (this.report === null) {
this.report = new Report(fyo);
await this.report.initialize();
this.report = await getReport(this.reportClassName);
}
if (!this.report.reportData.length) {

View File

@ -1,6 +1,9 @@
<template>
<div class="overflow-hidden" :style="outerContainerStyle">
<div :style="innerContainerStyle">
<div
:style="innerContainerStyle"
:class="showOverflow ? 'overflow-auto no-scrollbar' : ''"
>
<slot></slot>
</div>
</div>
@ -21,6 +24,7 @@ export default defineComponent({
height: { type: Number, default: 29.7 },
width: { type: Number, default: 21 },
scale: { type: Number, default: 0.65 },
showOverflow: { type: Boolean, default: false },
},
computed: {
innerContainerStyle(): Record<string, string> {

View File

@ -48,165 +48,10 @@ import Button from 'src/components/Button.vue';
import Float from 'src/components/Controls/Float.vue';
import Select from 'src/components/Controls/Select.vue';
import FormHeader from 'src/components/FormHeader.vue';
import { paperSizeMap, printSizes } from 'src/utils/ui';
import { defineComponent } from 'vue';
const printSizes = [
'A0',
'A1',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
'B0',
'B1',
'B2',
'B3',
'B4',
'B5',
'B6',
'B7',
'B8',
'B9',
'Letter',
'Legal',
'Executive',
'C5E',
'Comm10',
'DLE',
'Folio',
'Ledger',
'Tabloid',
'Custom',
] as const;
type SizeName = typeof printSizes[number];
const paperSizeMap: Record<SizeName, { width: number; height: number }> = {
A0: {
width: 84.1,
height: 118.9,
},
A1: {
width: 59.4,
height: 84.1,
},
A2: {
width: 42,
height: 59.4,
},
A3: {
width: 29.7,
height: 42,
},
A4: {
width: 21,
height: 29.7,
},
A5: {
width: 14.8,
height: 21,
},
A6: {
width: 10.5,
height: 14.8,
},
A7: {
width: 7.4,
height: 10.5,
},
A8: {
width: 5.2,
height: 7.4,
},
A9: {
width: 3.7,
height: 5.2,
},
B0: {
width: 100,
height: 141.4,
},
B1: {
width: 70.7,
height: 100,
},
B2: {
width: 50,
height: 70.7,
},
B3: {
width: 35.3,
height: 50,
},
B4: {
width: 25,
height: 35.3,
},
B5: {
width: 17.6,
height: 25,
},
B6: {
width: 12.5,
height: 17.6,
},
B7: {
width: 8.8,
height: 12.5,
},
B8: {
width: 6.2,
height: 8.8,
},
B9: {
width: 4.4,
height: 6.2,
},
Letter: {
width: 21.59,
height: 27.94,
},
Legal: {
width: 21.59,
height: 35.56,
},
Executive: {
width: 19.05,
height: 25.4,
},
C5E: {
width: 16.3,
height: 22.9,
},
Comm10: {
width: 10.5,
height: 24.1,
},
DLE: {
width: 11,
height: 22,
},
Folio: {
width: 21,
height: 33,
},
Ledger: {
width: 43.2,
height: 27.9,
},
Tabloid: {
width: 27.9,
height: 43.2,
},
Custom: {
width: -1,
height: -1,
},
};
export default defineComponent({
props: { doc: { type: PrintTemplate, required: true } },
data() {

View File

@ -5,6 +5,7 @@ import GetStarted from 'src/pages/GetStarted.vue';
import ImportWizard from 'src/pages/ImportWizard.vue';
import ListView from 'src/pages/ListView/ListView.vue';
import PrintView from 'src/pages/PrintView/PrintView.vue';
import ReportPrintView from 'src/pages/PrintView/ReportPrintView.vue';
import QuickEditForm from 'src/pages/QuickEditForm.vue';
import Report from 'src/pages/Report.vue';
import Settings from 'src/pages/Settings/Settings.vue';
@ -69,6 +70,12 @@ const routes: RouteRecordRaw[] = [
component: PrintView,
props: true,
},
{
path: '/report-print/:reportName',
name: 'ReportPrintView',
component: ReportPrintView,
props: true,
},
{
path: '/report/:reportClassName',
name: 'Report',

View File

@ -1,6 +1,5 @@
import { Fyo } from 'fyo';
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { DateTime } from 'luxon';
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
import { ModelNameEnum } from 'models/types';
@ -9,6 +8,7 @@ import { Schema } from 'schemas/types';
import { fyo } from 'src/initFyo';
import { QueryFilter } from 'utils/db/types';
import { PeriodKey } from './types';
import { reports } from 'reports/index';
export function getDatesAndPeriodList(period: PeriodKey): {
periodList: DateTime[];
@ -167,3 +167,15 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
export function getIsMac() {
return navigator.userAgent.indexOf('Mac') !== -1;
}
export async function getReport(name: keyof typeof reports) {
const cachedReport = fyo.store.reports[name];
if (cachedReport) {
return cachedReport;
}
const report = new reports[name](fyo);
await report.initialize();
fyo.store.reports[name] = report;
return report;
}

View File

@ -157,7 +157,8 @@ function getCreateList(fyo: Fyo): SearchItem[] {
function getReportList(fyo: Fyo): SearchItem[] {
const hasGstin = !!fyo.singles?.AccountingSettings?.gstin;
const hasInventory = !!fyo.singles?.AccountingSettings?.enableInventory;
return Object.keys(reports)
const reportNames = Object.keys(reports) as (keyof typeof reports)[];
return reportNames
.filter((r) => {
const report = reports[r];
if (report.isInventory && !hasInventory) {

View File

@ -711,3 +711,162 @@ function getDocReferenceLabel(doc: Doc) {
return doc.name || label;
}
export const printSizes = [
'A0',
'A1',
'A2',
'A3',
'A4',
'A5',
'A6',
'A7',
'A8',
'A9',
'B0',
'B1',
'B2',
'B3',
'B4',
'B5',
'B6',
'B7',
'B8',
'B9',
'Letter',
'Legal',
'Executive',
'C5E',
'Comm10',
'DLE',
'Folio',
'Ledger',
'Tabloid',
'Custom',
] as const;
export const paperSizeMap: Record<
typeof printSizes[number],
{ width: number; height: number }
> = {
A0: {
width: 84.1,
height: 118.9,
},
A1: {
width: 59.4,
height: 84.1,
},
A2: {
width: 42,
height: 59.4,
},
A3: {
width: 29.7,
height: 42,
},
A4: {
width: 21,
height: 29.7,
},
A5: {
width: 14.8,
height: 21,
},
A6: {
width: 10.5,
height: 14.8,
},
A7: {
width: 7.4,
height: 10.5,
},
A8: {
width: 5.2,
height: 7.4,
},
A9: {
width: 3.7,
height: 5.2,
},
B0: {
width: 100,
height: 141.4,
},
B1: {
width: 70.7,
height: 100,
},
B2: {
width: 50,
height: 70.7,
},
B3: {
width: 35.3,
height: 50,
},
B4: {
width: 25,
height: 35.3,
},
B5: {
width: 17.6,
height: 25,
},
B6: {
width: 12.5,
height: 17.6,
},
B7: {
width: 8.8,
height: 12.5,
},
B8: {
width: 6.2,
height: 8.8,
},
B9: {
width: 4.4,
height: 6.2,
},
Letter: {
width: 21.59,
height: 27.94,
},
Legal: {
width: 21.59,
height: 35.56,
},
Executive: {
width: 19.05,
height: 25.4,
},
C5E: {
width: 16.3,
height: 22.9,
},
Comm10: {
width: 10.5,
height: 24.1,
},
DLE: {
width: 11,
height: 22,
},
Folio: {
width: 21,
height: 33,
},
Ledger: {
width: 43.2,
height: 27.9,
},
Tabloid: {
width: 27.9,
height: 43.2,
},
Custom: {
width: -1,
height: -1,
},
};