ميزات غير واضحة لتطبيق Rotativa لتوليد PDF في تطبيق ASP.NET MVC

يواجه العديد من المطورين مهمة إنشاء تقارير PDF لتطبيقات الويب ، وهو طلب طبيعي تمامًا. أود أن أوجه انتباهكم إلى تجربتي مع هذه المهمة عند استخدام مكتبة Rotativa لإنشاء التقارير. هذه واحدة من المكتبات الأكثر ملاءمة ، في رأيي ، لهذا الغرض في فئتها ، ولكن عند استخدامها واجهت عدة نقاط غير واضحة أريد الحديث عنها.


لأكون صريحًا تمامًا ، أود أن أشارك مجموعة المكابس التي خطوتها في عملية دمج هذه المكتبة ، دون أدنى شك سريعة ومريحة للغاية.


في هذه المقالة لن أتناول مسألة اختيار مكتبة. يمكن لأي شخص أن يكون لديه أسبابه الخاصة لاستخدام هذا أو ذاك. لقد اخترت Rotativa ، لأنه مع الحد الأدنى من تكاليف الإعداد ، حصلت على كل ما هو ضروري لتغطية متطلبات العملاء. بالإضافة إلى ذلك ، جربت ثلاثة أو أربعة خيارات أخرى.


بيان المشكلة


تطبيق ويب على ASP.NET MVC، .NET الإصدار 4.6. الميزات الأخرى ليست ذات صلة في هذا السياق ، باستثناء النشر. من المتوقع أن يتم النشر في Azure. هذا مهم لأن بعض المكتبات الأخرى (مثل HiQPdf) لا تنقل عمليات التثبيت في بعض تكوينات Azure ، وهذا موثق.


أحتاج إلى فتح تقرير HTML ثابت مع رابط واحد ، ونسخة PDF من نفس التقرير مع الرابط الثاني. التقرير نفسه عبارة عن مجموعة من بعض الجداول والحقول والرسوم البيانية لعرضها على المستخدم. يتطلب كلا الإصدارين قائمة مع التنقل عبر أقسام التقرير ووجود الجداول وبعض الرسومات (الألوان وحجم النص والحدود).


استخدام مكتبة روتاتيفا


من السهل تطبيق Rotativa قدر الإمكان في رأيي.


  1. لديك بالفعل تقرير HTML جاهز في شكل قالب ASP.NET MVC ووحدة تحكم ، مثل هذا:

[HttpGet] public async Task<ActionResult> Index(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return View(model); } 

  1. تثبيت حزمة nuget Rotativa


  2. إضافة وحدة تحكم جديدة لتقرير PDF



 [HttpGet] public async Task<ActionResult> Pdf(int param1, string param2) { var model = await service.GetReportDataAsync(param1, param2); return new ViewAsPdf("Index", model); } 

من الآن فصاعدًا ، لديك ملف PDF تم إرجاعه كملف يحتوي على جميع البيانات من تقرير HTML الأصلي.


لم أصف التوجيه هنا ، ولكن من المفهوم أنك قمت بتكوين المسارات لاستدعاء كل من وحدات التحكم بشكل صحيح


من المثير للاهتمام أن هذه المكتبة نفسها هي في الأساس غلاف فوق أداة وحدة التحكم المعروفة wkhtmltopdf . سرعة العمل على ارتفاع ، يمكنك الرهان على Azure - ستعمل. ولكن هناك ميزات سنتحدث عنها.


رقم الصفحة


من المنطقي أن نفترض أن العميل سيطبع ملف PDF ويرغب في رؤية رقم الصفحة. كل شيء بسيط للغاية هنا ، بفضل صانعي روتاتيفا.


