/* Copyright 2013, Sebastian Reichel * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ [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 = "Kreativität trifft Technik e.V.\nBinsenstraße 3\n26129 Oldenburg\n\nAmtsgericht Oldenburg\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 = "Mail: vorstand@kreativitaet-trifft-technik.de\nWeb: www.kreativitaet-trifft-technik.de\nTwitter: @KtT_OL\n\nVorstand\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 = "Raiffeisenbank Oldenburg\nKontonummer: 370 18 500\nBankleitzahl: 280 602 28\n\nFinanzamt Oldenburg\nSteuer Nr.: 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"); } }