Skip to content

Commit

Permalink
Merge 80253f0 into 576b008
Browse files Browse the repository at this point in the history
  • Loading branch information
Omikhleia committed Dec 21, 2023
2 parents 576b008 + 80253f0 commit abd6d85
Show file tree
Hide file tree
Showing 3 changed files with 1,085 additions and 10 deletions.
117 changes: 107 additions & 10 deletions packages/dropcaps/init.lua
Expand Up @@ -9,16 +9,58 @@ function package:_init ()
self:loadPackage("raiselower")
end

local shapeHbox = function (options, content)
-- Clear irrelevant values before passing to font
options.lines, options.join, options.raise, options.shift, options.color, options.scale = nil, nil, nil, nil, nil, nil
SILE.call("noindent")
function package.declareSettings (_)
SILE.settings:declare({
parameter = "dropcaps.bsratio",
type = "number or nil",
default = nil, -- nil means "use computed value based on font metrics"
help = "When set, fixed default ratio of the descender with respect to the baseline (around 0.3 in usual fonts)."
})
end

local function shapeHbox (options, content)
local hbox = SILE.typesetter:makeHbox(function ()
SILE.call("font", options, content)
end)
return hbox
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

local function getToleranceDepth ()
-- In non-strict mode, we allow using more lines to fit the dropcap.
-- However we cannot just check if the "extra depth" of the dropcap is above 0.
-- First, our depth adjustment is but a best attempt.
-- Moreover, some characters may have a small depth of their own (ex. "O" in Gentium Plus)
-- We must just ensure they stay within "reasonable bounds" with respect to the baseline,
-- so as not to flow over the next lines.
-- We compute a tolerance ratio based on the font metrics, expecting the font to be well-designed.
-- The user can override the computation and set the dropcaps.bsratio setting manually.
-- (LaTeX would likely approximate it using a \strut = with a depth ratio of 0.3bs)
local bsratio
if SILE.settings:get("dropcaps.bsratio") then
bsratio = SILE.settings:get("dropcaps.bsratio")
SU.debug("dropcaps", "Using user-defined descender baseline ratio", bsratio)
else
bsratio = computeBaselineRatio()
SU.debug("dropcaps", "Using computed descender baseline ratio", bsratio)
end
return bsratio * SILE.measurement("1bs"):tonumber()
end

function package:registerCommands ()

-- This implementation relies on the hangafter and hangindent features of Knuth's line-breaking algorithm.
Expand All @@ -31,41 +73,83 @@ function package:registerCommands ()
local shift = SU.cast("measurement", options.shift or 0)
local size = SU.cast("measurement or nil", options.size or nil)
local scale = SU.cast("number", options.scale or 1.0)
local strict = SU.boolean(options.strict, true)
if strict and options.depthadjust then
SU.warn("The depthadjust option is ignored in strict mode.")
end
local color = options.color
options.size = nil -- we need to measure the "would have been" size before using this
-- We need to measure the "would have been" size before using this.
options.size = nil
-- Clear irrelevant option values before passing to font and measuring content.
options.lines, options.join, options.raise, options.shift, options.color, options.scale = nil, nil, nil, nil, nil, nil

if color then self:loadPackage("color") end

-- Some initial capital fonts have all their glyphs hanging below the baseline (e.g. EB Garamond Initials)
-- We cannot manage all pathological cases.
-- Quite empirically, we can shape character(s) which shouldn't usually have a depth normally.
-- If it has, then likely all glyphs do also and we need to compensate for that everywhere.
local depthadjust = options.depthadjust or "I"
local depthAdjustment = not strict and shapeHbox(options, { depthadjust }).depth:tonumber() or 0
SU.debug("dropcaps", "Depth adjustment", depthAdjustment)

-- We want the drop cap to span over N lines, that is N - 1 baselineskip + the height of the first line.
-- Note this only works for the default linespace mechanism.
-- We determine the height of the first line by measuring the size the initial content *would have* been.
-- This gives the font some control over its relative size, sometimes desired sometimes undesired.
local tmpHbox = shapeHbox(options, content)
local extraHeight = SILE.measurement((lines - 1).."bs"):tonumber()
local targetHeight = tmpHbox.height:tonumber() * scale + extraHeight
local curHeight = tmpHbox.height:tonumber() + depthAdjustment
local targetHeight = (curHeight - depthAdjustment) * scale + extraHeight
if strict then
-- Take into account the compensated depth of the initial
curHeight = curHeight + tmpHbox.depth:tonumber()
end
SU.debug("dropcaps", "Target height", targetHeight)

