Is there any package which can convert numbered pinyin (e.g. dian4 nao3) to UTF-8 pinyin with tone marks (e.g. diàn​ nǎo)?

I found this (https://stackoverflow.com/a/8200388/2421048) Python script, so it should be possible with LuaLaTeX using directlua.


I have translated the Python script mentioned in the question to Lua. Compile it with LuaLaTex and it should work:


PinyinToneMark = {
  {'ā', 'á', 'ǎ', 'à'},
  {'ē', 'é', 'ě', 'è'},
  {'ī', 'í', 'ǐ', 'ì'},
  {'ō', 'ó', 'ǒ', 'ò'},
  {'ū', 'ú', 'ǔ', 'ù'},
  {'ǖ', 'ǘ', 'ǚ', 'ǜ'}

function convertPinyin(str)
  if str~= nil and string.len(str)>0 then
  local s = string.lower(str)
  local r = ''
  local t = ''
  for i = 1, string.len(s) do
    local c = s:sub(i,i)
    if c >= 'a' and c <= 'z' then
      t = t .. c
    elseif c >= '0' and c <= '5' then
        local tone = tonumber(c)
        if tone ~= 0 then
          if string.find(t, 'a') ~= nil then
              t = string.gsub(t, "a", PinyinToneMark[1][tone])
          elseif string.find(t, 'e') ~= nil then
              t = string.gsub(t, "e", PinyinToneMark[2][tone])
          elseif string.find(t, 'i') ~= nil then
              t = string.gsub(t, "i", PinyinToneMark[3][tone])
          elseif string.find(t, 'o') ~= nil then
              t = string.gsub(t, "o", PinyinToneMark[4][tone])
          elseif string.find(t, 'u') ~= nil then
              t = string.gsub(t, "u", PinyinToneMark[5][tone])
          elseif string.find(t, 'v') ~= nil then
              t = string.gsub(t, "v", PinyinToneMark[6][tone])
      r = r .. t
      t = ""

 \directlua{convertPinyin("dian4 nao3")}

This version handles only v and not u: or ü.

  • This isn’t going to work for ü since it’s two bytes in utf. You’ll have to resort to the unicode libary or just use string.utfcharacters() right away. – Philipp Gesang Jul 23 '13 at 11:36

If you want to use xeCJK package, XeLaTeX is necessary instead of LuaLaTeX.

You can use xpinyin package for pinyin.


电脑 \pinyin{dian4 nao3}


You will get

电脑 diàn nǎo

In Luatex, the conversion can be achieved without extra packages. Below is some example code, operating on input strings. Note that I don’t know anything about Chinese in general, nor Pinyin in particular. All I did was follow the WP entry which seemed fairly straightforward. Thus I expect the conversion to require further tweaking. If it produces incorrect results then please extend your question with more examples.

Here is the main TeX document (in Plain, but it should translate to Latex):

%% load some font that covers the diacritic marks
\input luaotfload.sty
\font\diacritics = "file:lmroman10-regular.otf:mode=node" at 10pt

%% --------------------------------------------------------------------
%% load conversion routines; adjust filename here
\directlua{dofile "\jobname.lua"}

%% wrap converter in a TeX macro
  %% switch to appropriate hyphenation pattern goes here
  \directlua{packagedata.pinyintones.convert ([==[#1]==])}%

%% --------------------------------------------------------------------
%% demo

\def\showtest#1{((#1) (\convertpinyin{#1}))\par}

\def\testa{dian4 nao3}
\def\testb{ma ma1 ma2 ma3 ma4}



It loads a Lua file of the same name, but you can change the call to dofile() to something that fits your setup. Here is the code:

local utf                 = utf or require "unicode.utf8"
local lpeg                = require "lpeg"

local unpack              = unpack or table.unpack
local type                = type
local iowrite             = io.write
local stringformat        = string.format
local tableconcat         = table.concat
local utfchar             = utf.char
local texsprint           = tex.sprint

local C, Cg, Ct           = lpeg.C, lpeg.Cg, lpeg.Ct
local P, R, S, lpegmatch  = lpeg.P, lpeg.R, lpeg.S, lpeg.match

packagedata               = packagedata or { }
packagedata.pinyintones   = packagedata.pinyintones or { }
local pinyintones         = packagedata.pinyintones

---                           conversion

local toneno      = R"05"
local consonant   = S"bcdfghjklmnpqrstvwxyz" + S"BCDFGHJKLMNPQRSTVWXYZ"
local vowel       = S"aeiou" + S"AEIOU" + "ü" + "Ü"
local nucleus     = Ct(C(vowel)^1)
local syllable    = Cg((consonant^1)^-1, "onset")
                  * Cg(nucleus,          "nucleus")
                  * Cg((consonant^1)^-1, "coda")
                  * Cg(toneno^-1,        "tone")
local skip        = (1 - syllable)^1 --- keep this stuff
local pinyin      = Ct((Ct(syllable) + C(skip))^1)

local vowelpositions = function (nucleus)
  local tmp = { }
  for pos, vowel in next, nucleus do
    tmp[vowel] = pos
  return tmp

local precedence = { "a", "e", "o" }

local cmacron = utfchar (0x0304)
local cacute  = utfchar (0x0301)
local ccaron  = utfchar (0x030c)
local cgrave  = utfchar (0x0300)

local todiacritic = function (str)
  local result   = { }
  local analyzed = lpegmatch (pinyin, str)
  --inspect (analyzed)
  for i = 1, #analyzed do
    local elm = analyzed[i]
    local t   = type (elm)

    if t == "table" then --- syllable
      local nucleus  = elm.nucleus
      local nvowels  = #nucleus
      local tone     = elm.tone
      if tone then
        tone = tonumber (tone)

      if not tone then --- add unmodified
        result[#result+1] = elm.onset
        result[#result+1] = tableconcat (nucleus)
        result[#result+1] = elm.coda
        local tonified
        if nvowels == 1 then --- single vowel receives tone
          local vowel = nucleus[1]
          if tone == 1 then --- inlined for performance
            tonified = vowel .. cmacron
          elseif tone == 2 then
            tonified = vowel .. cacute
          elseif tone == 3 then
            tonified = vowel .. ccaron
          elseif tone == 4 then
            tonified = vowel .. cgrave
            tonified = vowel

        elseif nvowels > 1 then
          local positions = vowelpositions (nucleus)
          local pos

          --- 1) locate correct vowel
          for j = 1, 3 do
            pos = positions[precedence[j]]
            if pos then
          if not pos then -- iu or ui, thus second gets tone
            pos = 2

          local vowel = nucleus[pos]

          --- 2) place tone mark
          if tone == 1 then
            nucleus[pos] = vowel .. cmacron
          elseif tone == 2 then
            nucleus[pos] = vowel .. cacute
          elseif tone == 3 then
            nucleus[pos] = vowel .. ccaron
          elseif tone == 4 then
            nucleus[pos] = vowel .. cgrave

          tonified = tableconcat (nucleus)
        else --- no vowel, could be mismatch
          tonified = ""
        result[#result+1] = elm.onset
        result[#result+1] = tonified
        result[#result+1] = elm.coda

    elseif t == "string" then
      result[#result+1] = elm

  return tableconcat (result)

--- export

pinyintones.convert = function (str)
  local converted = todiacritic (str)
  if converted then
    texsprint (converted)


pinyin conversion demo

For convenience, I created a gist of the code.

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.