@@ -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 )
431438end
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.
433445local 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
457472end
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.
458479local 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
505536end
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
522559end
@@ -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