Skip to content

Instantly share code, notes, and snippets.

@zr-tex8r
Last active November 29, 2025 03:43
Show Gist options
  • Select an option

  • Save zr-tex8r/fa1fc93e51396fb21f8a81063d654e83 to your computer and use it in GitHub Desktop.

Select an option

Save zr-tex8r/fa1fc93e51396fb21f8a81063d654e83 to your computer and use it in GitHub Desktop.
LaTeX: DVIファイルに素敵⛄なページを追加する

scextenddvi

LaTeX: DVIファイルに素敵⛄なページを追加する

入力DVIファイルに対して本質的な内容をもつページを追加して出力する。この際に、指定したページ数の条件を満たすような最小数のページが追加される。

使用法

texlua scextenddvi.lua [«オプション»…] «入力パス» «ページ数» [«出力パス»]
  • [«出力パス»は上書き指定(-o)またはページ数表示モード(-c)では必ず省略し、それ以外では必ず指定する。
  • «ページ数»«B»«A»n«A»n+«B»«A»n-«B»の何れかの形式で指定する(«A»«B»は非負整数でnは文字通りに書く)。指定の形式でnに非負整数を当てはめた時の整数値が目標ページ数の候補となり、この中で入力DVIのページ数以上の最小の値が選択される。

オプション:

  • -o/--overwrite: 上書き指定。«入力パス»のファイルを更新する。
  • -n/--nice:(既定) 本質的で素敵なページを追加する。
  • -N/--no-nice-nの否定で、非本質的な白紙のページを追加する。
  • --muffler=«色»,...: マフラーの色を指定する。コンマ区切りで複数値が指定可能で、新しいページごとに指定した色が順番に使われる。色はCSS Color Module Level 5の規定された<color>値で指定するが、使用可能な書式は以下のタイプに限られる。既定値はred
    • RGB Hexadecimal Notations(#RRGGBB
    • rgb()関数
    • device-cmyk()関数
    • 色キーワード(redgreenなど)
  • -c/--count: ページ数表示モード。入力DVIファイルのページ数を標準出力に出力して終了する。
  • -v/--verbose: メッセージを増やす。
  • -q/--quiet: メッセージを減らす。
  • -V/--version: バージョン表示
  • -h/--help: ヘルプ表示

更新履歴

  • Version 0.2.0 <2025/11/29>
    • 最初の公開版。

Takayuki YATO (aka. "ZR")
https://github.com/zr-tex8r

program = 'scextenddvi'
version = '0.2.0'
mod_date = '2025/11/29'
---------------------------------------- preparations
verbose = 0
mode = 'extend'
nice = true
page_a, page_b = nil
muffler, scscale = 'red', 0.7
in_path, out_path = nil
local pack, unpack = string.pack, string.unpack
local insert, concat = table.insert, table.concat
---------------------------------------- logging
do
local function log(...)
local t = {program, ...}
for i = 1, #t do t[i] = tostring(t[i]) end
io.stderr:write(concat(t, ": ").."\n")
end
function info(...)
if verbose >= 1 then log(...) end
end
function alert(...)
if verbose >= 0 then log('WARNING', ...) end
end
function abort(...)
log('ERROR', ...)
os.exit(1)
end
function sure(val, ...)
if val then return val, ... end
abort(...)
end
end
---------------------------------------- helpers
do
-- zero-page (pseudo-)DVI dump
null_dvi = string.char(
247, 2, 1, 131, 146, 192, 28, 59, 0, 0, 0, 0, 3, 232, 1, 32,
248, 255, 255, 255, 255, 1, 131, 146, 192, 28, 59, 0, 0, 0, 0, 3,
232, 2, 232, 82, 248, 1, 240, 200, 254, 0, 8, 0, 0, 249, 0, 0,
0, 16, 2, 223, 223, 223, 223, 223
)
color_names = {
aliceblue = 'f0f8ff', antiquewhite = 'faebd7', aqua = '00ffff',
aquamarine = '7fffd4', azure = 'f0ffff', beige = 'f5f5dc', bisque =
'ffe4c4', black = '000000', blanchedalmond = 'ffebcd', blue = '0000ff',
blueviolet = '8a2be2', brown = 'a52a2a', burlywood = 'deb887',
cadetblue = '5f9ea0', chartreuse = '7fff00', chocolate = 'd2691e',
coral = 'ff7f50', cornflowerblue = '6495ed', cornsilk = 'fff8dc',
crimson = 'dc143c', cyan = '00ffff', darkblue = '00008b', darkcyan =
'008b8b', darkgoldenrod = 'b8860b', darkgray = 'a9a9a9', darkgreen =
'006400', darkgrey = 'a9a9a9', darkkhaki = 'bdb76b', darkmagenta =
'8b008b', darkolivegreen = '556b2f', darkorange = 'ff8c00', darkorchid =
'9932cc', darkred = '8b0000', darksalmon = 'e9967a', darkseagreen =
'8fbc8f', darkslateblue = '483d8b', darkslategray = '2f4f4f',
darkslategrey = '2f4f4f', darkturquoise = '00ced1', darkviolet =
'9400d3', deeppink = 'ff1493', deepskyblue = '00bfff', dimgray =
'696969', dimgrey = '696969', dodgerblue = '1e90ff', firebrick =
'b22222', floralwhite = 'fffaf0', forestgreen = '228b22', fuchsia =
'ff00ff', gainsboro = 'dcdcdc', ghostwhite = 'f8f8ff', gold = 'ffd700',
goldenrod = 'daa520', gray = '808080', green = '008000', greenyellow =
'adff2f', grey = '808080', honeydew = 'f0fff0', hotpink = 'ff69b4',
indianred = 'cd5c5c', indigo = '4b0082', ivory = 'fffff0', khaki =
'f0e68c', lavender = 'e6e6fa', lavenderblush = 'fff0f5', lawngreen =
'7cfc00', lemonchiffon = 'fffacd', lightblue = 'add8e6', lightcoral =
'f08080', lightcyan = 'e0ffff', lightgoldenrodyellow = 'fafad2',
lightgray = 'd3d3d3', lightgreen = '90ee90', lightgrey = 'd3d3d3',
lightpink = 'ffb6c1', lightsalmon = 'ffa07a', lightseagreen = '20b2aa',
lightskyblue = '87cefa', lightslategray = '778899', lightslategrey =
'778899', lightsteelblue = 'b0c4de', lightyellow = 'ffffe0', lime =
'00ff00', limegreen = '32cd32', linen = 'faf0e6', magenta = 'ff00ff',
maroon = '800000', mediumaquamarine = '66cdaa', mediumblue = '0000cd',
mediumorchid = 'ba55d3', mediumpurple = '9370db', mediumseagreen =
'3cb371', mediumslateblue = '7b68ee', mediumspringgreen = '00fa9a',
mediumturquoise = '48d1cc', mediumvioletred = 'c71585', midnightblue =
'191970', mintcream = 'f5fffa', mistyrose = 'ffe4e1', moccasin =
'ffe4b5', navajowhite = 'ffdead', navy = '000080', oldlace = 'fdf5e6',
olive = '808000', olivedrab = '6b8e23', orange = 'ffa500', orangered =
'ff4500', orchid = 'da70d6', palegoldenrod = 'eee8aa', palegreen =
'98fb98', paleturquoise = 'afeeee', palevioletred = 'db7093',
papayawhip = 'ffefd5', peachpuff = 'ffdab9', peru = 'cd853f', pink =
'ffc0cb', plum = 'dda0dd', powderblue = 'b0e0e6', purple = '800080',
rebeccapurple = '663399', red = 'ff0000', rosybrown = 'bc8f8f',
royalblue = '4169e1', saddlebrown = '8b4513', salmon = 'fa8072',
sandybrown = 'f4a460', seagreen = '2e8b57', seashell = 'fff5ee',
sienna = 'a0522d', silver = 'c0c0c0', skyblue = '87ceeb', slateblue =
'6a5acd', slategray = '708090', slategrey = '708090', snow = 'fffafa',
springgreen = '00ff7f', steelblue = '4682b4', tan = 'd2b48c', teal =
'008080', thistle = 'd8bfd8', tomato = 'ff6347', turquoise = '40e0d0',
violet = 'ee82ee', wheat = 'f5deb3', white = 'ffffff', whitesmoke =
'f5f5f5', yellow = 'ffff00', yellowgreen = '9acd32',
}
function parse_color(input)
local s = input:lower():gsub('[,/]', ' '):gsub('%s+', ' ')
s = s:gsub('^ ', ''):gsub(' $', '')
local arg, ccvs, max, t, u = color_names[s], {}
if s:match('^#[0-9a-f]+$') then arg = s:sub(2) end
if arg then
if #arg <= 4 then arg = arg:gsub('(.)', '%1%1') end
if #arg == 8 then arg = arg:sub(1, 6) end
if #arg ~= 6 then return end
for v in arg:gmatch('(..)') do
ccvs[#ccvs+1] = tonumber(v, 16) / 255
end
else
s, arg = s:match('^(%w+) ?%((.*)%)$')
for v in (arg or ''):gmatch('%S+') do
ccvs[#ccvs+1] = v
end
if s == 'rgb' or s == 'rgba' then t, max = 3, 255
elseif s == 'cmyk' then t, max = 4, 1
else return
end
if #ccvs == t + 1 then ccvs[t + 1] = nil end
if #ccvs ~= t then return end
for i, v in ipairs(ccvs) do
v, u = v:gsub('%%$', '')
if not tonumber(v) then return end
v = tonumber(v) / ((u > 0) and 100 or max)
ccvs[i] = (v < 0) and 0.0 or (v > 1) and 1.0 or v
end
end
if #ccvs == 3 and ccvs[1] == ccvs[2] and ccvs[2] == ccvs[3] then
ccvs[2], ccvs[3] = nil, nil
end
return ccvs
end
function aux_file_path()
local ext = in_path:lower():match('%.dvi$')
return in_path:sub(1, #in_path - ((ext) and 4 or 0))..'.aux'
end
function chars_to_string(chars)
local chunks = {}
for i, c in ipairs(chars) do
chunks[i] = string.char((33 <= c and c <= 126) and c or 32)
end
return concat(chunks)
end
function dump_dvi_info(dinfo)
for k, v in pairs(dinfo) do
info(k, (type(v) == 'number') and v or '['..type(v)..']')
end
end
end
---------------------------------------- parse DVI
do
-- Opcode table
-- An integer means the skip length of that opcode.
local op_table = {} -- 0-127 is set_char_N, 171-234 is fnt_num_N
for c = 0, 255 do op_table[c] = 0 end
for i, op in ipairs({ -- 128-170
1, 2, 3, 4, 8, 1, 2, 3, 4, 8, 0, 44, 0, 0, 0, -- 128
1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, -- 143
1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, -- 157
}) do op_table[i + 127] = op end
for i, op in ipairs({-- 235-255
1, 1, 1, 1, {'sp',1}, {'sp',2}, {'sp',3}, {'sp',4}, -- 235
{'fd',1}, {'fd',2}, {'fd',3}, {'fd',4}, -- 243
{'pre'}, {'post'}, {'pp'}, {}, {}, {}, {}, {}, 1, -- 247
}) do op_table[i + 234] = op end
local function make_reader(source)
return function(fmt, ...)
local v = { unpack(fmt, source, ...) }
return v[#v], v
end
end
-- Parse DVI data and return the DVI-info.
function parse_dvi(source)
local sr, pos, v, npos = make_reader(source), 1
npos, v = sr('B')
sure(v[1] == 247, "unexpected format")
local di = {
source = source, specials = {}, text = {}, extras = nil,
last_bop = nil, post = nil, postpost = nil, -- offsets
pages = nil, width = nil, height = nil,
}
while true do
npos, v = sr('B', pos)
local e = op_table[v[1]]
if type(e) == 'number' then -- e = skip length
-- info("op", v[1], e)
pos = npos + e
-- if v[1] < 128 then insert(di.text, v[1]) end
else -- e is table
local op, ver = e[1], e[2]
sure(op, "invalid opcode", v[1])
-- info("op", op, ver)
if op == 'pre' then
sure(pos == 1, "unexpected pre")
npos, v = sr('>BLLLs1', npos)
sure(v[1] == 2 or v[1] == 3, "unknown pre-ID", v[1])
sure(v[2] == 25400000 or v[3] == 473628672, -- TeX setting
"unknown unit", v[2], v[3])
sure(1 <= v[4] and v[4] <= 32767, -- valid in TeX
"unexpected mag value", v[4])
elseif op == 'post' then
di.post = pos - 1
npos, v = sr('>Lc12LLHH', npos)
di.last_bop, di.pages = v[1], v[6]
di.height, di.width = v[3], v[4]
elseif op == 'pp' then -- post_post
di.postpost = pos - 1
npos, v = sr('>LB', npos)
sure(di.post == v[1], "post offset inconsistent")
local trail = source:sub(npos)
sure(#trail >= 4 and trail == ('\223'):rep(#trail),
"bad trail bytes")
break
elseif op == 'sp' then -- xxx (special)
npos, v = sr('>s1', npos)
insert(di.specials, v[1])
elseif op == 'fd' then -- fnt_def
npos, v = sr('BB', npos + ver + 12)
npos = npos + v[1] + v[2]
else abort("???") -- unreachable
end
pos = npos
end
end
sure(di.last_bop, "missing postamble")
sure(di.post, "missing post-postamble")
return di
end
end
---------------------------------------- construct DVI
do
local function replace(str, offset, new)
return str:sub(1, offset - 1)..new..str:sub(offset + #new)
end
function construct_dvi(di)
-- Add bop and eop.
local lbop, pos, chunks = di.last_bop, di.post, {}
for i, page in ipairs(di.extras) do
local bop, eop = pack('>Bc40L', 139, '', lbop), '\140'
local chunk = bop..page..eop
insert(chunks, chunk)
lbop, pos = pos, pos + #chunk
end
-- Merge with original source.
local ns = replace(di.source, di.post + 2, pack('>L', lbop))
local pages = di.pages + #di.extras
ns = replace(ns, di.post + 28, pack('>H', pages))
ns = replace(ns, di.postpost + 2, pack('>L', pos))
local ppost = ns:sub(di.post + 1):gsub('\223+$', '')
ns = ns:sub(1, di.post)..concat(chunks)..ppost
return ns..('\223'):rep(7 - (#ns - 1) % 4) -- trail
end
end
---------------------------------------- extra page data
do
local units = { -- in DVI-units
pt = 65536, pc = 786432, bp = 65781, sp = 1,
cm = 1864679, mm = 186467, ['in'] = 4736286, dd = 70124, cc = 841489,
}
local op = {
PUSH = 141, POP = 142, RIGHT4 = 146, DOWN4 = 160, XXX2 = 240,
}
local function psnumber(value)
return ('%.4f'):format(value):gsub('0+$', ''):gsub('%.$', '')
end
local function pssetcolor(color)
if type(color) == 'string' then
sure(color_names[color], "unknown color name", color)
color = color_names[color]
end
local cc = {}
for i = 1, #color do
cc[i] = psnumber(color[i])
end
cc[#cc + 1] = (#color == 1) and 'setgray' or
(#color == 3) and 'setrgbcolor' or 'setcmykcolor'
return concat(cc, ' ')
end
-- Parse a length value and return it in DVI-units.
local function length(text)
local factor = tonumber(text:sub(1, #text-2))
local unit = units[text:sub(#text-1):lower()]
if not factor or not unit then return end
return math.floor(factor * unit + 0.5)
end
function target_page_count(incount)
if page_a == 0 then
if page_b < incount then
alert("target page count is less than current",
("%d < %d"):format(page_b, incount))
return incount
end
return page_b
end
local n = (incount - page_b) / page_a
n = math.max(0, math.ceil(n))
return page_a * n + page_b
end
local function parse_papersize_special(text)
if text:match('%w %w') then return end
text = text:gsub(' ', '')
local width, height = text:match('^papersize=([.%d]+%w%w),([.%d]+%w%w)$')
width, height = length(width), length(height)
if not width or not height then return end
return width, height
end
local function parse_pagesize_special(text)
text = text:gsub(' +', ' ')
local p = '^ ?pdf: ?pagesize width ([.%d]+%w%w) height ([.%d]+%w%w) ?$'
local width, height = text:match(p)
width, height = length(width), length(height)
if not width or not height then return end
return width, height
end
local function guess_paper_size(inwidth, inheight)
return -- TODO
end
function get_paper_size(dinfo)
for i, v in ipairs(dinfo.specials) do
local width, height = parse_papersize_special(v)
if width and height then
return width, height
end
local width, height = parse_pagesize_special(v)
if width and height then
return width, height
end
end
alert("cannot find papersize info")
local width, height = guess_paper_size(dinfo.width, dinfo.height)
if width and height then
return width, height
end
return 39158276, 55380990 -- A4 size
end
local nice_ps1 = ([[
$SCALE $SCALE scale
0 setgray 0.139 setlinewidth
1 setlinejoin 1 setlinecap
5 7.2 moveto
6.4 7.2 7.6 6.5 7.6 5.5 curveto
7.6 5.1 7.2 4.7 6.7 4.4 curveto
7.9 4.1 8.4 3.2 8.4 2.5 curveto
8.4 1.3 7.5 0.8 6.8 0.8 curveto
3.2 0.8 lineto
2.5 0.8 1.6 1.3 1.6 2.5 curveto
1.6 3.2 2.1 4.1 3.3 4.4 curveto
2.8 4.7 2.4 5.1 2.4 5.5 curveto
2.4 6.5 3.6 7.2 5 7.2 curveto
closepath stroke
4.2 5.6 moveto
4.2 5.766 4.11 5.9 4 5.9 curveto
3.889 5.9 3.8 5.766 3.8 5.6 curveto
3.8 5.434 3.889 5.3 4 5.3 curveto
4.11 5.3 4.2 5.434 4.2 5.6 curveto
closepath fill
6.2 5.6 moveto
6.2 5.766 6.111 5.9 6 5.9 curveto
5.89 5.9 5.8 5.766 5.8 5.6 curveto
5.8 5.434 5.89 5.3 6 5.3 curveto
6.111 5.3 6.2 5.434 6.2 5.6 curveto
closepath fill
4 4.8 moveto
4.5 4.5 5.5 4.5 6 4.8 curveto
stroke
5.8 9 moveto
7.7 8.1 lineto
7.4 6.1 lineto
6.6 6 5 6.6 4.6 7.2 curveto
5.8 9 lineto
closepath
gsave fill grestore gsave stroke grestore newpath
2 3.1 moveto
1.9 3.3 1.4 4.1 1.3 4.2 curveto
1.2 4.3 1 4.3 0.7 4.4 curveto
0.4 4.6 0.6 4.6 0.8 4.6 curveto
0.9 4.6 1.1 4.4 1.2 4.4 curveto
1.4 4.6 1.4 4.7 1.5 4.9 curveto
1.6 5.1 1.6 4.9 1.6 4.8 curveto
1.6 4.6 1.4 4.4 1.5 4.3 curveto
1.6 4.2 2.1 3.5 2.2 3.3 curveto
2.3 3.1 2.1 3 2 3.1 curveto
closepath
gsave fill grestore gsave stroke grestore newpath
8 3.1 moveto
8.1 3.3 8.6 4.1 8.7 4.2 curveto
8.8 4.3 9 4.3 9.3 4.4 curveto
9.6 4.6 9.4 4.6 9.2 4.6 curveto
9.1 4.6 8.9 4.4 8.8 4.4 curveto
8.6 4.6 8.6 4.7 8.5 4.9 curveto
8.4 5.1 8.4 4.9 8.4 4.8 curveto
8.4 4.6 8.6 4.4 8.5 4.3 curveto
8.4 4.2 7.9 3.5 7.8 3.3 curveto
7.7 3.1 7.9 3 8 3.1 curveto
closepath
gsave fill grestore gsave stroke grestore newpath
2.7 4.8 moveto
4.2 3.8 5.8 3.8 7.3 4.8 curveto
7.5 4.6 7.6 4.4 7.7 4.1 curveto
7.7 3.9 7.5 3.7 7.3 3.6 curveto
7.4 3.3 7.4 3.1 7.6 2.6 curveto
7.5 2.5 7.2 2.4 6.6 2.3 curveto
6.6 2.7 6.5 3 6.3 3.4 curveto
4.2 3 3.2 3.5 2.4 4.1 curveto
2.5 4.5 2.6 4.7 2.7 4.8 curveto
closepath
$MUFFLER
gsave fill grestore gsave stroke grestore newpath
0 setgray
5 1.6 moveto
5.3 1.6 moveto
5.3 1.766 5.166 1.9 5 1.9 curveto
4.834 1.9 4.7 1.766 4.7 1.6 curveto
4.7 1.434 4.834 1.3 5 1.3 curveto
5.166 1.3 5.3 1.434 5.3 1.6 curveto
closepath
gsave fill grestore gsave stroke grestore newpath
5 2.6 moveto
5.3 2.6 moveto
5.3 2.766 5.166 2.9 5 2.9 curveto
4.834 2.9 4.7 2.766 4.7 2.6 curveto
4.7 2.434 4.834 2.3 5 2.3 curveto
5.166 2.3 5.3 2.434 5.3 2.6 curveto
closepath
gsave fill grestore gsave stroke grestore newpath
1.1 2.8 moveto
1.1 3.021 0.921 3.2 0.7 3.2 curveto
0.479 3.2 0.3 3.021 0.3 2.8 curveto
0.3 2.579 0.479 2.4 0.7 2.4 curveto
0.921 2.4 1.1 2.579 1.1 2.8 curveto
closepath stroke
1.7 5.5 moveto
1.7 5.721 1.521 5.9 1.3 5.9 curveto
1.079 5.9 0.9 5.721 0.9 5.5 curveto
0.9 5.279 1.079 5.1 1.3 5.1 curveto
1.521 5.1 1.7 5.279 1.7 5.5 curveto
closepath stroke
1.2 6.8 moveto
1.2 7.021 1.021 7.2 0.8 7.2 curveto
0.579 7.2 0.4 7.021 0.4 6.8 curveto
0.4 6.579 0.579 6.4 0.8 6.4 curveto
1.021 6.4 1.2 6.579 1.2 6.8 curveto
closepath stroke
2.7 7.6 moveto
2.7 7.821 2.521 8 2.3 8 curveto
2.079 8 1.9 7.821 1.9 7.6 curveto
1.9 7.379 2.079 7.2 2.3 7.2 curveto
2.521 7.2 2.7 7.379 2.7 7.6 curveto
closepath stroke
4.6 8.9 moveto
4.6 9.121 4.421 9.3 4.2 9.3 curveto
3.979 9.3 3.8 9.121 3.8 8.9 curveto
3.8 8.679 3.979 8.5 4.2 8.5 curveto
4.421 8.5 4.6 8.679 4.6 8.9 curveto
closepath stroke
7.8 8.9 moveto
7.8 9.121 7.621 9.3 7.4 9.3 curveto
7.179 9.3 7 9.121 7 8.9 curveto
7 8.679 7.179 8.5 7.4 8.5 curveto
7.621 8.5 7.8 8.679 7.8 8.9 curveto
closepath stroke
9.2 7.3 moveto
9.2 7.521 9.021 7.7 8.8 7.7 curveto
8.579 7.7 8.4 7.521 8.4 7.3 curveto
8.4 7.079 8.579 6.9 8.8 6.9 curveto
9.021 6.9 9.2 7.079 9.2 7.3 curveto
closepath stroke
9.6 5.3 moveto
9.6 5.521 9.421 5.7 9.2 5.7 curveto
8.979 5.7 8.8 5.521 8.8 5.3 curveto
8.8 5.079 8.979 4.9 9.2 4.9 curveto
9.421 4.9 9.6 5.079 9.6 5.3 curveto
closepath stroke
9.8 2.3 moveto
9.8 2.521 9.621 2.7 9.4 2.7 curveto
9.179 2.7 9 2.521 9 2.3 curveto
9 2.079 9.179 1.9 9.4 1.9 curveto
9.621 1.9 9.8 2.079 9.8 2.3 curveto
closepath stroke
]]):gsub('%s+', ' ')
function create_nice_page(dinfo, muffler)
local width, height = get_paper_size(dinfo)
local sclength = math.min(width, height) * scscale
local psscale = psnumber(sclength / units.bp / 10)
local pssource = '\" '..nice_ps1:gsub('$SCALE', psscale)
local psmuffler = pssetcolor(muffler)
pssource = pssource:gsub('$MUFFLER', psmuffler)
local tmargin = 1 * units['in'] -- that 1 inch
local hoffset = math.floor((width - sclength) * 0.5 - tmargin) -- 1:1
local voffset = math.floor((height - sclength) * 0.4 + sclength - tmargin) -- 2:3
return pack('>BBlBlBs2B',
op.PUSH,
op.RIGHT4, hoffset,
op.DOWN4, voffset,
op.XXX2, pssource,
op.POP)
end
function create_extra_pages(dinfo, outpc)
local count, extras = outpc - dinfo.pages, {}
if count <= 0 then
return extras
end
local page = { pack('BB', op.PUSH, op.POP) }
if nice then
for i = 1, #muffler do
page[i] = create_nice_page(dinfo, muffler[i])
end
end
for i = 1, count do
extras[i] = page[(i - 1) % #page + 1]
end
return extras
end
end
---------------------------------------- processor
do
function process_record(dinfo)
local auxpath = aux_file_path()
local auxfile = sure(io.open(auxpath, 'ab'),
"cannot open file for append", auxpath)
local line = '%%'..program..': count = '..dinfo.pages..'\n'
assert(auxfile:write(line))
auxfile:close()
end
function process_extend(dinfo)
info("page spec", ("%dn%+d"):format(page_a, page_b))
local outpc = target_page_count(dinfo.pages)
info("target page count", outpc)
dinfo.extras = create_extra_pages(dinfo, outpc)
local outsrc = construct_dvi(dinfo)
local bak_path
if not out_path then
bak_path = in_path..'.bak'
info("backup file path", bak_path)
sure(os.rename(in_path, bak_path),
"cannot rename file", bak_path)
out_path = in_path
end
info("output file path", out_path)
local outfile = sure(io.open(out_path, 'wb'),
"cannot open file for write", out_path)
assert(outfile:write(outsrc))
outfile:close()
if bak_path then
os.remove(bak_path)
end
end
end
---------------------------------------- main procedure
do
local function show_usage()
io.stdout:write(([[
This is %s v%s <%s> by 'ZR'
Usage: %s[.lua] [OPTION...] IN_PATH PAGE_SPEC [OUT_PATH]
Here PAGE_SPEC can be given in the form <B>, <A>n, <A>n+<B>, or <A>n-<B>.
Options:
-o/--overwrite Overwrite mode
-n/--nice Add nice (essential) pages
-N/--no-nice Add non-nice (empty) pages
-c/--count Show page count and exit
--muffler=COLOR,... Specify muffler color
-v/--verbose Show more messages
-q/--quiet Show fewer messages
-h/--help Show help and exit
-V/--version Show version and exit
]]):format(program, version, mod_date, program))
os.exit(0)
end
local function parse_aux_file()
local auxfile, count = io.open(aux_file_path(), 'rb')
if not auxfile then return end
for line in auxfile:lines('l') do
local m = line:match('^%%%%'..program..': count = (%d+)')
if m then
count = tonumber(m)
end
end
return count
end
local function parse_page_spec(spec)
if spec:match('[0-9] [0-9]') then return end
spec = spec:gsub(' ', '')
if spec:match('[Pp]') then
local count = parse_aux_file()
if not count then
alert("page count is not saved")
return 0, 1
end
info('saved page count', count)
return 0, count
end
if not spec:match('[Nn]') then spec = '0n+'..spec end
if not spec:match('[-+]') then spec = spec..'+0' end
local a, p, b = spec:match('^([0-9]*)[Nn]([-+])([0-9]+)$')
if not a then return end
a = (a == '') and 1 or tonumber(a)
b = ((p == '-') and -1 or 1) * tonumber(b)
return a, b
end
function parse_muffler(input)
local colors, entry, pos, match = {}, '', 1
while pos <= #input + 1 do
match = input:match('^%b()', pos)
if not match then
match = input:match('^[^(,]+', pos)
end
if match then
entry, pos = entry..match, pos + #match
elseif input:match('^%(', pos) then
abort("bad color spec", entry.."(")
else
if entry:match('%S') then
local color = parse_color(entry)
if not color then
abort("ERROR: bad color spec", entry)
end
colors[#colors + 1] = color
end
entry, pos = '', pos + 1
end
end
return colors
end
local function read_option()
muffler = parse_muffler(muffler)
if #arg == 0 then show_usage() end
local idx, overwrite = 1, false
while idx <= #arg do
local opt = arg[idx]
if opt:sub(1, 1) ~= '-' then break end
if opt == '-h' or opt == '--help' then
show_usage()
elseif opt == '-v' or opt == '--verbose' then
verbose = 1
elseif opt == '-q' or opt == '--quiet' then
verbose = -1
elseif opt == '-o' or opt == '--overwrite' then
overwrite = true
elseif opt == '-n' or opt == '--nice' then
nice = true
elseif opt == '-N' or opt == '--no-nice' then
nice = false
elseif opt == '--record' then
mode = 'record'
elseif opt == '-c' or opt == '--count' then
mode = 'count'
elseif opt:match('^--muffler=') then
local arg = opt:sub(11)
muffler = parse_muffler(arg)
else abort("invalid option", opt)
end
idx = idx + 1
end
local ac = (mode == 'count' or mode == 'record') and 1 or
(overwrite) and 2 or 3
sure(#arg == idx + ac - 1,
"wrong number of arguments")
in_path, out_path = arg[idx], arg[idx + 2]
if ac > 1 then
page_a, page_b = parse_page_spec(arg[idx + 1])
sure(page_a, "page spec has bad format", arg[idx + 1])
end
end
function main()
read_option()
info("input file path", in_path)
local insrc = null_dvi
if in_path ~= '/' then
local infile = sure(io.open(in_path, 'rb'),
"cannot open file for read", in_path)
insrc = assert(infile:read('*a'))
infile:close()
end
local dinfo = parse_dvi(insrc)
info("input page count", dinfo.pages)
if mode == 'extend' then
process_extend(dinfo)
elseif mode == 'count' then
io.write(dinfo.pages, "\n")
elseif mode == 'record' then
process_record(dinfo)
end
info("done")
end
end
---------------------------------------- go to main
sure(string.pack, "Lua version is old")
main()
-- EOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment