Skip to content

Commit 448ab15

Browse files
OmikhleiaDidier Willis
authored andcommitted
fix(typesetters): Special punctuation spaces need better italic correction
The heuristics previously considered that special punctuation spaces (as used in French) would cancel italic automated correction. This could however still lead to character overlaps even with decent fonts, so a slightly better logic is needed here: by default, ensure minimal italic correction if the space is not sufficient, for partial compensation. A new setting is also added to fully apply italic correction.
1 parent 2544c7d commit 448ab15

File tree

1 file changed

+73
-35
lines changed

1 file changed

+73
-35
lines changed

typesetters/base.lua

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@ function typesetter.declareSettings (_)
137137
help = "Whether italic correction is activated or not",
138138
})
139139

140+
SILE.settings:declare({
141+
parameter = "typesetter.italicCorrection.punctuation",
142+
type = "boolean",
143+
default = true,
144+
help = "Whether italic correction is compensated on special punctuation spaces (e.g. in French)",
145+
})
146+
140147
SILE.settings:declare({
141148
parameter = "typesetter.softHyphen",
142149
type = "boolean",
@@ -430,93 +437,123 @@ function typesetter:breakIntoLines (nodelist, breakWidth)
430437
return self:breakpointsToLines(breakpoints)
431438
end
432439

440+
--- Extract the last shaped item from a node list.
441+
-- @tparam table nodelist A list of nodes.
442+
-- @treturn table The last shaped item.
443+
-- @treturn boolean Whether the list contains a glue after the last shaped item.
444+
-- @treturn number|nil The width of a punctuation kern after the last shaped item, if any.
433445
local function getLastShape (nodelist)
446+
local lastShape
434447
local hasGlue
435-
local last
448+
local punctSpaceWidth
436449
if nodelist then
437450
-- The node list may contain nnodes, penalties, kern and glue
438451
-- We skip the latter, and retrieve the last shaped item.
439452
for i = #nodelist, 1, -1 do
440453
local n = nodelist[i]
441454
if n.is_nnode then
442455
local items = n.nodes[#n.nodes].value.items
443-
last = items[#items]
456+
lastShape = items[#items]
444457
break
445458
end
446459
if n.is_kern and n.subtype == "punctspace" then
447460
-- Some languages such as French insert a special space around
448-
-- punctuations. In those case, we should not need italic correction.
449-
break
461+
-- punctuations.
462+
-- In those case, we have different strategies for handling
463+
-- italic correction.
464+
punctSpaceWidth = n.width:tonumber()
450465
end
451466
if n.is_glue then
452467
hasGlue = true
453468
end
454469
end
455470
end
456-
return last, hasGlue
471+
return lastShape, hasGlue, punctSpaceWidth
457472
end
473+
474+
--- Extract the first shaped item from a node list.
475+
-- @tparam table nodelist A list of nodes.
476+
-- @treturn table The first shaped item.
477+
-- @treturn boolean Whether the list contains a glue before the first shaped item.
478+
-- @treturn number|nil The width of a punctuation kern before the first shaped item, if any.
458479
local function getFirstShape (nodelist)
459-
local first
480+
local firstShape
460481
local hasGlue
482+
local punctSpaceWidth
461483
if nodelist then
462484
-- The node list may contain nnodes, penalties, kern and glue
463485
-- We skip the latter, and retrieve the first shaped item.
464486
for i = 1, #nodelist do
465487
local n = nodelist[i]
466488
if n.is_nnode then
467489
local items = n.nodes[1].value.items
468-
first = items[1]
490+
firstShape = items[1]
469491
break
470492
end
471493
if n.is_kern and n.subtype == "punctspace" then
472494
-- Some languages such as French insert a special space around
473-
-- punctuations. In those case, we should not need italic correction.
474-
break
495+
-- punctuations.
496+
-- In those case, we have different strategies for handling
497+
-- italic correction.
498+
punctSpaceWidth = n.width:tonumber()
475499
end
476500
if n.is_glue then
477501
hasGlue = true
478502
end
479503
end
480504
end
481-
return first, hasGlue
482-
end
483-
484-
local function fromItalicCorrection (precShape, curShape)
505+
return firstShape, hasGlue, punctSpaceWidth
506+
end
507+
508+
--- Compute the italic correction when switching from italic to non-italic.
509+
-- Computing italic correction is at best heuristics.
510+
-- The strong assumption is that italic is slanted to the right.
511+
-- Thus, the part of the character that goes beyond its width is usually maximal at the top of the glyph.
512+
-- E.g. consider a "f", that would be the top hook extent.
513+
-- Pathological cases exist, such as fonts with a Q with a long tail, but these will rarely occur in usual languages.
514+
-- For instance, Klingon's "QaQ" might be an issue, but there's not much we can do...
515+
-- Another assumption is that we can distribute that extent in proportion with the next character's height.
516+
-- This might not work that well with non-Latin scripts.
517+
--
518+
-- @tparam table precShape The last shaped item (italic).
519+
-- @tparam table curShape The first shaped item (non-italic).
520+
-- @tparam number|nil punctSpaceWidth The width of a punctuation kern between the two items, if any.
521+
local function fromItalicCorrection (precShape, curShape, punctSpaceWidth)
485522
local xOffset
486523
if not curShape or not precShape then
487524
xOffset = 0
525+
elseif precShape.height <= 0 then
526+
xOffset = 0
488527
else
489-
-- Computing italic correction is at best heuristics.
490-
-- The strong assumption is that italic is slanted to the right.
491-
-- Thus, the part of the character that goes beyond its width is usually
492-
-- maximal at the top of the glyph.
493-
-- E.g. consider a "f", that would be the top hook extent.
494-
-- Pathological cases exist, such as fonts with a Q with a long tail,
495-
-- but these will rarely occur in usual languages. For instance, Klingon's
496-
-- "QaQ" might be an issue, but there's not much we can do...
497-
-- Another assumption is that we can distribute that extent in proportion
498-
-- with the next character's height.
499-
-- This might not work that well with non-Latin scripts.
500528
local d = precShape.glyphWidth + precShape.x_bearing
501529
local delta = d > precShape.width and d - precShape.width or 0
502530
xOffset = precShape.height <= curShape.height and delta or delta * curShape.height / precShape.height
531+
if punctSpaceWidth and SILE.settings:get("typesetter.italicCorrection.punctuation") then
532+
xOffset = xOffset - punctSpaceWidth > 0 and (xOffset - punctSpaceWidth) or 0
533+
end
503534
end
504535
return xOffset
505536
end
506537

507-
local function toItalicCorrection (precShape, curShape)
508-
if not SILE.settings:get("typesetter.italicCorrection") then
509-
return
510-
end
538+
--- Compute the italic correction when switching from non-italic to italic.
539+
-- Same assumptions as fromItalicCorrection(), but on the starting side of the glyph.
540+
--
541+
-- @tparam table precShape The last shaped item (non-italic).
542+
-- @tparam table curShape The first shaped item (italic).
543+
-- @tparam number|nil punctSpaceWidth The width of a punctuation kern between the two items, if any.
544+
local function toItalicCorrection (precShape, curShape, punctSpaceWidth)
511545
local xOffset
512546
if not curShape or not precShape then
513547
xOffset = 0
548+
elseif precShape.depth <= 0 then
549+
xOffset = 0
514550
else
515-
-- Same assumptions as fromItalicCorrection(), but on the starting side of
516-
-- the glyph.
517551
local d = curShape.x_bearing
518552
local delta = d < 0 and -d or 0
519553
xOffset = precShape.depth >= curShape.depth and delta or delta * precShape.depth / curShape.depth
554+
if punctSpaceWidth and SILE.settings:get("typesetter.italicCorrection.punctuation") then
555+
xOffset = punctSpaceWidth - xOffset > 0 and xOffset or 0
556+
end
520557
end
521558
return xOffset
522559
end
@@ -537,23 +574,24 @@ function typesetter.shapeAllNodes (_, nodelist, inplace)
537574
local newNodelist = {}
538575
local prec
539576
local precShapedNodes
577+
local isItalicCorrectionEnabled = SILE.settings:get("typesetter.italicCorrection")
540578
for _, current in ipairs(nodelist) do
541579
if current.is_unshaped then
542580
local shapedNodes = current:shape()
543581

544-
if SILE.settings:get("typesetter.italicCorrection") and prec then
582+
if isItalicCorrectionEnabled and prec then
545583
local itCorrOffset
546584
local isGlue
547585
if isItalicLike(prec) and not isItalicLike(current) then
548586
local precShape, precHasGlue = getLastShape(precShapedNodes)
549-
local curShape, curHasGlue = getFirstShape(shapedNodes)
587+
local curShape, curHasGlue, curPunctSpaceWidth = getFirstShape(shapedNodes)
550588
isGlue = precHasGlue or curHasGlue
551-
itCorrOffset = fromItalicCorrection(precShape, curShape)
589+
itCorrOffset = fromItalicCorrection(precShape, curShape, curPunctSpaceWidth)
552590
elseif not isItalicLike(prec) and isItalicLike(current) then
553-
local precShape, precHasGlue = getLastShape(precShapedNodes)
591+
local precShape, precHasGlue, precPunctSpaceWidth = getLastShape(precShapedNodes)
554592
local curShape, curHasGlue = getFirstShape(shapedNodes)
555593
isGlue = precHasGlue or curHasGlue
556-
itCorrOffset = toItalicCorrection(precShape, curShape)
594+
itCorrOffset = toItalicCorrection(precShape, curShape, precPunctSpaceWidth)
557595
end
558596
if itCorrOffset and itCorrOffset ~= 0 then
559597
-- If one of the node contains a glue (e.g. "a \em{proof} is..."),

0 commit comments

Comments
 (0)