
02 اردیبهشت 1404 / 659 بازدید / 15 دقیقه
چندین سال قبل، روی سیستم مدیریت مالی یک موسسه کار میکردم، این مجموعه به شدت وابسته به یکسری فرمها و گزارشهای کاغذی بود و به طور کلی مقدار خیلی زیادی کاغذ بازی داشت. برای چنین سیستمی که وظیفه پیاده سازیش به عهده من قرار داشت، مدنظر داشتن از هر بخشی بتونن خروجی پی دی اف و قابل پرینت داشته باشن. در ابتدای همکاری من سعی کردم متقاعدشون کنم که بخشی از این کاغذ بازی رو حذف کنن ولی موفق به اینکار نشدم و در آخر قرار بر پیاده سازی خروجیهای پی دی اف شد. در قدم اول تصورم این بود که خروجی پی دی اف گرفتن از صفحات وب نباید چالش خاصی داشته باشد، اما زمانی که شما با حجم دیتای بیش از یک صفحه با ساختار غیر استاتیک مواجه هستید، چالشها سر و کلشون پیدا میشه. برای مثال در گزارشی ممکن بود حجم دیتا غیر قابل پیشبینی باشه و این دیتا باید در قالب جدول قرار میگرفت، سطر و ستون جدول براساس دیتایی که داخش قرار میگرفت ممکن بود طول و عرض متفاوتی داشته باشد که همین باعث میشد میزان حجم اطلاعاتی که در یک صفحه میتونست قرار بگیره متفاوت و غیر قابل پیشبینی باشد. بعد از بررسی و تحقیقاتی که داشتم، پیاده سازی این بخش رو با استفاده از پکیج html2pdf.js که بر پایه کتابخانه jsPdf هست، انجام دادم. در این مقاله، از ذکر جزئیات روش پیاده سازی چشم پوشی میکنم و تنها به صورت کلی سعی بر ارائه تصویر کلی از ایده پیاده سازی فرایند تولید فایل پی دی اف از صفحات وب برای توسعه دهنده و برنامه نویسان دیگر دارم.
در این مقاله کدهای نمایش داده شده به زبان Typescript و فریم ورک Angular خواهد بود، اما هیچ یک از قسمت ها وابسته به فریمورک خاصی نیست و شما میتونید کدهای این مقاله رو در یگر فریم ورک هایی سازگار با زبان Typescript اجرا کنید. تمامی کدها در این مخزن گیت هاب قابل دسترسی میباشد.
برای ایجاد پروژه انگولار با استفاده از Angular CLI میتوانید، دستور زیر را در ترمینال خود اجرا کنید:
ng new <projectname>
سوالات پیکر بندی را مطابق نیاز خودتون پاسخ دهید. پکیج html2pdf.js رو از یکی از دو روش زیر به پروژه ضافه کنید:
npm install html2pdf.js --save-dev
"html2pdf.js": "0.10.1"
و سپس دستور زیر را در ترمینال اجرا کنید:
npm install
بعد از نصب پکیج، نیاز هست که تایپهای پکیج را به پروژه معرفی کنیم، فایلی رو در مسیر src/types/ با نام html2pdf.js.d.ts با محتوی زیر ایجاد کنید:
declare module 'html2pdf.js' { const html2pdf: any; export default html2pdf; }
بعد از اتمام مراحل پیکر بندی شما میتوانید پکیج را در کامپوننت های خود اضافه کنید:
import html2pdf from "html2pdf.js";
برای تنظیم ناحیه چاپ ما نیاز داریم بدانیم کاربر از چه DPI صحفه نمایشی استفاده میکند تا بر اساس آن، اندازه کاغذ و جهت کاغذ (افقی یا عمودی)، اندازه ناحیه چاپ را تعیین کنیم. برای این منظور من تگ زیر را در DOM اضافه کردم:
<div id='testdiv' style='height: 1in; left: -100%; position: absolute; top: -100%; width: 1in;'></div>
در مرحله بعد، تابعی را جهت محاسبه اندازه محیط چاپ با استفاده از این تگ رو به صورت زیر نوشتم:
private _calculateDisplayDpi(): void { const testDiv = document.getElementById('testdiv'); if (testDiv) { const dpi_x = testDiv.offsetWidth; const dpi_y = testDiv.offsetHeight; switch (dpi_x) { case 72: this.paper.pageWidth = 595; break; case 96: this.paper.pageWidth = 794; break; case 150: this.paper.pageWidth = 1240; break; case 300: this.paper.pageWidth = 2480; break; } switch (dpi_y) { case 72: this.paper.pageHeight = 842; break; case 96: this.paper.pageHeight = 1123; break; case 150: this.paper.pageHeight = 1754; break; case 300: this.paper.pageHeight = 3508; break; } this.paper.dpiClass = `${this.paper.orientation}-${dpi_x}`; this.paper.dpiHeight = dpi_y; this.paper.dpiWidth = dpi_x; } }
این تابع با دسترسی به تگ تعریف شده در مرحله قبل DPI صفحه نمایش کاربر را تعیین میکند. همچنین paper فیلدی از کامپوننت هست در قالب زیر:
توجه 1 شما میتوانید این تابع و DPI لیست شده رو بر اساس نیاز خود بسط بدید، هدف من از ارائه کد تنها انتقال ایده در قالب کد میباشد.
توجه2 در فریم ورک انگولار شما میتونید با استفاده از ViewChild نیز به تگ های داخل DOM دسترسی داشته باشید و الزامی به استفاده از document.getElementById نمی باشد.
interface IPaper { orientation: string; dpiClass: string; dpiHeight: number | null; dpiWidth: number | null; pageHeight: number | null; pageWidth: number | null; } paper: IPaper = { orientation: "portrait", dpiClass: "", dpiHeight: null, dpiWidth: null, pageHeight: null, pageWidth: null };
بعد از تعیین شدن ابعاد ناحیه پرینت، میتوانید با استفاده از کلاس های CSS ابعاد ناحیه پرینت را مشخص کنید:
div.paper.portrait-72 { width: 595px; height: 842px; } div.paper.landscape-72 { height: 595px; width: 842px; } div.paper.portrait-96 { width: 794px; height: 1123px; } div.paper.landscape-96 { height: 794px; width: 1123px; } div.paper.portrait-150 { width: 1240px; height: 1754px; } div.paper.landscape-150 { height: 1240px; width: 1754px; } div.paper.portrait-300 { width: 2480px; height: 3508px; } div.paper.landscape-300 { height: 2480px; width: 3508px; } div.print-area.portrait-72 { width: 595px; } div.print-area.landscape-72 { width: 842px; } div.print-area.portrait-96 { width: 794px; } div.print-area.landscape-96 { width: 1123px; } div.print-area.portrait-150 { width: 1240px; } div.print-area.landscape-150 { width: 1754px; } div.print-area.portrait-300 { width: 2480px; } div.print-area.landscape-300 { width: 3508px; }
<div class="pdfContainer" id="pdf"> <div class="print-area" [ngClass]="paper.orientation + '-' + paper.dpiHeight"> <div class="paper" [ngClass]="paper.orientation + '-' + paper.dpiHeight"> <i class="fakharnia-logo logo"></i> </div> </div> </div>
در جدول زیر لیست سایز کاغذها بر اساس DPI متفاوت رو میتوانید بررسی کنید:
| Size | 72 PPI | 96 PPI | 150 PPI | 300 PPI |
|---|---|---|---|---|
| A4 | 595 x 842 | 794 x 1123 | 1240 x 1754 | 2480 x 3508 |
| A5 | 420 x 595 | 559 x 794 | 874 x 1240 | 1748 x 2480 |
اگر حجم اطلاعات متغییر و غیر قابل پیشبینی باشد و یا ساختار صفحه به صورت غیر استاتیک باشد نیاز هست تا ما فرایند انتقال دیتای مازاد صفحه به صفحه دیگر را مدیریت کنیم. همونطور که در مقدمه مقاله اشاره شد، ممکن است در برخی موارد ما صرفا با اطلاعات مختصر مشابه فاکتور فروش یا نمودار دیاگرام تک صفحه ای مواجه نباشیم و نیاز به حجم اطلاعات بیش از یک صفحه برای گنجاندن در صفحات پی دی اف داشته باشیم.
در چنین سناریوهای پیچیده، ما نیاز داریم که اطلاعات رو با روش مرحله به مرحله به DOM تزریق و در هر بار اندازه ناحیه پرینت را بررسی کنیم تا در صورت نیاز به انتقال ادامه اطلاعات به صفحه جدید، اقدامات مورد نیاز به عمل آید. کتابخانه jsPDF راه حل هایی را برای این قسمت ارائه داده است، و ما میتوانیم با استفاده از کلاسهای CSS : before، after و avoid تگهایی که نیاز هست صفحاتی قبل، بعد و یا صرف نظر از رفتن به صحفه جدید درنظر گرفته شود را کنترل کنیم.
در روش من، ما با تعریف چهار تابع با اهداف به ترتیب ایجاد تگ HTML، اضافه کردن تگ جدید در داخل تگ HTML دیگر، حذف یک تگ از تگ HTMLدیگر و بررسی اندازه ناحیه پرینت، عملیات مرحله به مرحله تزریق اطلاعات به DOM و بررسی اندازه هر صفحه جهت اقدام مورد نیاز را مدیریت میکنیم.
_createElement method: هدف این متد ایجاد تگ HTML می باشد.
insertChildrenToNode method: وظیفه این متد اضافه کردن تگ یا تگهایی در داخل تگ HTML میباشد.
removeLastChildFromNode method: وظیفه این متد حذف تگ مازاد بر صفحه از داخل تگ HTML میباشد. در واقع این حذف به معنای انتقال این تگ به صفحه بعد میباشد.
_canvasOverflowed method: این متد با اندازه گیری اندازه ناحیه چاپ نقش کلیدی جهت مدیریت صفحات پی دی اف ایفا میکند.
در ادامه نحوه پیاده سازی هریک از ان توابع را میتوانید بررسی کنید:
private _createElement(tag: string, id: string | null, classList: string[] | null = null, innerHtml: string | null = null): HTMLElement { const element = document.createElement(tag); if (id) { element.id = id; } if (classList) { element.classList.add(...classList); } if (innerHtml) { element.innerHTML = innerHtml; } return element; }
private insertChildrenToNode = (node: any) => (...children: any) => children.forEach((child: any) => node.appendChild(child));
private removeLastChildFromNode = (node: any) => (child: any) => node.removeChild(child);
private _canvasOverflowed(canvas: HTMLElement): boolean { let limitHeight: number; switch (this.paper.dpiHeight) { case 72: limitHeight = this.paper.orientation === "portrait" ? 842 - 135 : 595 - 135; break; case 96: limitHeight = this.paper.orientation === "portrait" ? 1123 - 135 : 794 - 135; break; case 150: limitHeight = this.paper.orientation === "portrait" ? 1754 - 135 : 1240 - 135; break; case 300: limitHeight = this.paper.orientation === "portrait" ? 3508 - 135 : 2480 - 135; break; default: return true; } return canvas.offsetHeight > limitHeight; }
توجه کدهای قسمت قبل اختصاصی یکی از پروژههایی هست که در گذشته من پیاده سازی کردم، از این کدها صرفا جهت آشنایی با این فرایند استفاده کنید و در صورت نیاز ویرایش، بهبود و توسعش رو در نظر داشته باشید.
در مواردی که شما تنها با یک صفحه پی دی اف مواجه هستید یا از حجم اطلاعات خود و نحوه قرار گرفتن در DOM اطمینان دارید شما صرفا میتونید با استفاده از ویژگی های هر فریم ورک مثل ngFor در انگولار و تعریف کلاس های CSS مناسب خروجی مناسب رو در ناحیه پرینت پیاده سازی کنید.
در آخر میتوانید برای گرفتن خروجی PDF از ناحیه پرینت با استفاده از html2pdf.js اقدام کنید، در ادامه میتوانید کد یک مثال از نحوه گرفتن خروجی PDF را ملاحظه فرمایید:
onExportPdf(): void { const pageBreak = { mode: 'css', before: '.before', after: '.after', avoid: '.avoid' }; const options = { filename: "", image: { type: 'jpeg', quality: 1, margin: 0 }, html2canvas: { scale: 2, dpi: this.paper.dpiHeight, logging: true, scrollX: 0, scrollY: -window.scrollY }, pagebreak: pageBreak, jsPDF: { format: [this.paper.pageWidth, this.paper.pageHeight], orientation: this.paper.orientation, putOnlyUsedFonts: true, precision: 1, unit: "px" } }; const element = document.getElementById('pdf'); options.filename = `PDF_${Math.random() * 10}`; const pdf = new html2pdf(); pdf.set(options).from(element).toPdf().get('pdf').then((pdf: any) => { pdf.setProperties({ title: `Generate PDF` }); const pdfData = pdf.output('arraybuffer'); const pdfBlob = new Blob([pdfData], { type: 'application/pdf' }); const blobURL = URL.createObjectURL(pdfBlob); window.open(blobURL, '_blank'); }); }
برای درک بهتر مراحل و گزینه های کانفیگ خروجی میتوانید مستندات پکیج html2pdf.js و مستندات کتابخانه jsPDF را مطالعه فرمایید.
امروزه با توجه به دردسترس بودن ابزارهای هوش مصنوعی، نوشتن یک مقاله بر اساس یک سناریوی اختصاصی به صورت قدم به قدم شاید اتلاف وقت و انرژی بحساب آید، از اینرو در این مقاله سعی در نشان دادن مسیر و چالشهای احتمالی از نحوه پیاده سازی قابلیت گرفتن خروجی PDF از صفحات HTML را داشتم، از اولین باری که من این قابلیت رو در یکی از پروژههام توسعه دادم چندین سال میگذره و من اون رو در چندین پروژه دیگر توسعه و بهبود دادم. این مقاله ترجمهای از نسخه انگلیسی هست که من نوشتم، اگر با متن فارسی نتونستید ارتباط برقرار کنید میتونید نسخه انگلیسی رو با تغییر زبان سایت مطالعه فرمایید.