Fitur-fitur aplikasi Rotativa yang tidak jelas untuk menghasilkan PDF dalam aplikasi ASP.NET MVC

Banyak pengembang dihadapkan dengan tugas membuat laporan PDF untuk aplikasi web, permintaan yang sepenuhnya alami. Saya ingin menarik perhatian Anda pengalaman saya dengan tugas seperti itu ketika menggunakan perpustakaan Rotativa untuk menghasilkan laporan. Ini adalah salah satu yang paling, menurut pendapat saya, perpustakaan yang nyaman untuk tujuan seperti itu di segmennya, tetapi ketika menggunakannya saya menemukan beberapa poin yang tidak jelas yang ingin saya bicarakan.


Sejujurnya, saya ingin berbagi set garu yang saya tuju dalam proses mengintegrasikan perpustakaan ini, tanpa ragu cepat dan sangat nyaman.


Dalam artikel ini saya tidak akan membahas masalah memilih perpustakaan. Setiap orang dapat memiliki alasan sendiri untuk menggunakan ini atau itu. Saya memilih Rotativa, karena dengan biaya pemasangan minimal, semua yang diperlukan untuk memenuhi kebutuhan pelanggan. Selain dia, saya mencoba tiga atau empat opsi lagi.


Pernyataan masalah


Aplikasi web pada ASP.NET MVC, .NET versi 4.6. Fitur lain tidak relevan dalam konteks ini, dengan pengecualian penyebaran. Penempatan diharapkan berlangsung di Azure. Ini penting karena beberapa perpustakaan lain (mis. HiQPdf) tidak mentransfer instalasi dalam konfigurasi Azure tertentu, ini didokumentasikan.


Saya perlu membuka laporan HTML statis tertentu dengan satu tautan, dan versi PDF dari laporan yang sama dengan tautan kedua. Laporan itu sendiri hanyalah seperangkat beberapa tabel, bidang dan grafik untuk demonstrasi kepada pengguna. Kedua versi memerlukan menu dengan navigasi melalui bagian-bagian laporan, keberadaan tabel, beberapa gambar (warna, ukuran teks, batas).


Menggunakan Perpustakaan Rotativa


Menurut saya, Rotativa semudah mungkin diterapkan.


  1. Anda sudah memiliki laporan HTML siap pakai dalam bentuk templat dan controller ASP.NET MVC, seperti ini:

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

  1. Instal paket Rotugva nuget


  2. Tambahkan pengontrol baru untuk laporan PDF



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

Pada dasarnya mulai sekarang, Anda memiliki PDF yang dikembalikan sebagai file yang berisi semua data dari laporan HTML asli.


Saya tidak menjelaskan perutean di sini, tetapi dipahami bahwa Anda telah mengkonfigurasi rute untuk memanggil kedua pengontrol dengan benar


Menariknya, perpustakaan ini sendiri pada dasarnya adalah pembungkus atas utilitas konsol terkenal wkhtmltopdf . Kecepatan bekerja di ketinggian, Anda bisa bertaruh pada Azure - akan bekerja. Tetapi ada fitur yang akan kita bicarakan.


Nomor Halaman


Adalah logis untuk berasumsi bahwa pelanggan akan mencetak PDF dan ingin melihat nomor halaman. Semuanya sangat sederhana di sini, terima kasih kepada pencipta Rotativa.


Menurut dokumentasi Rotativa, melalui parameter CustomSwitches , CustomSwitches dapat menentukan argumen yang akan diteruskan ke utilitas wkhtmltopdf . Nah, tips online banyak sekali dengan contoh. Panggilan berikut menambahkan nomor di bagian bawah setiap halaman:


 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 }; 

Ini bekerja dengan baik. Nomor halaman itu sendiri dilewatkan menggunakan parameter [page] , parameter semacam ini akan diganti dengan nilai tertentu.