-- Now we need to figure out how to scale the dropcap font to get an initial of targetHeight.
-- From that we can also figure out the width it will be at that height.
local curSize = SILE.measurement(SILE.settings:get("font.size")):tonumber()
local curHeight, curWidth = tmpHbox.height:tonumber(), tmpHbox.width:tonumber()
local curWidth = tmpHbox.width:tonumber()
options.size = size and size:tonumber() or (targetHeight / curHeight * curSize)
local targetWidth = curWidth / curSize * options.size
SU.debug("dropcaps", "Target font size", options.size)
SU.debug("dropcaps", "Target width", targetWidth)

-- Typeset the dropcap with its final shape, but don't output it yet
-- Typeset the dropcap with its final shape, but don't output it yet.
local hbox = shapeHbox(options, content)

if not strict then
-- Compensation for regular extra depth.
local compensationHeight = depthAdjustment * options.size / curSize
SU.debug("dropcaps", "Compensation height", compensationHeight)

-- Some fonts have descenders on letters such as Q, J, etc.
-- In that case we may need extra lines to the dropcap.
local extraDepth = hbox.depth:tonumber() - compensationHeight
local toleranceDepth = getToleranceDepth()
if extraDepth > toleranceDepth then
SU.debug("dropcaps", "Extra depth", extraDepth, "> tolerance", toleranceDepth)
local extraLines = math.ceil((extraDepth - toleranceDepth) / SILE.measurement("1bs"):tonumber())
lines = lines + extraLines
SU.debug("dropcaps", "Extra lines needed to fit", extraLines)
else
SU.debug("dropcaps", "Extra depth", extraDepth, "< tolerance", toleranceDepth)
end
raise = raise:tonumber() + compensationHeight
else
raise = raise:tonumber() + hbox.depth:tonumber()
end

-- Setup up the necessary indents for the final paragraph content
local joinOffset = join and standoff:tonumber() or 0
SILE.settings:set("current.hangAfter", -lines)
SILE.settings:set("current.hangIndent", targetWidth + joinOffset)
SILE.call("noindent")
SU.debug("dropcaps", "joinOffset", joinOffset)

-- The paragraph is indented so as to leave enough space for the drop cap.
-- We "trick" the typesetter with a zero-dimension box wrapping our original box.
SILE.call("rebox", { height = 0, width = -joinOffset }, function ()
SILE.call("rebox", { height = 0, depth = 0, width = -joinOffset }, function ()
SILE.call("glue", { width = shift - targetWidth - joinOffset })
SILE.call("lower", { height = extraHeight - raise }, function ()
SILE.call(color and "color" or "noop", { color = color }, function ()
Expand All @@ -79,6 +163,7 @@ end

package.documentation = [[
\begin{document}
\use[module=packages.dropcaps]
The \autodoc:package{dropcaps} package allows you to format paragraphs with an “initial capital” (also commonly referred as a “drop cap”), typically one large capital letter used as a decorative element at the beginning of a paragraph.
It provides the \autodoc:command{\dropcap} command.
Expand All @@ -92,6 +177,18 @@ To tweak the position of the dropcap, measurements may be passed to the \autodoc
Other options passed to \autodoc:command{\dropcap} will be passed through to \autodoc:command{\font} when drawing the initial letter(s).
This may be useful for passing OpenType options or other font preferences.
Some fonts have capitals — such as, typically, \autodoc:example{Q} and \autodoc:example{J} — hanging below the baseline.
By default, the dropcap fits the specified number of lines and the characters are typeset in a smaller size to fit these descenders.
With the \autodoc:parameter{strict=false} option, the characters are scaled with respect to their height only, and extra hanged lines are added to the dropcap in order to accommodate the descenders.
The dropcap is allowed to overflow the baseline by a reasonable amount, before triggering the addition of extra lines, for fonts that have capitals very slightly hanging below the baseline.
This tolerance is computed based on the font metrics.
If you want to bypass this mechanism and adjust the tolerance, you can use the \autodoc:setting{dropcaps.bsratio} setting.
Moreover, some fonts, such as EB Garamond Initials, have \em{all} capitals hanging below the baseline.
To take this case into account in non-strict mode, the depth adjustment of the dropcap is empirically corrected based on that of a character which shouldn't have any, by default an \autodoc:example{I}.
The character(s) used for this depth adjustment correction can be specified using the \autodoc:parameter{depthadjust} option.
\begin{autodoc:note}
One caveat is that the size of the initials is calculated using the default linespacing mechanism.
If you are using an alternative method from the \autodoc:package{linespacing} package, you might see strange results.
Expand Down

0 comments on commit abd6d85

Please sign in to comment.