5

AFAIK word space in TeX work more or less like a glue instead of a real character. So it is not possible to kern a character with the space, e.g., to make the pair such as f<space>T to stay apart a little. pdfTeX used to have the ability to alter word space (though not exactly the same thing as kerning).

Is there a way to kern characters with word space in LuaTeX?

Update

I have figured out a way to do it. Please comment if there is any improvement can be made.

\documentclass{article}

\directlua{
  function kernspace(head)
    for space in node.traverse_id(node.id('glue'), head) do
      if space.subtype == 13 then % A spaceskip glue
        local prev = node.prev(space)
        local next = node.next(space)
        % Check that the space is between two glyphs
        if prev.id == 29 and next.id == 29 then
          % Below we increase space by 100pt, demonstration purpose only
          % In reality, we can adjust the space according to the prev and next
          % In particular, e.g., adjust the skip such that its natural space
          % will be increased by the kerning value between prev and next
          space.width = space.width + 6553600
        end
      end
    end
    return true
  end
  luatexbase.add_to_callback("pre_linebreak_filter", kernspace, "kernspace");
}

\begin{document}

A V

\end{document}

Update 2 Below is a somewhat more complete, but bit tedious example, in case any one find it useful.

mclass.par.kern = {}
mclass.par.size = {}

function mclass.par.space(head)
  for space in node.traverse_id(node.id('glue'), head) do
    if space.subtype == 13 then -- spaceskip
      local prev = node.prev(space)
      local next = node.next(space)
      if prev.id == 29 and next.id == 29 then -- glyphs
        if prev.font == next.font then
          if not mclass.par.kern[prev.font] then -- find and cache the kerning table of this font
            local fnt = font.getfont(prev.font)
            local seq = fnt.resources.sequences
            mclass.par.size[prev.font] = fnt.size
            for _,t in ipairs(seq) do
              if t.features.kern then
                mclass.par.kern[prev.font] = t.steps[1].coverage
              end
            end
          end
          if mclass.par.kern[prev.font][prev.char] then
            local k = mclass.par.kern[prev.font][prev.char][next.char]
            if k then
              space.width = space.width + k / 1000 * mclass.par.size[prev.font]
            end
          end
          print(space.width)
        end
      end
    end
  end
  return true
end

luatexbase.add_to_callback(
  "pre_linebreak_filter", mclass.par.space, "mclass_par_space");

The end result is that, as if all spaces are first removed, and glyphs properly kerned together (without ligature). And then a normal space is inserted

Below is an example of showing it works on Arno Pro

\definecolor{grey}{cmyk}{0,0,0,0.5}
\def\test#1{\leavevmode\hbox to 0pt{\color{grey}#1\hss}#1}

\begin{document}
\test{A V}\par
\test{A W}\par
\test{W A}\par
\test{V A}\par
\test{f H}\par
\end{document}

enter image description here

The main limitations are

  1. It will not work inside boxes where no paragraph. It is added to the pre_linebreak_filter. However, it might be possible to add it as another call back to make it work in hbox, etc.
  2. It will not check any additional kernings defined by users through the font loader. However, for well made fonts, I found the need for additional kerning is rare and usually are edge cases that do not involve spaces (such as a single quote followed by a double quote in Minion, which is not kerned).

The way I used to find the kerning table is tedious and look quite inefficient to me. If any one can help, many thanks in advance.

The LuaTeX document says that the font table returned by font.getfont shall have a characters table, which is there. And each character hash shall has a sub table names kerns. However, it appears not there, at least not for the OpenType fonts I tested with.

  • 2
    you have tagged this in as luatex, and if you look for luatex specific solutions you could adjust that in several ways either doing a replacement of f T to f\myftspace T in the input buffer or adjusting the horizontal node list before line breaking or .. – David Carlisle Mar 4 '17 at 12:23
  • Are you meaning f<space>T or fT? – egreg Mar 4 '17 at 21:36
  • @egreg I meant f<space>T – Yan Zhou Mar 5 '17 at 7:35
  • Please explain what you mean by "space" in f<space>T. Is it intra-word or inter-word whitespace? If it's inter-word whitespace, please explain what's unsatisfactory (in your view) about the space that gets inserted between "pdf" and "TeX" in "pdf TeX". – Mico Mar 5 '17 at 7:57
  • 2
    @Mico The object in a high level is to achieve visually even word space. For example, say of That will look spaced tighter than A Victor. It is similar to kerning except it applies to word space instead of tracking. Though your answer did not solve my problem, I think it is useful to may people. Examples of how to use call-back is sparse IMO. (However, for intra-word space in particular, I think an even easier way, and probably more appropriate, is to use font handler to define additional kerning table) – Yan Zhou Mar 5 '17 at 14:07
3