Selain [page] ada yang lain:
  • [halaman] Digantikan dengan jumlah halaman yang sedang dicetak
  • [frompage] Diganti dengan nomor halaman pertama yang akan dicetak
  • [topage] Diganti dengan nomor halaman terakhir yang akan dicetak
  • [halaman web] Digantikan oleh URL halaman yang sedang dicetak
  • [bagian] Digantikan dengan nama bagian saat ini
  • [ayat] Digantikan dengan nama ayat saat ini
  • [date] Digantikan oleh tanggal saat ini dalam format lokal sistem
  • [isodate] Digantikan oleh tanggal saat ini dalam format diperpanjang ISO 8601
  • [waktu] Diganti oleh waktu saat ini dalam format lokal sistem
  • [title] Digantikan dengan judul objek halaman saat ini
  • [doctitle] Digantikan oleh judul dokumen keluaran
  • [sitepage] Digantikan oleh jumlah halaman di situs saat ini sedang dikonversi
  • [sitepages] Digantikan oleh jumlah halaman di situs saat ini sedang dikonversi


Daftar isi


Laporan multi-halaman besar membutuhkan konten PDF dan navigasi halaman. Ini sangat mudah dan sangat penting ketika jumlah halaman dalam laporan melebihi seratus.


Manual wkhtmltopdf berisi daftar lengkap semua opsi, di antaranya adalah --toc . Melihat parameter ini, utilitas pada dasarnya mengumpulkan semua tag <h1>, <h2>, ... <h6> dalam dokumen dan menghasilkan daftar isi berdasarkan pada mereka. Oleh karena itu, perlu untuk menyediakan penggunaan tag header ini dengan benar dalam template HTML Anda.


Namun pada kenyataannya, menambahkan --toc tidak menyebabkan konsekuensi apa pun. Seolah tidak ada parameter. Namun, opsi lain berfungsi. Berkat posting di beberapa forum, saya menemukan bahwa parameter ini harus dilewatkan tanpa tanda hubung: toc . Memang, dalam hal ini, konten ditambahkan sebagai halaman pertama. Ketika Anda mengklik pada baris dalam konten, Anda pergi ke halaman yang diinginkan dalam dokumen, nomor halaman sudah benar.


Belum sepenuhnya jelas cara mengatur gaya, tapi saya belum melakukannya.


Eksekusi Javascript


Poin berikutnya yang saya temui adalah kebutuhan untuk menambahkan grafik ke dalam laporan. Halaman HTML saya berisi kode JS yang menambahkan gambar menggunakan perpustakaan dc.js Berikut ini sebuah contoh:


 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(); } 

Pada saat yang sama, dalam HTML, saya memiliki elemen yang sesuai:


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

Agar kode ini berfungsi, Anda perlu mengimpor perpustakaan yang sesuai: dc.js , d3.js , crossfilter.js . Panggilan ke fungsi initChart akan membuat grafik dan memasukkan hasilnya
svg ke item yang ditentukan di pohon.


Tetapi PDF tidak mengandung jejak grafik. Serta jejak eksekusi kode JavaScript lainnya sebelum rendering PDF. Memeriksa ini cukup mudah - cukup tambahkan kode dasar untuk membuat elemen <div> sederhana dengan teks, hanya untuk menguji fakta panggilan JavaScript.


Secara empiris, ternyata lokasi kode JS untuk wkhtmltopdf memainkan peran penting. Kode yang terletak di akhir <html> atau, katakanlah, di akhir <body> JS tidak akan dieksekusi. Tampaknya utilitas itu tidak memperhatikannya, atau tidak berharap bertemu dengannya di sana.


Tetapi kode di dalam <head> dijalankan. Jadi, saya sampai pada skema ketika kode JavaScript terletak setelah deklarasi gaya di dalam <head> , dan dipanggil oleh konstruksi biasa:


 <body onload="initCharts()"> 

Dalam hal ini, kode akan dieksekusi seperti yang diharapkan.


Keterbatasan JavaScript


Tapi tetap saja tidak ada grafik dalam output PDF. Kemudian saya mulai menebak bahwa karena bukan peramban lengkap, rendering pdf dan mesin eksekusi kemungkinan besar tidak sempurna dan tidak mengerti aturan terakhir. Sekali lagi, secara eksperimen, saya menemukan bahwa fungsi panah tidak dirasakan. Selain itu, jika penerjemah menemukan sesuatu yang tidak dikenal untuknya, maka itu hanya berhenti bekerja.


Mengganti fungsi panah dari form x => x.value dengan function(x) { return x.value; } lebih klasik function(x) { return x.value; } function(x) { return x.value; } membantu dan semua kode dieksekusi, grafik yang dihasilkan masuk ke file PDF.


Lebar bagan