وفقًا لوثائق Rotativa ، من خلال معلمة CustomSwitches ، CustomSwitches تحديد الوسيطات التي سيتم تمريرها إلى الأداة المساعدة wkhtmltopdf . حسنًا ، النصائح عبر الإنترنت سخية مع أمثلة. تضيف المكالمة التالية رقمًا أسفل كل صفحة:


 return new ViewAsPdf("Index", model) { PageMargins = new Rotativa.Options.Margins(10, 10, 10, 10), PageSize = Rotativa.Options.Size.A4, PageOrientation = Rotativa.Options.Orientation.Portrait, CustomSwitches = "--page-offset 0 --footer-center [page] --footer-font-size 8 }; 

إنه يعمل بشكل رائع. يتم تمرير رقم الصفحة نفسه باستخدام المعلمة [page] ، وسيتم استبدال هذا النوع من المعلمات بقيم محددة.


بالإضافة إلى [page] هناك أخرى:
  • [الصفحة] تم استبداله بعدد الصفحات الجاري طباعتها حاليًا
  • [frompage] يتم استبداله برقم الصفحة الأولى المراد طباعتها
  • [topage] تم استبداله برقم آخر صفحة سيتم طباعتها
  • [صفحة الويب] تم استبداله بعنوان URL للصفحة الجاري طباعتها
  • [المقطع] تم استبداله باسم القسم الحالي
  • [القسم الفرعي] يتم استبداله باسم القسم الفرعي الحالي
  • [التاريخ] تم استبداله بالتاريخ الحالي بتنسيق محلي للنظام
  • [isodate] تم استبداله بالتاريخ الحالي بتنسيق ISO 8601 الموسع
  • [الوقت] تم استبداله بالوقت الحالي بتنسيق النظام المحلي
  • [عنوان] تم استبداله بعنوان كائن الصفحة الحالية
  • [doctitle] يستعاض عنه بعنوان وثيقة الإخراج
  • [موقع الويب] يتم استبداله برقم الصفحة في الموقع الحالي الذي يتم تحويله
  • [صفحات الموقع] يتم استبداله بعدد الصفحات في الموقع الحالي الذي يتم تحويله


جدول المحتويات


تتطلب التقارير الكبيرة متعددة الصفحات محتوى PDF والتنقل بين الصفحات. هذا أمر مريح للغاية وحيوي ببساطة عندما يتجاوز عدد الصفحات في التقرير مائة.


يحتوي دليل wkhtmltopdf على قائمة كاملة بجميع الخيارات ، من بينها --toc . عند رؤية هذه المعلمة ، تقوم الأداة بشكل أساسي بتجميع كافة العلامات <h1>, <h2>, ... <h6> في المستند وإنشاء جدول محتويات بناءً عليها. وفقًا لذلك ، من الضروري توفير الاستخدام الصحيح لعلامات العناوين هذه في قالب HTML الخاص بك.


ولكن في الواقع ، لا يؤدي إضافة --toc إلى أي عواقب. كما لو لم يكن هناك معلمة. ومع ذلك ، تعمل خيارات أخرى. بفضل منشور في بعض المنتديات ، وجدت أنه يجب تمرير هذه المعلمة بدون واصلات: toc . في الواقع ، في هذه الحالة ، يتم إضافة المحتوى كصفحة أولى. عندما تنقر على سطر في المحتوى ، تنتقل إلى الصفحة المطلوبة في المستند ، وتكون أرقام الصفحات صحيحة.


لم يتضح بعد كيفية تخصيص الأنماط ، لكنني لم أفعل ذلك بعد.


تنفيذ جافا سكريبت


النقطة التالية التي واجهتها هي الحاجة إلى إضافة رسوم بيانية إلى التقرير. تحتوي صفحة HTML الخاصة بي على شفرة JS التي تضيف رسومات باستخدام مكتبة dc.js هنا مثال:


 function initChart() { renderChart(@Html.Raw(Json.Encode(Model.Chart_1_Data)), 'chartDiv_1'); } function renderChart(data, chartElementId) { var colors = ['#03a9f4', '#67daff', '#8bc34a']; var barHeight = 45; var clientHeight = data.length * barHeight + 50; var clientWidth = document.getElementById(chartId).offsetWidth; var chart = dc.rowChart('#' + chartElementId); var ndx = crossfilter(dataToRender); var dimension = ndx.dimension(d => d.name); var group = dimension.group().reduceSum(d => d.value); chart .width(clientWidth) .height(clientHeight) .margins({ top: 16, right: 16, bottom: 16, left: 16 }) .ordinalColors(colors) .dimension(dimension) .group(group) .xAxis() .scale(d3.scaleLinear().domain([0, 2]).range([1, 3]).nice()); chart.render(); } 

في الوقت نفسه ، في HTML ، لدي عنصر مقابل:


 <div id="chart_C2" class="dc-chart"></div> 

لكي يعمل هذا الرمز ، تحتاج إلى استيراد المكتبات المناسبة: d3.js ، crossfilter.js ، crossfilter.js . initChart دالة initChart إلى إنشاء رسم بياني وإدراج النتيجة
svg إلى العنصر المحدد في الشجرة.


لكن PDF لا يحتوي على أي أثر للرسوم البيانية. بالإضافة إلى أي أثر آخر لتنفيذ تعليمات JavaScript البرمجية قبل عرض PDF. التحقق من ذلك أمر سهل للغاية - ما عليك سوى إضافة الرمز الأساسي لإنشاء عنصر <div> بسيط مع النص ، فقط لاختبار حقيقة استدعاء JavaScript.


من الناحية التجريبية ، اتضح أن موقع شفرة JS لـ wkhtmltopdf يلعب دورًا مهمًا. لن يتم تنفيذ الرمز الموجود في نهاية <html> أو ، على سبيل المثال ، في نهاية <body> JS. يبدو أن الأداة ببساطة لا تلاحظه ، أو لا تتوقع مقابلته هناك.


ولكن يتم تنفيذ الرمز داخل <head> . وهكذا ، جئت إلى المخطط عندما توجد شفرة جافا سكريبت بعد إعلان الأنماط داخل علامة <head> ، ويتم استدعاؤها من خلال البناء المعتاد:


 <body onload="initCharts()"> 

في هذه الحالة ، سيتم تنفيذ الرمز كما هو متوقع.


قيود JavaScript


ولكن لم يكن هناك أي رسوم بيانية في ملف PDF الناتج على أي حال. ثم بدأت أخمن أنه نظرًا لعدم كونه متصفحًا كاملاً ، فمن المرجح أن محرك عرض وتنفيذ pdf ليس مثاليًا ولا يفهم القواعد الأخيرة. مرة أخرى ، من الناحية التجريبية ، اكتشفت أن وظائف الأسهم غير مدركة. علاوة على ذلك ، إذا وجد المترجم شيئًا غير معروف له ، فإنه يتوقف عن العمل ببساطة.


استبدال وظائف الأسهم للنموذج x => x.value بمزيد من function(x) { return x.value; } الكلاسيكية function(x) { return x.value; } function(x) { return x.value; } ساعدت وتم تنفيذ جميع التعليمات البرمجية ، دخل الرسم البياني الناتج إلى ملف PDF.


عرض المخطط


من الناحية التجريبية ، اتضح أنه كان من الضروري الإشارة بوضوح إلى عرض العنصر الأصلي للرسم البياني. لهذا ، حددت نمط dc-chart . يحتوي على عرض الرسم البياني بالبكسل. خلاف ذلك ، سيكون الرسم البياني على PDF صغيرًا جدًا ، على الرغم من حقيقة أنه في إصدار HTML سيشغل العرض بالكامل. تحديد عرض النسبة المئوية سيعمل فقط مع HTML.


جافا سكريبت / CSS مضمنة


في الختام ، أود أن أشير إلى أن العديد من المكتبات التي تحول HTML إلى PDF تقبل baseUrl معينة كمعلمة. هذا هو عنوان URL الذي سيكمل المحول على أساسه المسارات النسبية لاستلام أنماط CSS المستوردة أو ملفات JavaScirpt أو الخطوط. لا أستطيع أن أقول على وجه اليقين كيف يعمل هذا في روتاتيفا ، لكني توصلت إلى نهج مختلف.


لتسريع التحميل الأولي للتقرير والتخلص من مصدر مشاكل تضمين النص أو ملفات النمط أثناء التحويل ، أقوم بتضمين JS و CSS الضروريين مباشرةً في نص قالب HTML.


للقيام بذلك ، قم بإنشاء الحزم المناسبة:


 public class BundleConfig { public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new StyleBundle("~/Styles/report-html") .Include("~/Styles/report-common.css") .Include("~/Styles/report-html.css") ); bundles.Add(new StyleBundle("~/Styles/report-pdf") .Include("~/Styles/report-common.css") .Include("~/Styles/report-pdf.css") ); bundles.Add(new ScriptBundle("~/Scripts/charts") .Include("~/Scripts/d3/d3.js") .Include("~/Scripts/crossfilter/crossfilter.js") .Include("~/Scripts/dc/dc.js") ); } } 

أضف مكالمة تهيئة لهذه الحزم إلى Global.asax.cs


 protected void Application_Start() { ... BundleConfig.RegisterBundles(BundleTable.Bundles); } 

وأضف الطريقة المناسبة لتضمين الكود في الصفحة. يجب وضعه في نفس مساحة الاسم مثل Global.asax.cs حتى يمكن استدعاء الطريقة من قالب HTML:


 public static class HtmlHelperExtensions { public static IHtmlString InlineStyles(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<style rel=\"stylesheet\" type=\"text/css\">{bundleContent}</style>"; return new HtmlString(htmlTag); } public static IHtmlString InlineScripts(this HtmlHelper htmlHelper, string bundleVirtualPath) { string bundleContent = LoadBundleContent(htmlHelper.ViewContext.HttpContext, bundleVirtualPath); string htmlTag = $"<script type=\"text/javascript\">{bundleContent}</script>"; return new HtmlString(htmlTag); } private static string LoadBundleContent(HttpContextBase httpContext, string bundleVirtualPath) { var bundleContext = new BundleContext(httpContext, BundleTable.Bundles, bundleVirtualPath); var bundle = BundleTable.Bundles.Single(b => b.Path == bundleVirtualPath); var bundleResponse = bundle.GenerateBundleResponse(bundleContext); return bundleResponse.Content; } } 

