summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastian Reichel <sre@ring0.de>2013-03-15 22:47:33 +0100
committerSebastian Reichel <sre@ring0.de>2013-03-15 22:47:33 +0100
commit33ad66dfe716745547a8762b0d20260caf4a1d63 (patch)
tree29f991d3efa353c8aa0c094969ef31f3da565568
parent9be077996417f12a25c05b04e03c7479f843be6f (diff)
downloadserial-barcode-scanner-33ad66dfe716745547a8762b0d20260caf4a1d63.tar.bz2
new invoice PDF generation tool
The new tool is written in vala and uses cairo and pango to generate the invoice PDF. Thus a full-blown latex install is no longer needed. The new tool is also much fast than generating the invoice with latex (ca. 10x speed improvement) and generates files, which have only half of the size pdflatex produces.
-rw-r--r--src/pdf-invoice/Makefile9
-rw-r--r--src/pdf-invoice/pdf-invoice.vala674
2 files changed, 683 insertions, 0 deletions
diff --git a/src/pdf-invoice/Makefile b/src/pdf-invoice/Makefile
new file mode 100644
index 0000000..2a5d2a4
--- /dev/null
+++ b/src/pdf-invoice/Makefile
@@ -0,0 +1,9 @@
+all: pdf-invoice
+
+pdf-invoice: pdf-invoice.vala ../price.vapi
+ valac --pkg pangocairo --pkg posix --pkg gio-2.0 $^
+
+clean:
+ rm -rf pdf-invoice
+
+.PHONY: all clean
diff --git a/src/pdf-invoice/pdf-invoice.vala b/src/pdf-invoice/pdf-invoice.vala
new file mode 100644
index 0000000..6beb5e6
--- /dev/null
+++ b/src/pdf-invoice/pdf-invoice.vala
@@ -0,0 +1,674 @@
+[DBus (name = "io.mainframe.shopsystem.InvoicePDFError")]
+public errordomain InvoicePDFError {
+ /* missing invoice data */
+ NO_INVOICE_DATA,
+ NO_INVOICE_DATE,
+ NO_INVOICE_ID,
+ NO_INVOICE_RECIPIENT,
+
+ /* data not supported by renderer */
+ ARTICLE_NAME_TOO_LONG,
+ PRICE_TOO_HIGH,
+ TOO_FAR_IN_THE_FUTURE
+}
+
+public struct InvoiceRecipient {
+ public string firstname;
+ public string lastname;
+ public string street;
+ public string postal_code;
+ public string city;
+ public string gender;
+}
+
+public struct InvoiceEntry {
+ int timestamp;
+ string article;
+ Price price;
+}
+
+[DBus (name = "io.mainframe.shopsystem.InvoicePDF")]
+public class InvoicePDF {
+ /* A4 sizes (in points, 72 DPI) */
+ private const double width = 595.27559; /* 210mm */
+ private const double height = 841.88976; /* 297mm */
+
+ /* invoice content, which should appear in the PDF */
+ public string invoice_id { set; get; }
+ public int64 invoice_date { set; get; }
+ public InvoiceRecipient invoice_recipient { set; get; }
+ public InvoiceEntry[] invoice_entries { set; get; }
+
+ /* pdf data */
+ private uint8[] data;
+ private uint useddatalength;
+
+ /* internal helper */
+ private DateTime previous_tm;
+
+ private const string[] calendermonths = {
+ "Januar",
+ "Februar",
+ "März",
+ "April",
+ "Mai",
+ "Juni",
+ "Juli",
+ "August",
+ "September",
+ "Oktober",
+ "November",
+ "Dezember"
+ };
+
+ public InvoicePDF() {
+ }
+
+ private void draw_footer(Cairo.Context ctx) {
+ /* TODO: get path from config file, support svg */
+ var footer = new Cairo.ImageSurface.from_png("../../invoice/footer-line.png");
+ ctx.set_source_surface(footer, 0, 817);
+ ctx.paint();
+ }
+
+ private void draw_logo(Cairo.Context ctx) {
+ /* TODO: get path from config file, support svg */
+ var logo = new Cairo.ImageSurface.from_png("../../invoice/logo.png");
+
+ var pattern = new Cairo.Pattern.for_surface(logo);
+ Cairo.Matrix scaler;
+ pattern.get_matrix(out scaler);
+ scaler.scale(1.41,1.41);
+ scaler.translate(-364.5,-22.5);
+ pattern.set_matrix(scaler);
+ pattern.set_filter(Cairo.Filter.BEST);
+
+ ctx.set_source(pattern);
+ ctx.paint();
+ }
+
+ private void draw_address(Cairo.Context ctx) {
+ ctx.save();
+ ctx.set_source_rgb(0, 0, 0);
+ ctx.set_line_width(1.0);
+
+ /* upper fold mark (20 mm left, 85 mm width, 51.5 mm top) */
+ ctx.move_to(56.69, 146);
+ ctx.line_to(297.59, 146);
+ ctx.stroke();
+
+ /* actually LMSans8 */
+ ctx.set_source_rgb(0, 0, 0);
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ ctx.set_font_size(8.45);
+
+ ctx.move_to(56.5, 142);
+ /* TODO: get string from config file */
+ ctx.show_text("Kreativität trifft Technik e.V., Binsenstraße 3, 26129 Oldenburg");
+
+ /* actually LMRoman12 */
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ ctx.set_font_size(12.3);
+
+ ctx.move_to(56.5, 184);
+ ctx.show_text(invoice_recipient.firstname + " " + invoice_recipient.lastname);
+
+ ctx.move_to(56.5, 198);
+ ctx.show_text(invoice_recipient.street);
+
+ /* actually LMRoman12 */
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
+ ctx.move_to(56.5, 227);
+ ctx.show_text(invoice_recipient.postal_code + " " + invoice_recipient.city);
+
+ ctx.restore();
+ }
+
+ private void draw_folding_marks(Cairo.Context ctx) {
+ ctx.save();
+ ctx.set_source_rgb(0, 0, 0);
+ ctx.set_line_width(1.0);
+
+ /* upper fold mark (105 mm) */
+ ctx.move_to(10, 297.65);
+ ctx.line_to(15, 297.65);
+ ctx.stroke();
+
+ /* middle fold mark (148.5 mm)*/
+ ctx.move_to(10, 420.912);
+ ctx.line_to(23, 420.912);
+ ctx.stroke();
+
+ /* lower fold mark (210 mm)*/
+ ctx.move_to(10, 595.3);
+ ctx.line_to(15, 595.3);
+ ctx.stroke();
+
+ ctx.restore();
+ }
+
+ private void draw_date(Cairo.Context ctx) {
+ ctx.save();
+ ctx.move_to(56.5, 280.0);
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMSans10");
+ font.set_size((int) 9.0 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* right alignment */
+ layout.set_alignment(Pango.Alignment.RIGHT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set page width */
+ layout.set_width((int) 446 * Pango.SCALE);
+
+ /* write invoice date */
+ var invdate = new DateTime.from_unix_local(invoice_date);
+ var day = "%d".printf(invdate.get_day_of_month());
+ var month = invdate.get_month();
+ var year = "%d".printf(invdate.get_year());
+ var date = day + ". " + calendermonths[month-1] + " " + year;
+ layout.set_text(date, date.length);
+
+ /* render text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ ctx.restore();
+ }
+
+ private void draw_title(Cairo.Context ctx) {
+ ctx.save();
+
+ /* actually LMRoman12 */
+ ctx.set_source_rgb(0, 0, 0);
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
+ ctx.set_font_size(12.9);
+
+ ctx.move_to(56.5, 323);
+
+ /* TODO: get text from config file */
+ ctx.show_text(@"Rechnung Nr. $invoice_id");
+
+ ctx.restore();
+ }
+
+ private void draw_footer_text_left(Cairo.Context ctx) {
+ ctx.save();
+ ctx.move_to(64.0, 754.5);
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMRoman8");
+ font.set_size((int) 6.0 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* left alignment */
+ layout.set_alignment(Pango.Alignment.LEFT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set line spacing */
+ layout.set_spacing((int) (-2.0 * Pango.SCALE));
+
+ /* set page width */
+ layout.set_width((int) 140 * Pango.SCALE);
+
+ /* TODO: get text from config file */
+ var text = "<b>Kreativität trifft Technik e.V.</b>\nBinsenstraße 3\n26129 Oldenburg\n\n<b>Amtsgericht Oldenburg</b>\nVR 201044";
+
+ /* write invoice date */
+ layout.set_markup(text, text.length);
+
+ /* render text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ ctx.restore();
+ }
+
+ private void draw_footer_text_middle(Cairo.Context ctx) {
+ ctx.save();
+ ctx.move_to(216.5, 754.5);
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMRoman8");
+ font.set_size((int) 6.0 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* left alignment */
+ layout.set_alignment(Pango.Alignment.LEFT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set line spacing */
+ layout.set_spacing((int) (-2.0 * Pango.SCALE));
+
+ /* set page width */
+ layout.set_width((int) 195 * Pango.SCALE);
+
+ /* TODO: get text from config file */
+ var text = "<b>Mail:</b> vorstand@kreativitaet-trifft-technik.de\n<b>Web:</b> www.kreativitaet-trifft-technik.de\n<b>Twitter:</b> @KtT_OL\n\n<b>Vorstand</b>\nPatrick Günther, Martin Hilscher, Holger Cremer";
+
+ /* write invoice date */
+ layout.set_markup(text, text.length);
+
+ /* render text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ ctx.restore();
+ }
+
+ private void draw_footer_text_right(Cairo.Context ctx) {
+ ctx.save();
+ ctx.move_to(424.0, 754.5);
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMRoman8");
+ font.set_size((int) 6.0 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* left alignment */
+ layout.set_alignment(Pango.Alignment.LEFT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set line spacing */
+ layout.set_spacing((int) (-2.0 * Pango.SCALE));
+
+ /* set page width */
+ layout.set_width((int) 150 * Pango.SCALE);
+
+ /* TODO: get text from config file */
+ var text = "<b>Raiffeisenbank Oldenburg</b>\n<b>Kontonummer:</b> 370 18 500\n<b>Bankleitzahl:</b> 280 602 28\n\n<b>Finanzamt Oldenburg</b>\n<b>Steuer Nr.:</b> 64/220/18413";
+
+ /* write invoice date */
+ layout.set_markup(text, text.length);
+
+ /* render text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ ctx.restore();
+ }
+
+ private Price get_sum() {
+ Price sum = 0;
+ foreach(var e in invoice_entries) {
+ sum += e.price;
+ }
+ return sum;
+ }
+
+ private string get_address() {
+ string address;
+ switch(invoice_recipient.gender) {
+ case "masculinum":
+ address = "Sehr geehrter Herr";
+ break;
+ case "femininum":
+ address = "Sehr geehrte Frau";
+ break;
+ default:
+ address = "Moin";
+ break;
+ }
+
+ return address;
+ }
+
+ private void draw_first_page_text(Cairo.Context ctx) {
+ ctx.save();
+ ctx.move_to(56.5, 352.5);
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMRoman12");
+ font.set_size((int) 9.0 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* left alignment */
+ layout.set_alignment(Pango.Alignment.LEFT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set line spacing */
+ layout.set_spacing((int) (-2.1 * Pango.SCALE));
+
+ /* set page width */
+ layout.set_width((int) 446 * Pango.SCALE);
+
+ string address = get_address();
+ Price sum = get_sum();
+
+ /* load text template */
+ try {
+ var text = "";
+ FileUtils.get_contents("template.txt", out text);
+ text = text.replace("{{{ADDRESS}}}", address);
+ text = text.replace("{{{LASTNAME}}}", invoice_recipient.lastname);
+ text = text.replace("{{{SUM}}}", @"$sum");
+ layout.set_markup(text, text.length);
+ } catch(GLib.FileError e) {
+ error("File Error: %s\n", e.message);
+ }
+
+ /* render text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ ctx.restore();
+ }
+
+ private void draw_invoice_table_header(Cairo.Context ctx) {
+ ctx.save();
+
+ /* border & font color */
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* line width of the border */
+ ctx.set_line_width(0.8);
+
+ /* header font */
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.BOLD);
+ ctx.set_font_size(12);
+
+ /* borders */
+ ctx.move_to(58, 50);
+ ctx.line_to(530, 50);
+ ctx.line_to(530, 65);
+ ctx.line_to(58, 65);
+ ctx.line_to(58, 50);
+ ctx.move_to(120, 50);
+ ctx.line_to(120, 65);
+ ctx.move_to(180, 50);
+ ctx.line_to(180, 65);
+ ctx.move_to(480, 50);
+ ctx.line_to(480, 65);
+ ctx.stroke();
+
+ /* header text */
+ ctx.move_to(62, 61.5);
+ ctx.show_text("Datum");
+ ctx.move_to(124, 61.5);
+ ctx.show_text("Uhrzeit");
+ ctx.move_to(184, 61.5);
+ ctx.show_text("Artikel");
+ ctx.move_to(484, 61.5);
+ ctx.show_text("Preis");
+
+ ctx.restore();
+ }
+
+ private void draw_invoice_table_footer(Cairo.Context ctx, double y) {
+ ctx.save();
+
+ /* border & font color */
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* line width of the border */
+ ctx.set_line_width(0.8);
+
+ /* end of table is just a line */
+ ctx.move_to(58, y);
+ ctx.line_to(530, y);
+ ctx.stroke();
+
+ ctx.restore();
+ }
+
+ private bool draw_invoice_table_entry(Cairo.Context ctx, double y, InvoiceEntry e, out double newy) throws InvoicePDFError {
+ ctx.save();
+
+ /* border & font color */
+ ctx.set_source_rgb(0, 0, 0);
+
+ /* y remains the same by default */
+ newy = y;
+
+ /* generate strings for InvoiceEntry */
+ var tm = new DateTime.from_unix_local(e.timestamp);
+ var date = tm.format("%Y-%m-%d");
+ var time = tm.format("%H:%M:%S");
+ var article = e.article;
+ var price = @"$(e.price)€".replace(".", ",");
+
+ if(e.price > 999999) {
+ throw new InvoicePDFError.PRICE_TOO_HIGH("Prices > 9999.99€ are not supported!");
+ }
+
+ if(tm.get_year() > 9999) {
+ throw new InvoicePDFError.TOO_FAR_IN_THE_FUTURE("Years after 9999 are not supported!");
+ }
+
+ /* if date remains the same do not add it again */
+ if(previous_tm != null &&
+ previous_tm.get_year() == tm.get_year() &&
+ previous_tm.get_month() == tm.get_month() &&
+ previous_tm.get_day_of_month() == tm.get_day_of_month()) {
+ date = "";
+ }
+
+ /* move to position for article text */
+ ctx.move_to(184, y);
+
+ /* get pango layout */
+ var layout = Pango.cairo_create_layout(ctx);
+
+ /* setup font */
+ var font = new Pango.FontDescription();
+ font.set_family("LMSans10");
+ font.set_size((int) 8 * Pango.SCALE);
+ layout.set_font_description(font);
+
+ /* left alignment */
+ layout.set_alignment(Pango.Alignment.LEFT);
+ layout.set_wrap(Pango.WrapMode.WORD_CHAR);
+
+ /* set line spacing */
+ layout.set_spacing((int) (-2.0 * Pango.SCALE));
+
+ /* set page width */
+ layout.set_width((int) 290 * Pango.SCALE);
+
+ /* write invoice date */
+ layout.set_text(article, article.length);
+
+ /* get height of text */
+ int w,h;
+ layout.get_size(out w, out h);
+ double height = h/Pango.SCALE;
+
+ /* verify that the text fits on the page */
+ if(750 < y + height)
+ return false;
+
+ /* render article text */
+ Pango.cairo_update_layout(ctx, layout);
+ Pango.cairo_show_layout(ctx, layout);
+
+ /* render date, time (toy font api uses different y than pango) */
+ ctx.select_font_face("LMSans10", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
+ ctx.set_font_size(11);
+ ctx.move_to(62, y+12.0);
+ ctx.show_text(date);
+ ctx.move_to(124, y+12.0);
+ ctx.show_text(time);
+
+ /* render price */
+ ctx.move_to(484, y);
+ var pricelayout = Pango.cairo_create_layout(ctx);
+ pricelayout.set_font_description(font);
+ pricelayout.set_alignment(Pango.Alignment.RIGHT);
+ pricelayout.set_width((int) 42 * Pango.SCALE);
+ pricelayout.set_text(price, price.length);
+ Pango.cairo_update_layout(ctx, pricelayout);
+ Pango.cairo_show_layout(ctx, pricelayout);
+
+ /* add borders */
+ ctx.set_line_width(0.8);
+ ctx.move_to(58, y);
+ ctx.line_to(58, y+height);
+ ctx.move_to(120, y);
+ ctx.line_to(120, y+height);
+ ctx.move_to(180, y);
+ ctx.line_to(180, y+height);
+ ctx.move_to(480, y);
+ ctx.line_to(480, y+height);
+ ctx.move_to(530, y);
+ ctx.line_to(530, y+height);
+ ctx.stroke();
+
+ ctx.restore();
+
+ newy += height;
+ previous_tm = tm;
+
+ return true;
+ }
+
+ private void draw_invoice_table(Cairo.Context ctx) throws InvoicePDFError {
+ ctx.save();
+
+ draw_footer(ctx);
+ draw_invoice_table_header(ctx);
+
+ /* initial position for entries */
+ double y = 65;
+
+ foreach(var entry in invoice_entries) {
+ if(!draw_invoice_table_entry(ctx, y, entry, out y)) {
+ /* entry could not be added, because end of page has been reached */
+ draw_invoice_table_footer(ctx, y);
+ ctx.show_page();
+
+ /* draw page footer & table header on new page */
+ draw_footer(ctx);
+ draw_invoice_table_header(ctx);
+
+ /* reset position */
+ y = 65;
+
+ /* always print date on new pages */
+ previous_tm = null;
+
+ /* retry adding the entry */
+ if(!draw_invoice_table_entry(ctx, y, entry, out y)) {
+ throw new InvoicePDFError.ARTICLE_NAME_TOO_LONG("Article name \"%s\" does not fit on a single page!", entry.article);
+ }
+ }
+ }
+
+ draw_invoice_table_footer(ctx, y);
+ ctx.show_page();
+
+ ctx.restore();
+ }
+
+ private Cairo.Status pdf_write(uchar[] newdata) {
+ if(data == null) {
+ data = newdata;
+ useddatalength = newdata.length;
+ } else {
+ if(useddatalength + newdata.length > data.length) {
+ uint8[] alldata = new uint8[data.length + newdata.length + 512];
+ Posix.memcpy(alldata, data, data.length);
+ data = alldata;
+ }
+
+ Posix.memcpy((uint8*) data + useddatalength, newdata, newdata.length);
+ useddatalength += newdata.length;
+ }
+
+ return Cairo.Status.SUCCESS;
+ }
+
+ public uint8[] generate() throws InvoicePDFError {
+ var document = new Cairo.PdfSurface.for_stream(pdf_write, width, height);
+
+ var ctx = new Cairo.Context(document);
+
+ if(invoice_id == "")
+ throw new InvoicePDFError.NO_INVOICE_ID("No invoice ID given!");
+
+ if(invoice_entries == null)
+ throw new InvoicePDFError.NO_INVOICE_DATA("No invoice data given!");
+
+ if(invoice_date == 0)
+ throw new InvoicePDFError.NO_INVOICE_DATE("No invoice date given!");
+
+ if(invoice_recipient.firstname == "" || invoice_recipient.lastname == "")
+ throw new InvoicePDFError.NO_INVOICE_RECIPIENT("No invoice recipient given!");
+
+ /* first page */
+ draw_logo(ctx);
+ draw_address(ctx);
+ draw_folding_marks(ctx);
+ draw_footer(ctx);
+ draw_footer_text_left(ctx);
+ draw_footer_text_middle(ctx);
+ draw_footer_text_right(ctx);
+ draw_date(ctx);
+ draw_title(ctx);
+ draw_first_page_text(ctx);
+ ctx.show_page();
+
+ /* following pages: invoice table */
+ draw_invoice_table(ctx);
+
+ document.finish();
+ document.flush();
+
+ return data;
+ }
+
+ public void clear() {
+ invoice_date = 0;
+ invoice_id = "";
+ invoice_recipient = {};
+ invoice_entries = null;
+ }
+}
+
+public static int main(string[] args) {
+ Bus.own_name(
+ BusType.SESSION,
+ "io.mainframe.shopsystem.InvoicePDF",
+ BusNameOwnerFlags.NONE,
+ on_bus_aquired,
+ () => {},
+ () => stderr.printf ("Could not aquire name\n"));
+
+ new MainLoop ().run ();
+
+ return 0;
+}
+
+void on_bus_aquired(DBusConnection conn) {
+ try {
+ conn.register_object ("/io/mainframe/invoicepdf", new InvoicePDF());
+ } catch (IOError e) {
+ stderr.printf ("Could not register service\n");
+ }
+}