|
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 |