Secara empiris, ternyata perlu menunjukkan dengan jelas lebar elemen induk grafik. Untuk ini, saya menentukan gaya dc-chart . Berisi lebar grafik dalam piksel. Kalau tidak, bagan pada PDF akan sangat kecil, meskipun pada kenyataannya dalam versi HTML itu akan menempati seluruh lebar. Menentukan lebar persentase hanya akan berfungsi untuk HTML.


JavaScript sebaris / CSS


Sebagai kesimpulan, saya ingin mencatat bahwa banyak perpustakaan yang mengkonversi HTML ke PDF menerima baseUrl tertentu sebagai parameter. Ini adalah URL yang akan digunakan konverter untuk menyelesaikan jalur relatif untuk menerima gaya CSS impor, file JavaScirpt, atau font. Saya tidak bisa mengatakan dengan pasti bagaimana ini bekerja di Rotativa, tetapi saya datang dengan pendekatan yang berbeda.


Untuk mempercepat pemuatan awal laporan dan menghilangkan sumber masalah saat menyematkan skrip atau file gaya selama konversi, saya menyematkan JS dan CSS yang diperlukan langsung ke badan template HTML.


Untuk melakukan ini, buat bundel yang sesuai:


 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") ); } } 

Tambahkan panggilan konfigurasi untuk bundel ini ke Global.asax.cs


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

Dan tambahkan metode yang sesuai untuk menanamkan kode di halaman. Itu perlu ditempatkan di namespace yang sama dengan Global.asax.cs sehingga metode ini dapat dipanggil dari templat 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; } } 

Nah, sentuhan terakhir adalah panggilan dari templat:


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

Akibatnya, semua CSS dan JavaScript yang diperlukan akan langsung dalam HTML, meskipun selama pengembangan Anda dapat bekerja dengan file individual.


Kemungkinan besar, banyak orang akan langsung berpikir tentang ketidakefisienan pendekatan ini dalam hal permintaan caching oleh browser. Tetapi saya memiliki dua tujuan khusus:


  • sehingga konverter PDF tidak harus membuat permintaan di suatu tempat untuk gaya atau kode, dan pengguna harus menunggu masing-masing;
  • sehingga pengunduhan pertama laporan PDF dan HTML membutuhkan waktu minimal, tanpa perlu menunggu beberapa permintaan tambahan. Dalam konteks proyek saya, ini penting;

Istirahat halaman


Penataan laporan menjadi beberapa bagian dapat disertai dengan persyaratan untuk memulai bagian baru dari halaman baru. Dalam hal ini, Anda dapat berhasil menggunakan pendekatan CSS sederhana:


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

Utilitas wkhtmltopdf berhasil membaca kelas-kelas ini dan memahami bahwa perlu untuk memulai halaman baru. Kelas pertama - page-break-before - memberi tahu utilitas untuk selalu memulai halaman baru dengan elemen ini. Kelas kedua - no-page-break-inside - harus diterapkan pada elemen-elemen yang diinginkan untuk selengkap mungkin pada halaman. Misalnya, Anda memiliki blok informasi terstruktur berturut-turut, atau katakan tabel. Jika dua blok cocok pada halaman - mereka akan ditemukan. Jika yang ketiga tidak sesuai dengan halaman, itu tidak akan menjadi yang berikutnya. Jika lebih besar dari satu halaman, maka transfernya sudah tidak terhindarkan. Semua ini berfungsi dengan baik dan nyaman.


Perilaku Flex di wkhtmltopdf


Nah, fitur terakhir yang saya perhatikan terkait dengan penggunaan gaya markup flexbox. Kita semua menjadi terbiasa dengan mereka dan hampir semua markup dibuat oleh flexes. Namun, wkhtmltopdf sedikit ketinggalan dalam hal ini. Opsi flex horisontal tidak berfungsi (setidaknya dalam kasus saya ini tidak berhasil. Saya melihat di jaringan menyebutkan bahwa ada baiknya menduplikasi gaya flex sebagai berikut:


 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; 

Namun sayangnya ini tidak mengarah pada markup yang diharapkan dalam PDF. Saya harus mengulang tata letak beberapa elemen sehingga penempatan horisontal blok sesuai dengan persyaratan. Jika ada yang punya pengalaman sukses mengintegrasikan flex untuk wkhtmltopdf, silakan bagikan. Itu akan sangat membantu.


Beberapa tautan:


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


All Articles