From fbadf39b7b5ca4c643d8a191022335498b579a22 Mon Sep 17 00:00:00 2001 From: Omikhleia Date: Sun, 10 Mar 2024 17:23:04 +0100 Subject: [PATCH] feat: Add decorations to character styles --- examples/manual-packages/packages.sil | 4 + examples/manual-styling/basics/character.md | 50 +++- examples/sile-resilient-manual-styles.yml | 8 + packages/resilient/liners/init.lua | 256 ++++++++++++++++++++ packages/resilient/styles/init.lua | 18 ++ resilient.sile-2.3.0-1.rockspec | 94 +++++++ resilient.sile-dev-1.rockspec | 2 + resilient/layouts/base.lua | 2 +- 8 files changed, 430 insertions(+), 4 deletions(-) create mode 100644 packages/resilient/liners/init.lua create mode 100644 resilient.sile-2.3.0-1.rockspec diff --git a/examples/manual-packages/packages.sil b/examples/manual-packages/packages.sil index fa8908f..85fdf05 100644 --- a/examples/manual-packages/packages.sil +++ b/examples/manual-packages/packages.sil @@ -210,6 +210,10 @@ class writers. \package-documentation{resilient.sectioning} +\section{Helper multi-liners for character styles} + +\package-documentation{resilient.liners} + %\section{Specialized packages} % %\subsection{(teidict) XML TEI P4 print dictionaries} diff --git a/examples/manual-styling/basics/character.md b/examples/manual-styling/basics/character.md index debaea1..8d4cae5 100644 --- a/examples/manual-styling/basics/character.md +++ b/examples/manual-styling/basics/character.md @@ -13,6 +13,7 @@ A regular character style obeys to the following specification properties: position: "normal|super|sub" case: "normal|upper|lower|title" + decoration: ⟨decoration specification⟩ ``` The ⟨font specification⟩ is an object which can contain any of the usual elements, as @@ -20,9 +21,6 @@ used in the SILE `\font` command. The ⟨color specification⟩ follows the same syntax as defined in the SILE **color** package. -The "properties" might be extended in a future revision; for now they support a position element, to specify a superscript or subscript formatting, and a text case element. -The "normal" values may be used to override a parent style definition, when style inheritance is used. - As an example, the following style results in a blue italic superscript in the Libertinus Serif font. @@ -37,6 +35,52 @@ my-custom-style-name: position: "super" ``` +#### Properties {.unnumbered} + +The "properties" might be extended in a future revision; for now they support a position element, to specify a superscript or subscript formatting, and a text case element. +The "normal" values may be used to override a parent style definition, when style inheritance is used. + +#### Decorations {.unnumbered} + +The ⟨decoration specification⟩ is an object which can contain any of the following elements. + +```yaml +decoration: + line: "underline|strikethrough|redacted|mark" + color: "⟨color specification⟩" + thickness: "⟨dimen⟩" + rough: true|false + fillstyle: "hachure|solid|zigzag|cross-hatch|dashed|zigzag-line" +``` + +The "line" element specifies the type of decoration to be drawn. + +The "color" element denotes the color of the decoration. +If unspecified, the current color is utilized. + +Positioning and thickness of underlines and strikethroughs adhere to the metrics of the current font, respecting values defined by the type designer. +The thickness can be overridden if specified in the style definition. + +Setting the "rough" element to `true` results in a sketchy decoration resembling hand-drawn strokes, as opposed to a solid and +straight appearance. + +A "mark" decoration places its content over a background, while "redacted" _replaces_ is content with a pattern occupying the same space. +For these decorations, the "fillstyle" element dictates the pattern used to fill the area in rough mode. +It defaults to "solid" and "zizag" respectively. +The "thickness" defaults to `0.5pt`. + +For instance, the `md-mark` style overrides the Markdown or Djot default rendering of the "highlight" syntax. +Let's ==mark some text== for the sake of demonstration, with the following style definition: + +```yaml +md-mark: + style: + decoration: + line: "mark" + color: "orange" + rough: true +``` + ### Number styles Some styles are applied to number values (e.g. counters), in which case diff --git a/examples/sile-resilient-manual-styles.yml b/examples/sile-resilient-manual-styles.yml index 4cf2e0c..9c2f3e1 100644 --- a/examples/sile-resilient-manual-styles.yml +++ b/examples/sile-resilient-manual-styles.yml @@ -1,3 +1,9 @@ +md-mark: + style: + decoration: + line: "mark" + color: "orange" + rough: true blockquote: origin: "resilient.book" @@ -1047,4 +1053,6 @@ verbatim: align: "obeylines" before: skip: "smallskip" + after: + skip: "smallskip" diff --git a/packages/resilient/liners/init.lua b/packages/resilient/liners/init.lua new file mode 100644 index 0000000..0ddea25 --- /dev/null +++ b/packages/resilient/liners/init.lua @@ -0,0 +1,256 @@ +-- +-- Some "liners" for the SILE typesetting system. +-- This an alternative to some commands from the "rules" package (underline, strikethrough). +-- Rough drawing is supported, using the low level API from the "framebox" package, +-- which is a dependency of the resilient collection. +-- +-- 2024, Didier Willis +-- License: MIT +-- +-- FIXME TODO: Pretty repetitive code, could be refactored... +-- But early abstraction is often a bad idea, so let's wait for more use cases. +-- +local base = require("packages.base") + +local package = pl.class(base) +package._name = "resilient.liners" + +local graphics = require("packages.framebox.graphics.renderer") +local PathRenderer = graphics.PathRenderer +local RoughPainter = graphics.RoughPainter + +local function getUnderlineParameters () + local ot = require("core.opentype-parser") + local fontoptions = SILE.font.loadDefaults({}) + local face = SILE.font.cache(fontoptions, SILE.shaper.getFace) + local font = ot.parseFont(face) + local upem = font.head.unitsPerEm + local underlinePosition = font.post.underlinePosition / upem * fontoptions.size + local underlineThickness = font.post.underlineThickness / upem * fontoptions.size + return underlinePosition, underlineThickness +end + +local function getStrikethroughParameters () + local ot = require("core.opentype-parser") + local fontoptions = SILE.font.loadDefaults({}) + local face = SILE.font.cache(fontoptions, SILE.shaper.getFace) + local font = ot.parseFont(face) + local upem = font.head.unitsPerEm + local yStrikeoutPosition = font.os2.yStrikeoutPosition / upem * fontoptions.size + local yStrikeoutSize = font.os2.yStrikeoutSize / upem * fontoptions.size + return yStrikeoutPosition, yStrikeoutSize +end + +local metrics = require("fontmetrics") +local bsratiocache = {} +local computeBaselineRatio = function () + local fontoptions = SILE.font.loadDefaults({}) + local bsratio = bsratiocache[SILE.font._key(fontoptions)] + if not bsratio then + local face = SILE.font.cache(fontoptions, SILE.shaper.getFace) + local m = metrics.get_typographic_extents(face) + bsratio = m.descender / (m.ascender + m.descender) + bsratiocache[SILE.font._key(fontoptions)] = bsratio + end + return bsratio +end + +function package:_init () + base._init(self) +end + +function package:registerCommands () + + self:registerCommand("resilient:liner:underline", function (options, content) + local underlinePosition, underlineThickness = getUnderlineParameters() + local isRough = SU.boolean(options.rough, false) + + local color + if options.thickness and options.thickness ~= "auto" then + underlineThickness = SU.cast("measurement", options.thickness):tonumber() + end + if options.color and options.color ~= "auto" then + color = SILE.color(options.color) + end + + local paintOptions = {} + if isRough then + paintOptions.preserveVertices = true + paintOptions.disableMultiStroke = true + end + paintOptions.strokeWidth = underlineThickness + paintOptions.stroke = color + + SILE.typesetter:liner("resilient:liner:underline", content, + function (box, typesetter, line) + local oldX = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + + -- Build the content. + -- Cursor will be moved by the actual definitive size. + box:outputContent(typesetter, line) + local newX = typesetter.frame.state.cursorX + + -- Output a line. + -- NOTE: According to the OpenType specs, underlinePosition is "the suggested distance of + -- the top of the underline from the baseline" so it seems implied that the thickness + -- should expand downwards + local painter = PathRenderer(isRough and RoughPainter()) + local w = (newX - oldX):tonumber() + local path = painter:line(0, 0, w, 0, paintOptions) + + SILE.outputter:drawSVG(path, + oldX, Y - underlinePosition + underlineThickness, + newX - oldX, underlineThickness/2, 1) + end + ) + end, "Underlines some content") + + + self:registerCommand("resilient:liner:strikethrough", function (options, content) + local yStrikeoutPosition, yStrikeoutSize = getStrikethroughParameters() + local isRough = SU.boolean(options.rough, false) + + local color + if options.thickness and options.thickness ~= "auto" then + yStrikeoutSize = SU.cast("measurement", options.thickness):tonumber() + end + if options.color and options.color ~= "auto" then + color = SILE.color(options.color) + end + + local paintOptions = {} + if isRough then + paintOptions.preserveVertices = true + paintOptions.disableMultiStroke = true + end + paintOptions.strokeWidth = yStrikeoutSize + paintOptions.stroke = color + + SILE.typesetter:liner("resilient:liner:strikethrough", content, + function (box, typesetter, line) + local oldX = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + + -- Build the content. + -- Cursor will be moved by the actual definitive size. + box:outputContent(typesetter, line) + local newX = typesetter.frame.state.cursorX + + -- Output a line. + -- NOTE: The OpenType spec is not explicit regarding how the size + -- (thickness) affects the position. We opt to distribute evenly + local painter = PathRenderer(isRough and RoughPainter()) + local w = (newX - oldX):tonumber() + local path = painter:line(0, 0, w, 0, paintOptions) + + SILE.outputter:drawSVG(path, + oldX, Y - yStrikeoutPosition - yStrikeoutSize / 2, + newX - oldX, - yStrikeoutSize / 2, 1) + end + ) + end, "Strikes out some content") + + self:registerCommand("resilient:liner:redacted", function (options, content) + local bs = SILE.measurement("0.9bs"):tonumber() + local bsratio = computeBaselineRatio() + local isRough = SU.boolean(options.rough, false) + + -- TODO still some discrepancies with the color between rough and non-rough painter + -- despite ptable 3.0 /!\ + local color = SILE.color(options.color or "black") + + local paintOptions = {} + if isRough then + paintOptions.preserveVertices = true + paintOptions.fillStyle = options.fillstyle or 'solid' + end + paintOptions.stroke = 'none' + paintOptions.fill = color + paintOptions.strokeWidth = SU.cast("measurement", options.thickness or "0.5pt"):tonumber() + + SILE.typesetter:liner("resilient:liner:redacted", content, + function (box, typesetter, line) + local outputWidth = SU.rationWidth(box.width, box.width, line.ratio) + local H = SU.max(box.height:tonumber(), (1 - bsratio) * bs) + local D = SU.max(box.depth:tonumber(), bsratio * bs) + local X = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + + local painter = PathRenderer(isRough and RoughPainter()) + local w = (outputWidth):tonumber() + local path = painter:rectangle(0, 0, w, H + D, paintOptions) + + SILE.outputter:drawSVG(path, + X, Y+D, outputWidth, H+D, 1) + + typesetter.frame:advanceWritingDirection(outputWidth) + end + ) + end) + + self:registerCommand("resilient:liner:mark", function (options, content) + local bs = SILE.measurement("0.9bs"):tonumber() + local bsratio = computeBaselineRatio() + local isRough = SU.boolean(options.rough, false) + + -- TODO still some discrepancies with the color between rough and non-rough painter + -- despite ptable 3.0 /!\ + local color = SILE.color(options.color or "yellow") + + local paintOptions = {} + if isRough then + paintOptions.preserveVertices = true + paintOptions.fillStyle = options.fillstyle or 'zizag' + end + paintOptions.stroke = "none" + paintOptions.fill = color + paintOptions.strokeWidth = SU.cast("measurement", options.thickness or "0.5pt"):tonumber() + + SILE.typesetter:liner("resilient:liner:mark", content, + function (box, typesetter, line) + local outputWidth = SU.rationWidth(box.width, box.width, line.ratio) + local H = SU.max(box.height:tonumber(), (1 - bsratio) * bs) + local D = SU.max(box.depth:tonumber(), bsratio * bs) + local X = typesetter.frame.state.cursorX + local Y = typesetter.frame.state.cursorY + + local painter = PathRenderer(isRough and RoughPainter()) + local w = (outputWidth):tonumber() + local path = painter:rectangle(0, 0, w, H + D, paintOptions) + + SILE.outputter:drawSVG(path, + X, Y+D, outputWidth, H+D, 1) + + box:outputContent(typesetter, line) + end + ) + end) + +end + +package.documentation = [[ +\begin{document} +\use[module=packages.resilient.liners] + +The \autodoc:package{resilient.liners} package provides commands to: + +\begin{itemize} +\item{Underline content, \autodoc:command{\resilient:liner:underline}.} +\item{Strikethrough content, \autodoc:command{\resilient:liner:strikethrough}.} +\item{Redact content, \autodoc:command{\resilient:liner:redacted}.} +\item{Mark (highlight) content, \autodoc:command{\resilient:liner:mark}.} +\end{itemize} + +These content can span multiple lines, and the decorations will be drawn accordingly. + +This is \resilient:liner:underline{underlined}, \resilient:liner:underline[rough=true]{roughly underlined}, +\resilient:liner:strikethrough{struck out}, \resilient:liner:strikethrough[rough=true]{roughly struck out}, +\resilient:liner:redacted{redacted} (redacted), \resilient:liner:redacted[rough=true]{roughly redacted} (roughly redacted), +\resilient:liner:mark{marked}, and \resilient:liner:mark[rough=true]{roughly marked}. + +These commands were designed for the \autodoc:package{resilient.style} package, where they are used to support the rendering of “decorations” in character styles. +\end{document} +]] + +return package diff --git a/packages/resilient/styles/init.lua b/packages/resilient/styles/init.lua index 4eecce2..9e6b2a1 100644 --- a/packages/resilient/styles/init.lua +++ b/packages/resilient/styles/init.lua @@ -18,6 +18,7 @@ function package:_init (options) self.class:loadPackage("textsubsuper") self.class:loadPackage("textcase") + self.class:loadPackage("resilient.liners") self.class:registerHook("finish", self.writeStyles) @@ -179,6 +180,14 @@ SILE.scratch.styles = { positions = { super = "textsuperscript", sub = "textsubscript", + }, + -- Known decoration options + -- Packages and classes can register extra options in this table. + decorations = { + underline = "resilient:liner:underline", + strikethrough = "resilient:liner:strikethrough", + mark = "resilient:liner:mark", + redacted = "resilient:liner:redacted", } } @@ -284,6 +293,15 @@ function package:registerCommands () content = createCommand(caseCommand, {}, content) end end + if style.decoration then + if style.decoration.line then + local lineCommand = SILE.scratch.styles.decorations[style.decoration.line] + if not lineCommand then + SU.error("Invalid style decoration line '"..style.decoration.line.."'") + end + content = createCommand(lineCommand, style.decoration, content) + end + end if style.color then content = createCommand("color", { color = style.color }, content) end diff --git a/resilient.sile-2.3.0-1.rockspec b/resilient.sile-2.3.0-1.rockspec new file mode 100644 index 0000000..ccff393 --- /dev/null +++ b/resilient.sile-2.3.0-1.rockspec @@ -0,0 +1,94 @@ +rockspec_format = "3.0" +package = "resilient.sile" +version = "2.3.0-1" +source = { + url = "git+https://github.com/Omikhleia/resilient.sile.git", + tag = "v2.3.0", +} +description = { + summary = "Advanced book classes and tools for the SILE typesetting system.", + detailed = [[ + This collection of classes and packages for the SILE typesetter system provides + advanced classes and tools for easier print-quality book production. + ]], + homepage = "https://github.com/Omikhleia/resilient.sile", + license = "MIT", +} +dependencies = { + "lua >= 5.1", + "barcodes.sile >= 1.0.0", + "couyards.sile >= 1.0.0", + "embedders.sile >= 0.1.0", + "fancytoc.sile >= 1.0.1", + "labelrefs.sile >= 0.1.0", + "printoptions.sile >= 1.0.0", + "ptable.sile >= 3.0.0", + "qrcode.sile >= 1.0.0", + "textsubsuper.sile >= 1.1.1", + "markdown.sile >= 2.0.0", + "silex.sile >= 0.5.0", +} +build = { + type = "builtin", + modules = { + ["sile.classes.resilient.base"] = "classes/resilient/base.lua", + + ["sile.classes.resilient.book"] = "classes/resilient/book.lua", + ["sile.classes.resilient.resume"] = "classes/resilient/resume.lua", + + ["sile.packages.resilient.base"] = "packages/resilient/base.lua", + + ["sile.packages.resilient.abbr"] = "packages/resilient/abbr/init.lua", + ["sile.packages.resilient.styles"] = "packages/resilient/styles/init.lua", + ["sile.packages.resilient.tableofcontents"] = "packages/resilient/tableofcontents/init.lua", + ["sile.packages.resilient.sectioning"] = "packages/resilient/sectioning/init.lua", + ["sile.packages.resilient.poetry"] = "packages/resilient/poetry/init.lua", + ["sile.packages.resilient.footnotes"] = "packages/resilient/footnotes/init.lua", + ["sile.packages.resilient.lists"] = "packages/resilient/lists/init.lua", + ["sile.packages.resilient.headers"] = "packages/resilient/headers/init.lua", + ["sile.packages.resilient.epigraph"] = "packages/resilient/epigraph/init.lua", + ["sile.packages.resilient.bookmatters"] = "packages/resilient/bookmatters/init.lua", + ["sile.packages.resilient.verbatim"] = "packages/resilient/verbatim/init.lua", + ["sile.packages.resilient.defn"] = "packages/resilient/defn/init.lua", + ["sile.packages.resilient.liners"] = "packages/resilient/liners/init.lua", + + ["sile.packages.resilient.bible.usx"] = "packages/resilient/bible/usx/init.lua", + ["sile.packages.resilient.bible.tei"] = "packages/resilient/bible/tei/init.lua", + + ["sile.packages.autodoc-resilient"] = "packages/autodoc-resilient/init.lua", + + ["sile.resilient.utils"] = "resilient/utils.lua", + ["sile.resilient.layoutparser"] = "resilient/layoutparser.lua", + + ["sile.resilient.layouts.base"] = "resilient/layouts/base.lua", + + ["sile.resilient.layouts.canonical"] = "resilient/layouts/canonical.lua", + ["sile.resilient.layouts.division"] = "resilient/layouts/division.lua", + ["sile.resilient.layouts.frenchcanon"] = "resilient/layouts/frenchcanon.lua", + ["sile.resilient.layouts.geometry"] = "resilient/layouts/geometry.lua", + ["sile.resilient.layouts.marginal"] = "resilient/layouts/marginal.lua", + + ["sile.resilient.adapters.frameset"] = "resilient/adapters/frameset.lua", + + ["sile.inputters.silm"] = "inputters/silm.lua", + + ["sile.resilient-tinyyaml"] = "lua-libraries/resilient-tinyyaml.lua", + }, + install = { + lua = { + ["sile.packages.resilient.bible.tei.monograms.default"] = "packages/resilient/bible/tei/monograms/default.png", + ["sile.packages.resilient.bible.tei.monograms.jn"] = "packages/resilient/bible/tei/monograms/jn.png", + ["sile.packages.resilient.bible.tei.monograms.mk"] = "packages/resilient/bible/tei/monograms/mk.png", + ["sile.packages.resilient.bible.tei.monograms.mt"] = "packages/resilient/bible/tei/monograms/mt.png", + ["sile.packages.resilient.bible.tei.monograms.lk"] = "packages/resilient/bible/tei/monograms/lk.png", + ["sile.packages.resilient.bible.tei.monograms.sign"] = "packages/resilient/bible/tei/monograms/sign.png", + + ["sile.templates.halftitle-recto"] = "templates/halftitle-recto.djt", + ["sile.templates.halftitle-verso"] = "templates/halftitle-verso.djt", + ["sile.templates.title-recto"] = "templates/title-recto.djt", + ["sile.templates.title-verso"] = "templates/title-verso.djt", + ["sile.templates.endpaper-recto"] = "templates/endpaper-recto.djt", + ["sile.templates.endpaper-verso"] = "templates/endpaper-verso.djt", + } + } +} diff --git a/resilient.sile-dev-1.rockspec b/resilient.sile-dev-1.rockspec index 60951b3..5e46646 100644 --- a/resilient.sile-dev-1.rockspec +++ b/resilient.sile-dev-1.rockspec @@ -1,3 +1,4 @@ +rockspec_format = "3.0" package = "resilient.sile" version = "dev-1" source = { @@ -48,6 +49,7 @@ build = { ["sile.packages.resilient.bookmatters"] = "packages/resilient/bookmatters/init.lua", ["sile.packages.resilient.verbatim"] = "packages/resilient/verbatim/init.lua", ["sile.packages.resilient.defn"] = "packages/resilient/defn/init.lua", + ["sile.packages.resilient.liners"] = "packages/resilient/liners/init.lua", ["sile.packages.resilient.bible.usx"] = "packages/resilient/bible/usx/init.lua", ["sile.packages.resilient.bible.tei"] = "packages/resilient/bible/tei/init.lua", diff --git a/resilient/layouts/base.lua b/resilient/layouts/base.lua index ceb3b16..8b1ed77 100644 --- a/resilient/layouts/base.lua +++ b/resilient/layouts/base.lua @@ -139,7 +139,7 @@ local function buildFrameRect (painter, frame, wratio, hratio, options) frame:top():tonumber() * hratio, frame:width():tonumber() * wratio, frame:height():tonumber() * hratio, { - fill = options.fillcolor and SILE.color(options.fillcolor), + fill = options.fillcolor and SILE.color(options.fillcolor) or "none", stroke = options.strokecolor and SILE.color(options.strokecolor), preserveVertices = SU.boolean(options.preserve, true), disableMultiStroke = SU.boolean(options.singlestroke, true),