حسنًا ، اللمسة الأخيرة هي مكالمة من النموذج:


 @Html.InlineStyles("~/Styles/report-pdf"); @Html.InlineScripts("~/Scripts/charts"); 

ونتيجة لذلك ، ستكون جميع CSS وجافا سكريبت الضرورية مباشرة بتنسيق HTML ، على الرغم من أنه أثناء التطوير يمكنك العمل مع الملفات الفردية.


على الأرجح ، سيفكر الكثيرون على الفور في عدم كفاءة هذا النهج من حيث طلبات التخزين المؤقت التي يقدمها المتصفح. ولكن كان لدي هدفين محددين:


  • بحيث لا يحتاج محول PDF إلى تقديم طلبات في مكان ما للأنماط أو التعليمات البرمجية ، ويجب على المستخدم انتظار ذلك ، على التوالي ؛
  • بحيث يستغرق التنزيل الأول لتقرير PDF و HTML وقتًا قصيرًا ، دون الحاجة إلى الانتظار لبضعة طلبات إضافية. في سياق مشروعي ، هذا مهم.

فواصل الصفحات


قد يصاحب تنظيم التقرير إلى أقسام متطلبات لبدء قسم جديد من صفحة جديدة. في هذه الحالة ، يمكنك استخدام نهج CSS البسيط بنجاح:


 .page-break-before { page-break-before: always; } .no-page-break-inside { page-break-before: auto; page-break-inside: avoid; } 

تقرأ الأداة المساعدة wkhtmltopdf بنجاح هذه الفئات وتدرك أنه من الضروري بدء صفحة جديدة. تخبر الفئة الأولى - page-break-before - الأداة المساعدة دائمًا ببدء صفحة جديدة مع هذا العنصر. يجب أن يتم تطبيق الفئة الثانية - no-page-break-inside - على تلك العناصر التي من المستحسن الاحتفاظ بها كاملة قدر الإمكان على الصفحة. على سبيل المثال ، لديك مجموعات متتالية من المعلومات المنظمة ، أو قل الجداول. إذا احتوت كتلتان على الصفحة - فسيتم تحديد موقعهما. إذا كان الثالث لا يلائم الصفحة ، فلن يكون التالي. إذا كانت أكبر من صفحة ، فإن نقلها أمر لا مفر منه بالفعل. كل هذا يعمل بشكل ملائم ومريح.


سلوك فليكس في wkhtmltopdf


حسنًا ، الميزة الأخيرة التي لاحظتها تتعلق باستخدام أنماط ترميز flexbox. لقد أصبحنا جميعًا معتادين عليها وكل ما يقرب من الترميز يتم من خلال الثنيات. ومع ذلك ، فإن wkhtmltopdf متأخر قليلاً في هذا الصدد. الخيارات المرنة الأفقية لا تعمل (على الأقل في حالتي هذه لم تنجح. رأيت في الشبكة أن الأمر يستحق تكرار الأنماط المرنة على النحو التالي:


 display: -webkit-flex; display: flex; flex-direction: row; -webkit-flex-direction: row; -webkit-box-pack: justify; /* wkhtmltopdf uses this one */ -webkit-justify-content: space-between; justify-content: space-between; 

ولكن لسوء الحظ ، لم يؤد هذا إلى الترميز المتوقع في ملف PDF. اضطررت إلى إعادة تخطيط بعض العناصر بحيث يكون الوضع الأفقي للكتل كما هو مطلوب. إذا كان أي شخص لديه تجربة ناجحة في دمج المرنات لـ wkhtmltopdf ، فيرجى المشاركة. هذا سيكون مفيد جدا


بعض الروابط:


Source: https://habr.com/ru/post/ar425511/


All Articles