Skip to content

Commit

Permalink
Merge pull request #78 from Omikhleia/feat-liners
Browse files Browse the repository at this point in the history
feat: Add decorations to character styles
  • Loading branch information
Omikhleia committed Mar 17, 2024
2 parents b728cc1 + fbadf39 commit 72f64fd
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 4 deletions.
4 changes: 4 additions & 0 deletions examples/manual-packages/packages.sil
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
50 changes: 47 additions & 3 deletions examples/manual-styling/basics/character.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@ 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
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.

Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions examples/sile-resilient-manual-styles.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
md-mark:
style:
decoration:
line: "mark"
color: "orange"
rough: true

blockquote:
origin: "resilient.book"
Expand Down Expand Up @@ -1047,4 +1053,6 @@ verbatim:
align: "obeylines"
before:
skip: "smallskip"
after:
skip: "smallskip"

256 changes: 256 additions & 0 deletions packages/resilient/liners/init.lua
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions packages/resilient/styles/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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",
}
}

Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 72f64fd

Please sign in to comment.