summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-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");
+ }
+}