6

The xeCJK package provides a key-value option xCJKecglue which sets the horizontal space when transitioning between CJK script and Latin script. This transitional space usually ranges from 15 to 14 of an em. It is often desired to set xCJKecglue + digit.width + xCJKecglue equal to the width of a (square) ideograph, so that phrases like 第一章 and 第 1 章 (both meaning “Chapter 1”) occupy the same amount of space.

Unlike ideographs (usually designed in a fixed square), Latin fonts are usually proportional. Even if the digits are tabular/mono-spaced, they can have different widths under different weights (e.g., wider in boldface). I would like to add \xeCJKsetup{ xCJKecglue = <glue> } in an automatic way whenever the faces change (which concerns 4 usual faces in both the roman family and the sans family). Basically, I was wondering if xCJKecglue could be specified in a “face-aware” way.

\documentclass{article}
\usepackage{xeCJK} % loads fontspec internally

\setmainfont{TeX Gyre Schola} % Latin fonts specification

% Regular and italic faces have digit width 556/1000
\xeCJKsetup{ xCJKecglue = {\hskip 0.222em plus 0.05em minus 0.05em\relax} }

% Bold and bold italic faces have digit width 574/1000
%\xeCJKsetup{ xCJKecglue = {\hskip 0.213em plus 0.05em minus 0.05em\relax} }

\begin{document}
\huge
\begin{tabular}{ll}
第一章\rule[-0.2em]{0.4pt}{1em} & \bfseries 第一章\rule[-0.2em]{0.4pt}{1em} \\
第1章\rule[-0.2em]{0.4pt}{1em} & \bfseries 第1章\rule[-0.2em]{0.4pt}{1em} \\
\end{tabular}
\end{document}

misaligned

Note: The above MWE is minimal so it is hard to observe the misalignment for the boldface. The misalignment would be much more visible when switching to the sans family (with a very different digit width of course).


Previous attempt: There is half a solution that I have used in the past: Without setting xCJKecglue, xeCJK will insert a normal Latin space by default. So in principle I could modify \fontdimen2 through the WordSpace option of fontspec; that is, I could do something like

\setmainfont{TeX Gyre Schola}[
  UprightFeatures={
    WordSpace=...
  },
  BoldFeatures={
    WordSpace=...
  },
  ...
]

This works fine if the inter-word space is not distorted too much (e.g., when pairing Source Han Sans with FiraGO, I’d shrink the inter-word space to 85%–98%). But for most other pairings, Latin fonts usually need to be scaled up first, which leads to a distortion of inter-word space around 75% — this no doubt destroys the color of Latin phrases/sentences inserted into the document.

  • 1
  • @AlanMunn Thank you for the pointer. From what I can understand, the post provides a way to “detect” current face. Any idea on “adding something to a hook (that probably does not exist)”? BTW, I’ve added my previous attempt which is to distort the normal Latin space (not recommended TBH). – Ruixi Zhang May 11 at 3:25
  • Would the Unicode fullwidth digits, such as 1 (U+FF11), work for you? – Davislor May 13 at 4:47
  • There are other packages that make this task simpler, but I don’t know if that’s relevant to you. – Davislor May 13 at 4:50
  • @Davislor I'd like to stick with ASCII digits if possible as they will be rendered in Latin fonts instead of CJK fonts. – Ruixi Zhang May 13 at 12:14
3

The solution consists of 3 parts.

First, Detect which text “mode” (normal, italic, bold, etc.) is currently in use?, as pointed by @AlanMunn (thanks!), provides a way of identifying the current font attributes. Some nested conditionals can be used to treat the different faces separately. I’ll use the XeTeX primitive \strcmp to perform string comparison. The order of logical tests should be \f@family, then \f@series and finally \f@shape, with decreasing probabilities of different digit widths.

\newcommand*\setCJKecglue{%
  \ifnum\strcmp{\f@family}{\rmdefault}=0 %
    % Code for the roman family
    % \ifnum\strcmp{\f@series}{\mddefault}=0 %
    %   \ifnum\strcmp{\f@shape}{\updefault}=0 %
    %     % Code for roman regular
    %   \else
    %     % Code for roman italic
    %   \fi
    % \else
    %   \ifnum\strcmp{\f@shape}{\updefault}=0 %
    %     % Code for roman bold
    %   \else
    %     % Code for roman bolditalic
    %   \fi
    % \fi
  \else
    \ifnum\strcmp{\f@family}{\sfdefault}=0 %
      % Code for the sans-serif family
    \else
      % Code for the mono-spaced family
    \fi
  \fi
}