[Caveat: I wrote this answer based on a misunderstanding of the OP's objective. I thought the objective was to insert a kern if (say) pT occur within a word (e.g., "pdfTeX"). However, this interpretation was wrong. The OP has asked me, though, not to delete this answer as it might still be of use to future readers who, in fact, do wish to influence intra-word kerning.]

Here's an implementation of David Carlisle's suggestion to set up a Lua function that provides automatic kerning of "fT" pairs and to assign the Lua function to the "process_input_buffer" callback. The code below sets up such a Lua function and provides LaTeX macros to activate and deactivate the Lua function.

enter image description here

\documentclass{article}
\newcommand{\fTkern}{\kern0.5pt} % choose a suitable kern
%% Lua-side code
\usepackage{luacode}
\begin{luacode}
function kernfT ( s )
   return ( string.gsub ( s , "fT" , "f\\fTkern T" ) )
end
\end{luacode}
%% TeX-side code: macros to activate and deactivate the Lua function
\newcommand{\kernftOn}{\directlua{%
  luatexbase.add_to_callback ( "process_input_buffer" ,
  kernfT, "kernFT" )}}
\newcommand{\kernftOff}{\directlua{%
  luatexbase.remove_from_callback ( "process_input_buffer" ,
  "kernFT" )}}
\begin{document}
\kernftOn 
pdfTeX 
\kernftOff
pdfTeX 
pdf\kern0.5ptTeX
\end{document}
  • 1
    I'm not sure this is what the OP wants: note the question mentions “kerning against a space”. – egreg Mar 4 '17 at 21:37
  • @Mico is there a chance to adapt this feature and include it into selnolig to get the question tex.stackexchange.com/q/315123/112503 solved? (not every font offers for example a short armed f) – lAtExFaN Mar 4 '17 at 21:45
  • @egreg - I'm also not entirely sure what the OP wants, given that words "kern" and "space" are both used (even within a given sentence). Maybe "space" is used generically as "whitespace"? Let's see if the OP weighs in and clarifies the posting's objective. – Mico Mar 4 '17 at 22:10
  • @lAtExFaN - Nice to know that people care about selnolig. :-) Indeed, quite a few fonts have only a "long-armed" f glyph, which can tangle with subsequent characters. In my view, the need for a bit of whitespace (babel uses 0.03em, but that's often more than is needed) while breaking typographic ligatures arises primarily for a couple of f-ligatures -- f-l and f-i (but not for f-f; the famous "shelfful" case...) -- and not for most other ligatures (such as ct, st, sp, tt, th, as, es, is, us, et, at, ta, etc.). That's why I've so far been hesitant about inserting ... (continued) – Mico Mar 4 '17 at 22:23
  • @lAtExFaN - (continued) ... whitespace automatically. Assuming, though, that it had been decided that whitespace should be inserted, a second issue is: should the whitespace be breaking or nonbreaking? In the answer above, the hard kern is nonbreaking, i.e., TeX won't insert a hyphenation point at the kern. While this is quite likely OK for the case of "pdfTeX", it's not OK for German words in which an fl or an fi ligature needs to be broken up (e.g, "sträflich" and "Schilfinsel"). As you can see, though, I'm open to having a thorough discussion. Maybe send me an email with your thoughts? – Mico Mar 4 '17 at 22:39
2

You could improve your code by using the kerning callback rather than the pre_linebreak_filter callback. As you observed, the pre_linebreak_filter callback does not run for \hboxes (which makes sense, because there are no lines to be broken). When using the kerning callback, however, you have to first call node.kerning on the node list to apply TeX's kerning.

Also I suggest that you use node.id and node.subtypes rather than hardcoding the magic numbers. In the past LuaTeX has changed those numbers around, so your code might break when you update.

test.lua

local mclass = { par = {} }

mclass.par.kern = {}
mclass.par.size = {}

local subtypes = node.subtypes("glue")

function mclass.par.space(head, tail)
    -- Apply TeX's kerning first
    head, tail = node.kerning(head,tail)

    for space in node.traverse_id(node.id("glue"), head) do if subtypes[space.subtype] == "spaceskip" then -- spaceskip
            local prv = node.prev(space)
            local nxt = node.next(space)
            if prv.id == node.id("glyph") and nxt.id == node.id("glyph") then -- glyphs
                if prv.font == nxt.font then
                    if not mclass.par.kern[prv.font] then -- find and cache the kerning table of this font
                        local fnt = font.getfont(prv.font)
                        local seq = fnt.resources.sequences
                        mclass.par.size[prv.font] = fnt.size
                        for _,t in ipairs(seq) do
                            if t.features.kern then
                                mclass.par.kern[prv.font] = t.steps[1].coverage
                            end
                        end
                    end
                    if mclass.par.kern[prv.font][prv.char] then
                        local k = mclass.par.kern[prv.font][prv.char][nxt.char]
                        if k then
                            space.width = space.width + k / 1000 * mclass.par.size[prv.font]
                        end
                    end
                    print(space.width)
                end
            end
        end
    end
end

luatexbase.add_to_callback("kerning", mclass.par.space, "mclass_par_space");
\documentclass{article}
\usepackage{xcolor}

\begin{document}
\pagestyle{empty}
\setbox0=\hbox to 0pt{\textcolor{gray}{A V}\hss}
\setbox1=\hbox to 0pt{\textcolor{gray}{A W}\hss}
\setbox2=\hbox to 0pt{\textcolor{gray}{W A}\hss}
\setbox3=\hbox to 0pt{\textcolor{gray}{V A}\hss}
\setbox4=\hbox to 0pt{\textcolor{gray}{f H}\hss}
%
\directlua{dofile("test.lua")}
%
\noindent\box0 A V\par
\noindent\box1 A W\par
\noindent\box2 W A\par
\noindent\box3 V A\par
\noindent\box4 f H\par
\end{document}

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.