Add a Lua PDF generator
Publishing my old invoice layouter in a reusable scripting-based form, rather than an annoyingly fixed binary. Because Lua compiled for C++ might be hard to find, we provide a wrap. Curiously, only GitHub releases seem to contain onelua.c, which is a very handy file. We could have also subprojected libqr, which is in the public domain, however the other main dependencies are also LGPL like libqrencode is. And it is likely to be installed. The user manual also serves as a test.
This commit is contained in:
parent
147b880524
commit
e8752e53ac
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017 - 2024, Přemysl Eric Janouch <p@janouch.name>
|
||||
Copyright (c) 2017 - 2025, Přemysl Eric Janouch <p@janouch.name>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted.
|
||||
|
11
README.adoc
11
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,
|
||||
|
9
lpg/.clang-format
Normal file
9
lpg/.clang-format
Normal file
@ -0,0 +1,9 @@
|
||||
BasedOnStyle: LLVM
|
||||
ColumnLimit: 80
|
||||
IndentWidth: 4
|
||||
TabWidth: 4
|
||||
UseTab: ForContinuationAndIndentation
|
||||
SpaceAfterCStyleCast: true
|
||||
AlignAfterOpenBracket: DontAlign
|
||||
AlignOperands: DontAlign
|
||||
SpacesBeforeTrailingComments: 2
|
1138
lpg/lpg.cpp
Normal file
1138
lpg/lpg.cpp
Normal file
File diff suppressed because it is too large
Load Diff
240
lpg/lpg.lua
Normal file
240
lpg/lpg.lua
Normal file
@ -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 {"<tt>" .. table.concat {...} .. "</tt>"},
|
||||
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("<b>lpg</b> 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("<b>lpg</b> <i>program.lua</i> [<i>args...</i>]"),
|
||||
|
||||
h2("API"),
|
||||
p("The Lua program receives <b>lpg</b>'s and its own path joined " ..
|
||||
"as <tt>arg[0]</tt>. Any remaining sequential elements " ..
|
||||
"of this table represent the passed <i>args</i>."),
|
||||
|
||||
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 <b>lpg</b> 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 <i>Document</i> object, whose pages are all " ..
|
||||
"the same size in 72 DPI points, as specified by <b>width</b> " ..
|
||||
"and <b>height</b>. The <b>margin</b> is used by <b>show</b> " ..
|
||||
"on all sides of pages."),
|
||||
p("The file is finalized when the object is garbage collected.")),
|
||||
|
||||
define("<i>Document</i>.title, author, subject, keywords, " ..
|
||||
"creator, create_date, mod_date",
|
||||
p("Write-only PDF <i>Info</i> dictionary metadata strings.")),
|
||||
|
||||
define("<i>Document</i>:show ([widget...])",
|
||||
p("Starts a new document page, and renders <i>Widget</i> 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 <i>Widget</i> is expected but another " ..
|
||||
"kind of value is received, <b>lpg.Text</b> widget will be invoked " ..
|
||||
"on that value."),
|
||||
p("Once a <i>Widget</i> is included in another <i>Widget</i>, " ..
|
||||
"the original Lua object can no longer be used, " ..
|
||||
"as its reference has been consumed."),
|
||||
p("<i>Widgets</i> can be indexed by strings to get or set " ..
|
||||
"their <i>attributes</i>. All <i>Widget</i> constructor tables " ..
|
||||
"also accept attributes, for convenience. Attributes can be " ..
|
||||
"either strings or numbers, mostly only act " ..
|
||||
"on specific <i>Widget</i> kinds, and are hereditary. " ..
|
||||
"Prefix their names with an underscore to set them ‘privately’."),
|
||||
p("<i>Widget</i> 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 <b>thickness</b>.")),
|
||||
define("lpg.VLine {[thickness]}",
|
||||
p("Returns a new widget that draws a simple vertical line " ..
|
||||
"of the given <b>thickness</b>.")),
|
||||
define("lpg.Text {[value...]}",
|
||||
p("Returns a new text widget that renders the concatenation of all " ..
|
||||
"passed values filtered through Lua’s <b>tostring</b> " ..
|
||||
"function. Non-strings will additionally be escaped."),
|
||||
define("<i>Text</i>.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("<i>Frame</i>.color",
|
||||
p("Text and line colour, for example <tt>0xff0000</tt> for red.")),
|
||||
define("<i>Frame</i>.w_override",
|
||||
p("Forcefully changes the child <i>Widget</i>’s " ..
|
||||
"requested width, such as to negative values.")),
|
||||
define("<i>Frame</i>.h_override",
|
||||
p("Forcefully changes the child <i>Widget</i>’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 <b>target</b>, " ..
|
||||
"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 <b>filename</b>, " ..
|
||||
"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 <b>contents</b> " ..
|
||||
"string using the given <b>module</b> 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([[
|
||||
<small><b>function</b> pad (widget)
|
||||
<b>local function</b> f (...) <b>return</b> lpg.Filler {...} <b>end</b>
|
||||
<b>return</b> lpg.VBox {f(-1, 2), lpg.HBox {f(4), w, f(), f(4)}, f(-1, 2)}
|
||||
<b>end</b>
|
||||
|
||||
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 {}}</small>]]),
|
||||
|
||||
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
|
24
lpg/meson.build
Normal file
24
lpg/meson.build
Normal file
@ -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)
|
10
lpg/subprojects/lua++.wrap
Normal file
10
lpg/subprojects/lua++.wrap
Normal file
@ -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
|
20
lpg/subprojects/packagefiles/lua-5.4.7/LICENSE.build
Normal file
20
lpg/subprojects/packagefiles/lua-5.4.7/LICENSE.build
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2025 Přemysl Eric Janouch <p@janouch.name>
|
||||
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.
|
50
lpg/subprojects/packagefiles/lua-5.4.7/meson.build
Normal file
50
lpg/subprojects/packagefiles/lua-5.4.7/meson.build
Normal file
@ -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,
|
||||
)
|
4
lpg/subprojects/packagefiles/lua-5.4.7/meson_options.txt
Normal file
4
lpg/subprojects/packagefiles/lua-5.4.7/meson_options.txt
Normal file
@ -0,0 +1,4 @@
|
||||
option(
|
||||
'loadlib', type : 'feature',
|
||||
description : 'Allow Lua to "require" C extension modules'
|
||||
)
|
1
lpg/subprojects/packagefiles/lua-5.4.7/onelua.cpp
Normal file
1
lpg/subprojects/packagefiles/lua-5.4.7/onelua.cpp
Normal file
@ -0,0 +1 @@
|
||||
#include "onelua.c"
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user