Second, we can then add the nested conditionals to the hook provided by the everysel package. Namely, \EverySelectfont{\setCJKecglue}.

Third, although xeCJK can handle various spacing issues, there are exceptions. One such exception is that phrases like 图~\ref{fig:example} 展示了 (meaning Figure~\ref{fig:example} shows that) will end up with normal Latin spaces around the reference number. One should instead use 图\nobreak\CJKecglue\ref{fig:example}\CJKecglue 展示了. Furthermore, when the reference is followed by a punctuation mark (a full stop, for instance), one should use 图\nobreak\CJKecglue\ref{fig:example}。 (without the second \CJKecglue).

Putting everything together, we have

\documentclass{article}
\usepackage{xeCJK} % loads fontspec internally
\setmainfont{TeX Gyre Schola}
\setsansfont{TeX Gyre Adventor}
\setmonofont{TeX Gyre Cursor}

\makeatletter
\def\setCJKecglue@nnn#1#2#3{%
  \xeCJKsetup
    { xCJKecglue = {\hskip #1em plus #2em minus #3em\relax} }%
}
\newcommand*\setCJKecglue{%
  \ifnum\strcmp{\f@family}{\rmdefault}=0 %
    \ifnum\strcmp{\f@series}{\mddefault}=0 %
      \ifnum\strcmp{\f@shape}{\updefault}=0 %
        \setCJKecglue@nnn{0.09}{0.045}{0.03}%
      \else
        \setCJKecglue@nnn{0.5}{0.25}{0.167}%
      \fi
    \else
      \ifnum\strcmp{\f@shape}{\updefault}=0 %
        \setCJKecglue@nnn{1}{0.5}{0.333}%
      \else
        \setCJKecglue@nnn{1.5}{0.75}{0.5}%
      \fi
    \fi
  \else
    \ifnum\strcmp{\f@family}{\sfdefault}=0 %
      \ifnum\strcmp{\f@series}{\mddefault}=0 %
        \setCJKecglue@nnn{0.6}{0.3}{0.2}%
      \else
        \setCJKecglue@nnn{0.9}{0.45}{0.3}%
      \fi
    \else
      \setCJKecglue@nnn{0.2}{0.1}{0.067}%
    \fi
  \fi
}
\makeatother

\usepackage{everysel}
\EverySelectfont{\setCJKecglue}

\newcommand\test{%
  \begin{tabular}{llll}
  第1章 & \itshape 第1章 & \bfseries 第1章 & \bfseries\itshape 第1章 \\
  第 1 章 & \itshape 第 1 章 & \bfseries 第 1 章 & \bfseries\itshape 第 1 章 \\
  \end{tabular}%
}
\newcommand*\figref[1]{%
  图\nobreak\CJKecglue\ref{#1}\CJKecglue\ignorespaces
}
\newcommand*\figrefatend[1]{%
  图\nobreak\CJKecglue\ref{#1}\ignorespaces
}

\begin{document}
\LARGE    \test\par
\sffamily \test\par
\ttfamily \test\par
\rmfamily
\begin{figure}
\caption{A figure to be referenced.}\label{fig:example}
\end{figure}
Type ``1'': Figure~1 shows that\dots\par
图~1 展示了……(unbalanced spaces)\par
图\nobreak\CJKecglue1 展示了……(balanced spaces)\par
Use labels: Figure~\ref{fig:example} shows that\dots\par
图~\ref{fig:example} 展示了……(Latin spaces)\par
\figref{fig:example} 展示了……(correct spaces)\par
See Figure~\ref{fig:example}.\par
见\figref{fig:example}。(wrong space before full stop)\par
见 \figrefatend{fig:example} 。(no space before full stop)
\end{document}

CJKecglue

Special thanks to Qing Lee (maintainer of xeCJK) for suggesting everysel.

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.