diff --git a/LICENSE b/LICENSE index 7511f3e..689d036 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 - 2024, Přemysl Eric Janouch +Copyright (c) 2017 - 2025, Přemysl Eric Janouch Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/README.adoc b/README.adoc index 10e581f..674594f 100644 --- a/README.adoc +++ b/README.adoc @@ -33,6 +33,8 @@ Runtime dependencies: libcrypto (OpenSSL 1.1 API) $ cd builddir $ ninja +Go +~~ In addition to the C++ version, also included is a native Go port, which has enhanced PDF 1.5 support: @@ -56,6 +58,15 @@ Type=^PDF Open=%cd %p/extfs-pdf:// ---- +Lua PDF generator +~~~~~~~~~~~~~~~~~ +Build dependencies: Meson, a C++17 compiler, pkg-config + +Runtime dependencies: C++ Lua >= 5.3 (custom Meson wrap fallback), + cairo >= 1.15.4, pangocairo, libqrencode + +This is a parasitic subproject located in the _lpg_ subdirectory. +It will generate its own documentation. + Contributing and Support ------------------------ Use https://git.janouch.name/p/pdf-simple-sign to report bugs, request features, diff --git a/lpg/.clang-format b/lpg/.clang-format new file mode 100644 index 0000000..339b7e1 --- /dev/null +++ b/lpg/.clang-format @@ -0,0 +1,9 @@ +BasedOnStyle: LLVM +ColumnLimit: 80 +IndentWidth: 4 +TabWidth: 4 +UseTab: ForContinuationAndIndentation +SpaceAfterCStyleCast: true +AlignAfterOpenBracket: DontAlign +AlignOperands: DontAlign +SpacesBeforeTrailingComments: 2 diff --git a/lpg/lpg.cpp b/lpg/lpg.cpp new file mode 100644 index 0000000..0301945 --- /dev/null +++ b/lpg/lpg.cpp @@ -0,0 +1,1138 @@ +// +// lpg: Lua PDF generator +// +// Copyright (c) 2017 - 2025, Přemysl Eric Janouch +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// 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. +// + +#include +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace std; +using attribute = variant; + +#define DefWidget(name) struct name : public Widget +struct Widget { + virtual ~Widget() {} + + unordered_map attributes; + using attribute_map = decltype(attributes); + Widget *setattr(string key, attribute_map::mapped_type value) { + attributes.insert({key, value}); + return this; + } + + optional getattr(const string &name) { + if (auto it = attributes.find("_" + name); it != attributes.end()) + return {it->second}; + if (auto it = attributes.find(name); it != attributes.end()) + return {it->second}; + return {}; + } + + /// Top-down attribute propagation. + virtual void apply_attributes(const attribute_map &attrs = {}) { + for (const auto &kv : attrs) + if (*kv.first.c_str() != '_') + attributes.insert(kv); + } + + // We need CAIRO_ROUND_GLYPH_POS_OFF to be set in font options, + // which can only be done internally, se we have to pass a context that + // is based on an actual PDF surface. + // + // Font maps also need some kind of a backend, like Cairo. + + /// Compute and return space required for the widget's contents. + virtual tuple prepare([[maybe_unused]] PangoContext *pc) { + return {0, 0}; + } + + /// Compute and return space required for the widget's contents, + /// given a fixed size (favouring any dimension). + virtual tuple prepare_for_size(PangoContext *pc, + [[maybe_unused]] double width, [[maybe_unused]] double height) { + return prepare(pc); + } + + /// Render to the context within the designated space, no clipping. + virtual void render([[maybe_unused]] cairo_t *cr, [[maybe_unused]] double w, + [[maybe_unused]] double h) {} +}; + +/// Special container that basically just fucks with the system right now. +DefWidget(Frame) { + unique_ptr child; + Frame(Widget *w) : child(w) {} + + virtual void apply_attributes(const attribute_map &attrs) override { + Widget::apply_attributes(attrs); + child->apply_attributes(attributes); + } + + virtual tuple prepare(PangoContext *pc) override { + auto d = child->prepare(pc); + if (auto v = getattr("w_override")) + get<0>(d) = get(*v); + if (auto v = getattr("h_override")) + get<1>(d) = get(*v); + return d; + } + + virtual void render(cairo_t *cr, double w, double h) override { + cairo_save(cr); + + if (auto v = getattr("color")) { + int rgb = get(*v); + cairo_set_source_rgb(cr, ((rgb >> 16) & 0xFF) / 255., + ((rgb >> 8) & 0xFF) / 255., (rgb & 0xFF) / 255.); + } + + child->render(cr, w, h); + cairo_restore(cr); + } +}; + +#define DefContainer(name) struct name : public Container +DefWidget(Container) { + vector> children; + + inline void add() {} + template void add(Widget *w, Args && ...args) { + children.push_back(unique_ptr(w)); + add(args...); + } + + Container(vector> &&children) + : children(std::move(children)) {} + + virtual void apply_attributes(const attribute_map &attrs) override { + Widget::apply_attributes(attrs); + for (auto &i : children) + i->apply_attributes(attributes); + } +}; + +static void finalize_box(vector &sizes, double available) { + double fixed = 0, stretched = 0; + for (auto s : sizes) { + if (s >= 0) + fixed += s; + else + stretched += s; + } + if (stretched) { + auto factor = max(0., available - fixed) / stretched; + for (auto &s : sizes) + if (s < 0) + s *= factor; + } else { + // TODO(p): One should be able to *opt in* for this. + auto redistribute = max(0., available - fixed) / sizes.size(); + for (auto &s : sizes) + s += redistribute; + } +} + +DefContainer(HBox) { + HBox(vector> children = {}) + : Container(std::move(children)) {} + + vector widths; + virtual tuple prepare(PangoContext *pc) override { + double w = 0, h = 0; + widths.resize(children.size()); + for (size_t i = 0; i < children.size(); i++) { + auto d = children[i]->prepare(pc); + if ((widths[i] = get<0>(d)) > 0) + w += widths[i]; + h = max(h, get<1>(d)); + } + return {w, h}; + } + + virtual tuple prepare_for_size( + PangoContext *pc, double width, double height) override { + double w = 0, h = 0; + widths.resize(children.size()); + for (size_t i = 0; i < children.size(); i++) { + auto d = children[i]->prepare_for_size(pc, width, height); + if ((widths[i] = get<0>(d)) > 0) + w += widths[i]; + h = max(h, get<1>(d)); + } + return {w, h}; + } + + virtual void render(cairo_t *cr, double w, double h) override { + finalize_box(widths, w); + for (size_t i = 0; i < children.size(); i++) { + cairo_save(cr); + children[i]->render(cr, widths[i], h); + cairo_restore(cr); + cairo_translate(cr, widths[i], 0.); + } + } +}; + +DefContainer(VBox) { + VBox(vector> children = {}) + : Container(std::move(children)) {} + + vector heights; + virtual tuple prepare(PangoContext *pc) override { + double w = 0, h = 0; + heights.resize(children.size()); + for (size_t i = 0; i < children.size(); i++) { + auto d = children[i]->prepare(pc); + if ((heights[i] = get<1>(d)) > 0) + h += heights[i]; + w = max(w, get<0>(d)); + } + return {w, h}; + } + + virtual tuple prepare_for_size( + PangoContext *pc, double width, double height) override { + double w = 0, h = 0; + heights.resize(children.size()); + for (size_t i = 0; i < children.size(); i++) { + auto d = children[i]->prepare_for_size(pc, width, height); + if ((heights[i] = get<1>(d)) > 0) + h += heights[i]; + w = max(w, get<0>(d)); + } + return {w, h}; + } + + virtual void render(cairo_t *cr, double w, double h) override { + finalize_box(heights, h); + for (size_t i = 0; i < children.size(); i++) { + cairo_save(cr); + children[i]->render(cr, w, heights[i]); + cairo_restore(cr); + cairo_translate(cr, 0., heights[i]); + } + } +}; + +/// Fillers just take up space and don't render anything. +DefWidget(Filler) { + double w, h; + Filler(double w = -1, double h = -1) : w(w), h(h) {} + virtual tuple prepare( + [[maybe_unused]] PangoContext *pc) override { + return {w, h}; + } +}; + +DefWidget(HLine) { + double thickness; + HLine(double thickness = 1) : thickness(thickness) {} + virtual tuple prepare( + [[maybe_unused]] PangoContext *pc) override { + return {-1, thickness}; + } + virtual void render(cairo_t *cr, double w, double h) override { + cairo_move_to(cr, 0, h / 2); + cairo_line_to(cr, w, h / 2); + cairo_set_line_width(cr, thickness); + cairo_stroke(cr); + } +}; + +DefWidget(VLine) { + double thickness; + VLine(double thickness = 1) : thickness(thickness) {} + virtual tuple prepare( + [[maybe_unused]] PangoContext *pc) override { + return {thickness, -1}; + } + virtual void render(cairo_t *cr, double w, double h) override { + cairo_move_to(cr, w / 2, 0); + cairo_line_to(cr, w / 2, h); + cairo_set_line_width(cr, thickness); + cairo_stroke(cr); + } +}; + +DefWidget(Text) { + string text; + PangoLayout *layout = nullptr; + double y_offset = 0.; + + Text(string text = "") : text(text) {} + virtual ~Text() override { g_clear_object(&layout); } + + static string escape(const char *s, size_t len) { + auto escapechar = [](char c) -> const char * { + if (c == '<') return "<"; + if (c == '>') return ">"; + if (c == '&') return "&"; + return nullptr; + }; + string escaped; + for (size_t i = 0; i < len; i++) + if (auto entity = escapechar(s[i])) + escaped += entity; + else + escaped += s[i]; + return escaped; + } + + void prepare_layout(PangoContext *pc) { + g_clear_object(&layout); + layout = pango_layout_new(pc); + pango_layout_set_markup(layout, text.c_str(), -1); + pango_layout_set_alignment(layout, PANGO_ALIGN_LEFT); + + auto fd = pango_font_description_new(); + if (auto v = getattr("fontfamily")) + pango_font_description_set_family(fd, get(*v).c_str()); + if (auto v = getattr("fontsize")) + pango_font_description_set_size(fd, get(*v) * PANGO_SCALE); + if (auto v = getattr("fontweight")) + pango_font_description_set_weight(fd, PangoWeight(get(*v))); + + // We need this for the line-height calculation. + auto font_size = + double(pango_font_description_get_size(fd)) / PANGO_SCALE; + if (!font_size) + pango_font_description_set_size(fd, (font_size = 10)); + + // Supposedly this is how this shit works. + // XXX: This will never work if the markup changes the font size. + if (auto v = getattr("lineheight")) { + auto increment = get(*v) - 1; + y_offset = increment * font_size / 2; + pango_layout_set_spacing( + layout, increment * font_size * PANGO_SCALE); + } + + // FIXME: We don't want to override what's in the markup. + pango_layout_set_font_description(layout, fd); + pango_font_description_free(fd); + } + + virtual tuple prepare(PangoContext *pc) override { + prepare_layout(pc); + + int w, h; + pango_layout_get_size(layout, &w, &h); + return { + double(w) / PANGO_SCALE, double(h) / PANGO_SCALE + 2 * y_offset}; + } + + virtual tuple prepare_for_size(PangoContext *pc, + double width, [[maybe_unused]] double height) override { + prepare_layout(pc); + + // It's difficult to get vertical text, so wrap horizontally. + pango_layout_set_width(layout, PANGO_SCALE * width); + + int w, h; + pango_layout_get_size(layout, &w, &h); + return { + double(w) / PANGO_SCALE, double(h) / PANGO_SCALE + 2 * y_offset}; + } + + virtual void render(cairo_t *cr, double w, [[maybe_unused]] double h) + override { + g_return_if_fail(layout); + // Assuming horizontal text, make it span the whole allocation. + pango_layout_set_width(layout, PANGO_SCALE * w); + pango_cairo_update_layout(cr, layout); + cairo_translate(cr, 0, y_offset); + pango_cairo_show_layout(cr, layout); + } +}; + +DefWidget(Link) { + string target_uri; + unique_ptr child; + + Link(const string &target_uri, Widget *w) + : target_uri(target_uri), child(w) {} + + virtual void apply_attributes(const attribute_map &attrs) override { + Widget::apply_attributes(attrs); + child->apply_attributes(attributes); + } + + virtual tuple prepare(PangoContext *pc) override { + return child->prepare(pc); + } + + virtual void render(cairo_t *cr, double w, double h) override { + cairo_save(cr); + cairo_tag_begin( + cr, CAIRO_TAG_LINK, ("uri='" + target_uri + "'").c_str()); + child->render(cr, w, h); + cairo_tag_end(cr, CAIRO_TAG_LINK); + cairo_restore(cr); + } +}; + +// --- Pictures ---------------------------------------------------------------- + +struct image_info { + double width = 0., height = 0., dpi_x = 72., dpi_y = 72.; +}; + +/// http://libpng.org/pub/png/spec/1.2/PNG-Contents.html +static bool read_png_info(image_info &info, const char *data, size_t length) { + return length >= 24 && !memcmp(data, "\211PNG\r\n\032\n", 8) && + !memcmp(data + 12, "IHDR", 4) && + (info.width = ntohl(*(uint32_t *) (data + 16))) && + (info.height = ntohl(*(uint32_t *) (data + 20))); +} + +DefWidget(Picture) { + double w = 0, h = 0; + double scale_x = 1., scale_y = 1.; + cairo_surface_t *surface = nullptr; + + virtual tuple prepare(PangoContext *) override { + return {w * scale_x, h * scale_y}; + } + + virtual void render(cairo_t *cr, double width, double height) override { + if (!surface || width <= 0 || height <= 0) + return; + + double ww = this->w * scale_x; + double hh = this->h * scale_y; + double postscale = width / ww; + if (hh * postscale > height) + postscale = height / hh; + + // For PDF-A, ISO 19005-3:2012 6.2.8: interpolation is not allowed + // (Cairo sets it on by default). + bool interpolate = true; + + auto pattern = cairo_pattern_create_for_surface(surface); + cairo_pattern_set_filter( + pattern, interpolate ? CAIRO_FILTER_GOOD : CAIRO_FILTER_NEAREST); + + // Maybe we should also center the picture or something... + cairo_scale(cr, scale_x * postscale, scale_y * postscale); + cairo_set_source(cr, pattern); + cairo_paint(cr); + + cairo_pattern_destroy(pattern); + } + + static cairo_surface_t *make_surface_png(const string &data) { + using CharRange = pair; + CharRange iterator{&*data.begin(), &*data.end()}; + return cairo_image_surface_create_from_png_stream( + [](void *closure, unsigned char *data, uint len) { + auto i = (CharRange *) closure; + if (i->second - i->first < len) + return CAIRO_STATUS_READ_ERROR; + + memcpy(data, i->first, len); + i->first += len; + return CAIRO_STATUS_SUCCESS; + }, + &iterator); + } + + // Cairo doesn't support PNGs in PDFs by MIME type, + // until then we'll have to parametrize. + static function identify( + const string &picture, image_info &info) { + if (read_png_info(info, picture.data(), picture.length())) + return bind(make_surface_png, picture); + return nullptr; + } + + Picture(const string &filename) { + ifstream t{filename}; + stringstream buffer; + buffer << t.rdbuf(); + string picture = buffer.str(); + + image_info info; + if (auto make_surface = identify(picture, info)) { + surface = make_surface(); + w = info.width; + h = info.height; + scale_x = info.dpi_x / 72.; + scale_y = info.dpi_y / 72.; + } else { + cerr << "warning: unreadable picture: " << filename << endl; + } + } +}; + +// --- QR ---------------------------------------------------------------------- + +DefWidget(QR) { + QRcode *code = nullptr; + double T = 1.; + + QR(string text, double T) : T(T) { + QRinput *data = QRinput_new2( + 0 /* Version, i.e., size, here autoselect */, + QR_ECLEVEL_M /* 15% correction */); + if (!data) + return; + + auto u8 = reinterpret_cast(text.data()); + (void) QRinput_append(data, !QRinput_check(QR_MODE_AN, text.size(), u8) + ? QR_MODE_AN : QR_MODE_8, text.size(), u8); + + code = QRcode_encodeInput(data); + QRinput_free(data); + } + + virtual ~QR() override { + if (code) + QRcode_free(code); + } + + virtual tuple prepare([[maybe_unused]] PangoContext *pc) + override { + if (!code) + return {0, 0}; + + return {T * code->width, T * code->width}; + } + + virtual void render(cairo_t *cr, + [[maybe_unused]] double w, [[maybe_unused]] double h) override { + if (!code) + return; + + auto line = code->data; + for (int y = 0; y < code->width; y++) { + for (int x = 0; x < code->width; x++) { + if (line[x] & 1) + cairo_rectangle(cr, T * x, T * y, T, T); + } + line += code->width; + } + cairo_fill(cr); + } +}; + +// --- Lua Widget -------------------------------------------------------------- + +#define XLUA_WIDGET_METATABLE "widget" + +struct LuaWidget { + // shared_ptr would resolve the reference stealing API design issue. + unique_ptr widget; +}; + +static void xlua_widget_check(lua_State *L, LuaWidget *self) { + if (!self->widget) + luaL_error(L, "trying to use a consumed widget reference"); +} + +static attribute xlua_widget_tovalue(lua_State *L, LuaWidget *self, int idx) { + xlua_widget_check(L, self); + if (lua_isnumber(L, idx)) + return lua_tonumber(L, idx); + if (lua_isstring(L, idx)) { + size_t len = 0; + const char *s = lua_tolstring(L, idx, &len); + return string(s, len); + } + luaL_error(L, "expected string or numeric attributes"); + return {}; +} + +static void xlua_widget_set( + lua_State *L, LuaWidget *self, Widget *widget, int idx_attrs) { + self->widget.reset(widget); + if (!idx_attrs) + return; + + lua_pushvalue(L, idx_attrs); + lua_pushnil(L); + while (lua_next(L, -2)) { + if (lua_type(L, -2) == LUA_TSTRING) { + size_t key_len = 0; + const char *key = lua_tolstring(L, -2, &key_len); + widget->setattr( + string(key, key_len), xlua_widget_tovalue(L, self, -1)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); +} + +static int xlua_widget_gc(lua_State *L) { + auto self = (LuaWidget *) luaL_checkudata(L, 1, XLUA_WIDGET_METATABLE); + self->widget.reset(nullptr); + return 0; +} + +static int xlua_widget_index(lua_State *L) { + auto self = (LuaWidget *) luaL_checkudata(L, 1, XLUA_WIDGET_METATABLE); + // In theory, this could also index container children, + // but it does not seem practically useful. + auto key = luaL_checkstring(L, 2); + xlua_widget_check(L, self); + + if (auto it = self->widget->attributes.find(key); + it == self->widget->attributes.end()) + lua_pushnil(L); + else if (auto s = get_if(&it->second)) + lua_pushlstring(L, s->c_str(), s->length()); + else if (auto n = get_if(&it->second)) + lua_pushnumber(L, *n); + return 1; +} + +static int xlua_widget_newindex(lua_State *L) { + auto self = (LuaWidget *) luaL_checkudata(L, 1, XLUA_WIDGET_METATABLE); + auto key = luaL_checkstring(L, 2); + xlua_widget_check(L, self); + + self->widget->attributes[key] = xlua_widget_tovalue(L, self, 3); + return 0; +} + +static luaL_Reg xlua_widget_table[] = { + {"__gc", xlua_widget_gc}, + {"__index", xlua_widget_index}, + {"__newindex", xlua_widget_newindex}, + {} +}; + +// --- Lua Document ------------------------------------------------------------ + +#define XLUA_DOCUMENT_METATABLE "document" + +struct LuaDocument { + cairo_t *cr = nullptr; ///< Cairo + cairo_surface_t *pdf = nullptr; ///< PDF surface + PangoContext *pc = nullptr; ///< Pango context + + double page_width = 0.; ///< Page width in 72 DPI points + double page_height = 0.; ///< Page height in 72 DPI points + double page_margin = 0.; ///< Page margins in 72 DPI points +}; + +static int xlua_document_gc(lua_State *L) { + auto self = (LuaDocument *) luaL_checkudata(L, 1, XLUA_DOCUMENT_METATABLE); + cairo_destroy(self->cr); + g_object_unref(self->pc); + return 0; +} + +static int xlua_document_index(lua_State *L) { + if (auto key = luaL_checkstring(L, 2); *key == '_') + lua_pushnil(L); + else + luaL_getmetafield(L, 1, key); + return 1; +} + +// And probably for links as well. +#if CAIRO_VERSION < CAIRO_VERSION_ENCODE(1, 15, 4) +#error "At least Cairo 1.15.4 is required for setting PDF metadata." +#endif + +static optional metadata_by_name(const char *name) { + if (!strcmp(name, "title")) + return CAIRO_PDF_METADATA_TITLE; + if (!strcmp(name, "author")) + return CAIRO_PDF_METADATA_AUTHOR; + if (!strcmp(name, "subject")) + return CAIRO_PDF_METADATA_SUBJECT; + if (!strcmp(name, "keywords")) + return CAIRO_PDF_METADATA_KEYWORDS; + if (!strcmp(name, "creator")) + return CAIRO_PDF_METADATA_CREATOR; + if (!strcmp(name, "create_date")) + return CAIRO_PDF_METADATA_CREATE_DATE; + if (!strcmp(name, "mod_date")) + return CAIRO_PDF_METADATA_MOD_DATE; + return {}; +} + +static int xlua_document_newindex(lua_State *L) { + auto self = (LuaDocument *) luaL_checkudata(L, 1, XLUA_DOCUMENT_METATABLE); + auto name = luaL_checkstring(L, 2); + auto value = luaL_checkstring(L, 3); + + // These are all read-only in Cairo. + if (auto id = metadata_by_name(name)) + cairo_pdf_surface_set_metadata(self->pdf, id.value(), value); + else + return luaL_error(L, "%s: unknown property"); + return 0; +} + +static int xlua_document_show(lua_State *L) { + auto self = (LuaDocument *) luaL_checkudata(L, 1, XLUA_DOCUMENT_METATABLE); + for (int i = 2; i <= lua_gettop(L); i++) { + auto w = (LuaWidget *) luaL_checkudata(L, i, XLUA_WIDGET_METATABLE); + xlua_widget_check(L, w); + auto widget = w->widget.get(); + widget->apply_attributes(); + + auto inner_width = self->page_width - 2 * self->page_margin; + auto inner_height = self->page_height - 2 * self->page_margin; + widget->prepare_for_size(self->pc, inner_width, inner_height); + + cairo_save(self->cr); + cairo_translate(self->cr, self->page_margin, self->page_margin); + widget->render(self->cr, inner_width, inner_height); + cairo_restore(self->cr); + } + cairo_show_page(self->cr); + return 0; +} + +static luaL_Reg xlua_document_table[] = { + {"__gc", xlua_document_gc}, + {"__index", xlua_document_index}, + {"__newindex", xlua_document_newindex}, + {"show", xlua_document_show}, + {} +}; + +// --- Library ----------------------------------------------------------------- + +// 1 point is 1/72 inch, also applies to PDF surfaces. +static int xlua_cm(lua_State *L) { + lua_pushnumber(L, luaL_checknumber(L, 1) / 2.54 * 72); + return 1; +} + +struct xlua_numpunct : public numpunct { + optional thousands_sep_override; + optional decimal_point_override; + optional grouping_override; + + using super = std::numpunct; + + virtual char do_thousands_sep() const override { + return thousands_sep_override.value_or(super::do_thousands_sep()); + } + + virtual char do_decimal_point() const override { + return decimal_point_override.value_or(super::do_decimal_point()); + } + + virtual string_type do_grouping() const override { + return grouping_override.value_or(super::do_grouping()); + } +}; + +static int xlua_ntoa(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + auto np = new xlua_numpunct(); + const char *field = nullptr; + if (lua_getfield(L, 1, (field = "thousands_sep")) != LUA_TNIL) { + size_t len = 0; + auto str = lua_tolstring(L, -1, &len); + if (!str || len != 1) + return luaL_error(L, "invalid %s", field); + np->thousands_sep_override.emplace(str[0]); + } + if (lua_getfield(L, 1, (field = "decimal_point")) != LUA_TNIL) { + size_t len = 0; + auto str = lua_tolstring(L, -1, &len); + if (!str || len != 1) + return luaL_error(L, "invalid %s", field); + np->decimal_point_override.emplace(str[0]); + } + if (lua_getfield(L, 1, (field = "grouping")) != LUA_TNIL) { + size_t len = 0; + auto str = lua_tolstring(L, -1, &len); + if (!str) + return luaL_error(L, "invalid %s", field); + np->grouping_override.emplace(string(str, len)); + } + + ostringstream formatted; + formatted.imbue(locale(locale(), np)); + if (lua_getfield(L, 1, "precision") != LUA_TNIL) { + formatted.setf(formatted.fixed, formatted.floatfield); + formatted.precision(lua_tointeger(L, -1)); + } + + lua_geti(L, 1, 1); + if (lua_isinteger(L, -1)) + formatted << lua_tointeger(L, -1); + else if (lua_isnumber(L, -1)) + formatted << lua_tonumber(L, -1); + else + return luaL_error(L, "number expected as the first field"); + + lua_pushstring(L, formatted.str().c_str()); + return 1; +} + +static int xlua_escape(lua_State *L) { + string escaped; + for (int i = 1; i <= lua_gettop(L); i++) { + size_t len = 0; + const char *s = luaL_checklstring(L, i, &len); + escaped.append(Text::escape(s, len)); + } + lua_pushlstring(L, escaped.data(), escaped.length()); + return 1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static int xlua_document(lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + lua_Number width = luaL_checknumber(L, 2); + lua_Number height = luaL_checknumber(L, 3); + + LuaDocument *self = + static_cast(lua_newuserdata(L, sizeof *self)); + luaL_setmetatable(L, XLUA_DOCUMENT_METATABLE); + new(self) LuaDocument; + + self->pdf = cairo_pdf_surface_create(filename, + (self->page_width = width), (self->page_height = height)); + self->cr = cairo_create(self->pdf); + cairo_surface_destroy(self->pdf); + + self->page_margin = luaL_optnumber(L, 4, self->page_margin); + + auto pc = self->pc = pango_cairo_create_context(self->cr); + // By default the resolution is set to 96 DPI but the PDF surface uses 72. + pango_cairo_context_set_resolution(pc, 72.); + +#if PANGO_VERSION_CHECK(1, 44, 0) + // Otherwise kerning was broken in Pango before 1.48.6. + // Seems like this issue: https://gitlab.gnome.org/GNOME/pango/-/issues/562 + // and might be related to: https://blogs.gnome.org/mclasen/2019/08/ + pango_context_set_round_glyph_positions(pc, FALSE); +#endif + return 1; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static LuaWidget *xlua_newwidget(lua_State *L) { + LuaWidget *self = + static_cast(lua_newuserdata(L, sizeof *self)); + luaL_setmetatable(L, XLUA_WIDGET_METATABLE); + new(self) LuaWidget; + return self; +} + +static int xlua_filler(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + double width = -1, height = -1; + if (lua_geti(L, 1, 1); !lua_isnoneornil(L, -1)) + width = lua_tonumber(L, -1); + if (lua_geti(L, 1, 2); !lua_isnoneornil(L, -1)) + height = lua_tonumber(L, -1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Filler{width, height}, 1); + return 1; +} + +static int xlua_hline(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + double thickness = 1; + if (lua_geti(L, 1, 1); !lua_isnoneornil(L, -1)) + thickness = lua_tonumber(L, -1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new HLine{thickness}, 1); + return 1; +} + +static int xlua_vline(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + double thickness = 1; + if (lua_geti(L, 1, 1); !lua_isnoneornil(L, -1)) + thickness = lua_tonumber(L, -1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new VLine{thickness}, 1); + return 1; +} + +static string xlua_tostring(lua_State *L, int idx) { + // Automatic conversions are unlikely to be valid XML. + bool escape = !lua_isstring(L, idx); + + size_t length = 0; + const char *s = luaL_tolstring(L, idx, &length); + string text = escape ? Text::escape(s, length) : string(s, length); + lua_pop(L, 1); + return text; +} + +static int xlua_text(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + string text; + for (lua_Integer i = 1, len = luaL_len(L, 1); i <= len; i++) { + lua_geti(L, 1, i); + text.append(xlua_tostring(L, -1)); + lua_pop(L, 1); + } + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Text{text}, 1); + return 1; +} + +static LuaWidget *xlua_towidget(lua_State *L) { + if (luaL_testudata(L, -1, XLUA_WIDGET_METATABLE)) + return (LuaWidget *) luaL_checkudata(L, -1, XLUA_WIDGET_METATABLE); + + string text = xlua_tostring(L, -1); + lua_pop(L, 1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Text{text}, 0); + return self; +} + +static int xlua_frame(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (luaL_len(L, 1) != 1) + return luaL_error(L, "expected one child widget"); + + lua_geti(L, 1, 1); + auto child = xlua_towidget(L); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Frame{child->widget.release()}, 1); + return 1; +} + +static int xlua_link(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (luaL_len(L, 1) != 2) + return luaL_error(L, "expected link target and one child widget"); + + lua_geti(L, 1, 1); + size_t length = 0; + const char *s = luaL_tolstring(L, -1, &length); + string target(s, length); + lua_pop(L, 1); + + lua_geti(L, 1, 2); + auto child = xlua_towidget(L); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Link{target, child->widget.release()}, 1); + return 1; +} + +static int xlua_hbox(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + vector> children; + for (lua_Integer i = 1, len = luaL_len(L, 1); i <= len; i++) { + lua_geti(L, 1, i); + children.emplace_back(xlua_towidget(L)->widget.release()); + lua_pop(L, 1); + } + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new HBox{std::move(children)}, 1); + return 1; +} + +static int xlua_vbox(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + + vector> children; + for (lua_Integer i = 1, len = luaL_len(L, 1); i <= len; i++) { + lua_geti(L, 1, i); + children.emplace_back(xlua_towidget(L)->widget.release()); + lua_pop(L, 1); + } + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new VBox{std::move(children)}, 1); + return 1; +} + +static int xlua_picture(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (luaL_len(L, 1) != 1) + return luaL_error(L, "expected picture path"); + + lua_geti(L, 1, 1); + size_t length = 0; + const char *s = luaL_tolstring(L, -1, &length); + string filename(s, length); + lua_pop(L, 1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new Picture{filename}, 1); + return 1; +} + +static int xlua_qr(lua_State *L) { + luaL_checktype(L, 1, LUA_TTABLE); + if (luaL_len(L, 1) != 2) + return luaL_error(L, "expected contents and module size"); + + lua_geti(L, 1, 1); + size_t length = 0; + const char *s = luaL_tolstring(L, -1, &length); + string target(s, length); + lua_pop(L, 1); + + lua_geti(L, 1, 2); + auto T = lua_tonumber(L, -1); + lua_pop(L, 1); + + auto self = xlua_newwidget(L); + xlua_widget_set(L, self, new QR{target, T}, 1); + return 1; +} + +static luaL_Reg xlua_library[] = { + {"cm", xlua_cm}, + {"ntoa", xlua_ntoa}, + {"escape", xlua_escape}, + + {"Document", xlua_document}, + + {"Filler", xlua_filler}, + {"HLine", xlua_hline}, + {"VLine", xlua_vline}, + {"Text", xlua_text}, + {"Frame", xlua_frame}, + {"Link", xlua_link}, + {"HBox", xlua_hbox}, + {"VBox", xlua_vbox}, + {"Picture", xlua_picture}, + {"QR", xlua_qr}, + {} +}; + +// --- Initialisation, event handling ------------------------------------------ + +static int xlua_error_handler(lua_State *L) { + // Don't add tracebacks when there's already one, and pass nil through. + const char *string = luaL_optstring(L, 1, NULL); + if (string && !strchr(string, '\n')) { + luaL_traceback(L, L, string, 1); + lua_remove(L, 1); + } + return 1; +} + +static void *xlua_alloc([[maybe_unused]] void *ud, void *ptr, + [[maybe_unused]] size_t o_size, size_t n_size) { + if (n_size) + return realloc(ptr, n_size); + + free(ptr); + return NULL; +} + +static int xlua_panic(lua_State *L) { + cerr << "fatal: Lua panicked: " << lua_tostring(L, -1) << endl; + lua_close(L); + exit(EXIT_FAILURE); + return 0; +} + +int main(int argc, char *argv[]) { + if (argc < 2) { + cerr << "Usage: " << argv[0] << " program.lua [args...]" << endl; + return 1; + } + + lua_State *L = lua_newstate(xlua_alloc, NULL); + if (!L) { + cerr << "fatal: Lua initialization failed" << endl; + return 1; + } + lua_atpanic(L, xlua_panic); + luaL_openlibs(L); + luaL_checkversion(L); + + luaL_newlib(L, xlua_library); + lua_setglobal(L, "lpg"); + + luaL_newmetatable(L, XLUA_DOCUMENT_METATABLE); + luaL_setfuncs(L, xlua_document_table, 0); + lua_pop(L, 1); + + luaL_newmetatable(L, XLUA_WIDGET_METATABLE); + luaL_setfuncs(L, xlua_widget_table, 0); + lua_pop(L, 1); + + luaL_checkstack(L, argc, NULL); + + // Joining the first two might make a tiny bit more sense. + lua_createtable(L, argc - 1, 0); + lua_pushstring(L, (string(argv[0]) + " " + argv[1]).c_str()); + lua_rawseti(L, 1, 1); + for (int i = 2; i < argc; i++) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, 1, i - 1); + } + lua_setglobal(L, "arg"); + + int status = 0; + lua_pushcfunction(L, xlua_error_handler); + if ((status = luaL_loadfile(L, strcmp(argv[1], "-") ? argv[1] : NULL))) + goto error; + for (int i = 2; i < argc; i++) + lua_pushstring(L, argv[i]); + if ((status = lua_pcall(L, argc - 2, 0, 1))) + goto error; + lua_close(L); + return 0; + +error: + // Lua will unfortunately discard exceptions that it hasn't thrown itself. + if (const char *err = lua_tostring(L, -1)) + cerr << "error: " << err << endl; + else + cerr << "error: " << status << endl; + lua_close(L); + return 1; +} diff --git a/lpg/lpg.lua b/lpg/lpg.lua new file mode 100644 index 0000000..caf7b3e --- /dev/null +++ b/lpg/lpg.lua @@ -0,0 +1,240 @@ +#!/usr/bin/env lpg +local project_url = "https://git.janouch.name/p/pdf-simple-sign" + +function h1 (title) + return lpg.VBox {fontsize=18., fontweight=600, + title, lpg.HLine {2}, lpg.Filler {-1, 6}} +end +function h2 (title) + return lpg.VBox {fontsize=16., fontweight=600, + lpg.Filler {-1, 8}, title, lpg.HLine {1}, lpg.Filler {-1, 6}} +end +function h3 (title) + return lpg.VBox {fontsize=14., fontweight=600, + lpg.Filler {-1, 8}, title, lpg.HLine {.25}, lpg.Filler {-1, 6}} +end +function p (...) + return lpg.VBox {..., lpg.Filler {-1, 6}} +end +function code (...) + return lpg.VBox { + lpg.Filler {-1, 4}, + lpg.HBox { + lpg.Filler {12}, + lpg.VBox {"" .. table.concat {...} .. ""}, + lpg.Filler {}, + }, + lpg.Filler {-1, 6}, + } +end +function define (name, ...) + return lpg.VBox { + lpg.Filler {-1, 2}, + lpg.Text {fontweight=600, name}, lpg.Filler {-1, 2}, + lpg.HBox {lpg.Filler {12}, lpg.VBox {...}, lpg.Filler {}}, + lpg.Filler {-1, 2}, + } +end +function pad (widget) + return lpg.VBox { + lpg.Filler {-1, 2}, + lpg.HBox {lpg.Filler {4}, widget, lpg.Filler {}, lpg.Filler {4}}, + lpg.Filler {-1, 2}, + } +end + +local page1 = lpg.VBox {fontfamily="sans serif", fontsize=12., + h1("lpg User Manual"), + p("lpg is a Lua-based PDF document generator, exposing a trivial " .. + "layouting engine on top of the Cairo graphics library, " .. + "with manual paging."), + p("The author has primarily been using this system to typeset invoices."), + + h2("Synopsis"), + p("lpg program.lua [args...]"), + + h2("API"), + p("The Lua program receives lpg's and its own path joined " .. + "as arg[0]. Any remaining sequential elements " .. + "of this table represent the passed args."), + + h3("Utilities"), + + define("lpg.cm (centimeters)", + p("Returns how many document points are needed " .. + "for the given physical length.")), + + define("lpg.ntoa {number [, precision=…]\n" .. + "\t[, thousands_sep=…] [, decimal_point=…] [, grouping=…]}", + p("Formats a number using the C++ localization " .. + "and I/O libraries. " .. + "For example, the following call results in “3 141,59”:"), + code("ntoa {3141.592, precision=2,\n" .. + " thousands_sep=\" \", decimal_point=\",\", " .. + "grouping=\"\\003\"}")), + + define("lpg.escape (values...)", + p("Interprets all values as strings, " .. + "and escapes them to be used as literal text—" .. + "all text within lpg is parsed as Pango markup, " .. + "which is a subset of XML.")), + + h3("PDF documents"), + + define("lpg.Document (filename, width, height [, margin])", + p("Returns a new Document object, whose pages are all " .. + "the same size in 72 DPI points, as specified by width " .. + "and height. The margin is used by show " .. + "on all sides of pages."), + p("The file is finalized when the object is garbage collected.")), + + define("Document.title, author, subject, keywords, " .. + "creator, create_date, mod_date", + p("Write-only PDF Info dictionary metadata strings.")), + + define("Document:show ([widget...])", + p("Starts a new document page, and renders Widget trees over " .. + "the whole print area.")), + + lpg.Filler {}, +} + +local page2 = lpg.VBox {fontfamily="sans serif", fontsize=12., + h3("Widgets"), + p("The layouting system makes heavy use of composition, " .. + "and thus stays simple."), + p("For convenience, anywhere a Widget is expected but another " .. + "kind of value is received, lpg.Text widget will be invoked " .. + "on that value."), + p("Once a Widget is included in another Widget, " .. + "the original Lua object can no longer be used, " .. + "as its reference has been consumed."), + p("Widgets can be indexed by strings to get or set " .. + "their attributes. All Widget constructor tables " .. + "also accept attributes, for convenience. Attributes can be " .. + "either strings or numbers, mostly only act " .. + "on specific Widget kinds, and are hereditary. " .. + "Prefix their names with an underscore to set them ‘privately’."), + p("Widget sizes can be set negative, which signals to their " .. + "container that they should take any remaining space, " .. + "after all their siblings’ requests have been satisfied. " .. + "When multiple widgets make this request, that space is distributed " .. + "in proportion to these negative values."), + + define("lpg.Filler {[width] [, height]}", + p("Returns a new blank widget with the given dimensions, " .. + "which default to -1, -1.")), + define("lpg.HLine {[thickness]}", + p("Returns a new widget that draws a simple horizontal line " .. + "of the given thickness.")), + define("lpg.VLine {[thickness]}", + p("Returns a new widget that draws a simple vertical line " .. + "of the given thickness.")), + define("lpg.Text {[value...]}", + p("Returns a new text widget that renders the concatenation of all " .. + "passed values filtered through Lua’s tostring " .. + "function. Non-strings will additionally be escaped."), + define("Text.fontfamily, fontsize, fontweight, lineheight", + p("Various font properties, similar to their CSS counterparts."))), + define("lpg.Frame {widget}", + p("Returns a special container widget that can override " .. + "a few interesting properties."), + define("Frame.color", + p("Text and line colour, for example 0xff0000 for red.")), + define("Frame.w_override", + p("Forcefully changes the child Widget’s " .. + "requested width, such as to negative values.")), + define("Frame.h_override", + p("Forcefully changes the child Widget’s " .. + "requested height, such as to negative values."))), + + lpg.Filler {}, +} + +local page3 = lpg.VBox {fontfamily="sans serif", fontsize=12., + define("lpg.Link {target, widget}", + p("Returns a new hyperlink widget pointing to the target, " .. + "which is a URL. The hyperlink applies " .. + "to the entire area of the child widget. " .. + "It has no special appearance.")), + define("lpg.HBox {[widget...]}", + p("Returns a new container widget that places children " .. + "horizontally, from left to right."), + p("If any space remains after satisfying the children widgets’ " .. + "requisitions, it is distributed equally amongst all of them. " .. + "Also see the note about negative sizes.")), + define("lpg.VBox {[widget...]}", + p("Returns a new container widget that places children " .. + "vertically, from top to bottom.")), + define("lpg.Picture {filename}", + p("Returns a new picture widget, showing the given filename, " .. + "which currently must be in the PNG format. " .. + "Pictures are rescaled to fit, but keep their aspect ratio.")), + define("lpg.QR {contents, module}", + p("Returns a new QR code widget, encoding the contents " .. + "string using the given module size. " .. + "The QR code version is chosen automatically.")), + + h2("Examples"), + p("See the source code of this user manual " .. + "for the general structure of scripts."), + + h3("Size distribution and composition"), + lpg.VBox { + lpg.HLine {}, + lpg.HBox { + lpg.VLine {}, lpg.Frame {_w_override=lpg.cm(3), pad "3cm"}, + lpg.VLine {}, lpg.Frame {pad "Measured"}, + lpg.VLine {}, lpg.Frame {_w_override=-1, pad "-1"}, + lpg.VLine {}, lpg.Frame {_w_override=-2, pad "-2"}, + lpg.VLine {}, + }, + lpg.HLine {}, + }, + lpg.Filler {-1, 6}, + code([[ +function pad (widget) + local function f (...) return lpg.Filler {...} end + return lpg.VBox {f(-1, 2), lpg.HBox {f(4), w, f(), f(4)}, f(-1, 2)} +end + +lpg.VBox {lpg.HLine {}, lpg.HBox { + lpg.VLine {}, lpg.Frame {_w_override=lpg.cm(3), pad "3cm"}, + lpg.VLine {}, lpg.Frame {pad "Measured"}, + lpg.VLine {}, lpg.Frame {_w_override=-1, pad "-1"}, + lpg.VLine {}, lpg.Frame {_w_override=-2, pad "-2"}, + lpg.VLine {}, +}, lpg.HLine {}}]]), + + h3("Clickable QR code link"), + lpg.HBox { + lpg.VBox { + p("Go here to report bugs, request features, " .. + "or submit pull requests:"), + code(([[ +url = "%s" +lpg.Link {url, lpg.QR {url, 2.5}}]]):format(project_url)), + }, + lpg.Filler {}, + lpg.Link {project_url, lpg.QR {project_url, 2.5}}, + }, + + lpg.Filler {}, +} + +if #arg < 1 then + io.stderr:write("Usage: " .. arg[0] .. " OUTPUT-PDF..." .. "\n") + os.exit(false) +end +local width, height, margin = lpg.cm(21), lpg.cm(29.7), lpg.cm(2.0) +for i = 1, #arg do + local pdf = lpg.Document(arg[i], width, height, margin) + pdf.title = "lpg User Manual" + pdf.subject = "lpg User Manual" + pdf.author = "Přemysl Eric Janouch" + pdf.creator = ("lpg (%s)"):format(project_url) + + pdf:show(page1) + pdf:show(page2) + pdf:show(page3) +end diff --git a/lpg/meson.build b/lpg/meson.build new file mode 100644 index 0000000..3ce57ea --- /dev/null +++ b/lpg/meson.build @@ -0,0 +1,24 @@ +project('lpg', 'cpp', default_options : ['cpp_std=c++17'], + version : '1.1.1') + +conf = configuration_data() +conf.set_quoted('PROJECT_NAME', meson.project_name()) +conf.set_quoted('PROJECT_VERSION', meson.project_version()) +configure_file(output : 'config.h', configuration : conf) + +luapp = dependency('lua++', allow_fallback : true) +cairo = dependency('cairo') +pangocairo = dependency('pangocairo') +libqrencode = dependency('libqrencode') +lpg_exe = executable('lpg', 'lpg.cpp', + install : true, + dependencies : [luapp, cairo, pangocairo, libqrencode]) + +# XXX: https://github.com/mesonbuild/meson/issues/825 +docdir = get_option('datadir') / 'doc' / meson.project_name() +lpg_pdf = custom_target('lpg.pdf', + output : 'lpg.pdf', + input : 'lpg.lua', + command : [lpg_exe, '@INPUT@', '@OUTPUT@'], + install_dir : docdir, + build_by_default : true) diff --git a/lpg/subprojects/lua++.wrap b/lpg/subprojects/lua++.wrap new file mode 100644 index 0000000..1ddf5d6 --- /dev/null +++ b/lpg/subprojects/lua++.wrap @@ -0,0 +1,10 @@ +[wrap-file] +directory = lua-5.4.7 +source_url = https://github.com/lua/lua/archive/refs/tags/v5.4.7.tar.gz +source_filename = lua-5.4.7.tar.gz +source_hash = 5c39111b3fc4c1c9e56671008955a1730f54a15b95e1f1bd0752b868b929d8e3 +patch_directory = lua-5.4.7 + +[provide] +lua++-5.4 = lua_dep +lua++ = lua_dep diff --git a/lpg/subprojects/packagefiles/lua-5.4.7/LICENSE.build b/lpg/subprojects/packagefiles/lua-5.4.7/LICENSE.build new file mode 100644 index 0000000..c62f655 --- /dev/null +++ b/lpg/subprojects/packagefiles/lua-5.4.7/LICENSE.build @@ -0,0 +1,20 @@ +Copyright (c) 2025 Přemysl Eric Janouch +Copyright (c) 2021 The Meson development team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lpg/subprojects/packagefiles/lua-5.4.7/meson.build b/lpg/subprojects/packagefiles/lua-5.4.7/meson.build new file mode 100644 index 0000000..dbc8ff6 --- /dev/null +++ b/lpg/subprojects/packagefiles/lua-5.4.7/meson.build @@ -0,0 +1,50 @@ +project( + 'lua-5.4', + 'cpp', + license : 'MIT', + meson_version : '>=0.49.2', + version : '5.4.7', + default_options : ['c_std=c99', 'warning_level=2'], +) + +cxx = meson.get_compiler('cpp') + +# Skip bogus warning. +add_project_arguments(cxx.get_supported_arguments( + '-Wno-string-plus-int', '-Wno-stringop-overflow'), language : 'cpp') + +# Platform-specific defines. +is_posix = host_machine.system() in ['cygwin', 'darwin', 'dragonfly', 'freebsd', + 'gnu', 'haiku', 'linux', 'netbsd', 'openbsd', 'sunos'] +if is_posix + add_project_arguments('-DLUA_USE_POSIX', language : 'cpp') +endif + +# Library dependencies. +lua_lib_deps = [cxx.find_library('m', required : false)] + +if meson.version().version_compare('>= 0.62') + dl_dep = dependency('dl', required : get_option('loadlib')) +else + dl_dep = cxx.find_library('dl', required : get_option('loadlib')) +endif + +if dl_dep.found() + lua_lib_deps += dl_dep + add_project_arguments('-DLUA_USE_DLOPEN', language : 'cpp') +endif + +# Targets. +add_project_arguments('-DMAKE_LIB', language : 'cpp') +lua_lib = static_library( + 'lua', + 'onelua.cpp', + dependencies : lua_lib_deps, + implicit_include_directories : false, +) + +inc = include_directories('.') +lua_dep = declare_dependency( + link_with : lua_lib, + include_directories : inc, +) diff --git a/lpg/subprojects/packagefiles/lua-5.4.7/meson_options.txt b/lpg/subprojects/packagefiles/lua-5.4.7/meson_options.txt new file mode 100644 index 0000000..ea6f6c4 --- /dev/null +++ b/lpg/subprojects/packagefiles/lua-5.4.7/meson_options.txt @@ -0,0 +1,4 @@ +option( + 'loadlib', type : 'feature', + description : 'Allow Lua to "require" C extension modules' +) diff --git a/lpg/subprojects/packagefiles/lua-5.4.7/onelua.cpp b/lpg/subprojects/packagefiles/lua-5.4.7/onelua.cpp new file mode 100644 index 0000000..6517028 --- /dev/null +++ b/lpg/subprojects/packagefiles/lua-5.4.7/onelua.cpp @@ -0,0 +1 @@ +#include "onelua.c" diff --git a/meson.build b/meson.build index d68a99d..668b612 100644 --- a/meson.build +++ b/meson.build @@ -14,10 +14,10 @@ executable('pdf-simple-sign', 'pdf-simple-sign.cpp', asciidoctor = find_program('asciidoctor') foreach page : ['pdf-simple-sign'] custom_target('manpage for ' + page, - input: page + '.adoc', output: page + '.1', - command: [asciidoctor, '-b', 'manpage', + input : page + '.adoc', output: page + '.1', + command : [asciidoctor, '-b', 'manpage', '-a', 'release-version=' + meson.project_version(), '@INPUT@', '-o', '@OUTPUT@'], - install: true, - install_dir: join_paths(get_option('mandir'), 'man1')) + install : true, + install_dir : join_paths(get_option('mandir'), 'man1')) endforeach