|
#!/usr/bin/env texlua |
|
-- |
|
-- This is file 'chkdvidriver.lua'. |
|
-- |
|
-- Copyright (c) 2025 Takayuki YATO (aka. "ZR") |
|
-- GitHub: https://github.com/zr-tex8r |
|
-- Twitter: @zr_tex8r |
|
-- |
|
-- This package is distributed under the MIT License. |
|
-- |
|
program = 'chkdvidriver' |
|
version = '0.2.0' |
|
mod_date = '2025-12-25' |
|
---------------------------------------- preparations |
|
verbose = 0 |
|
mode, alarm, workshop = 'check', false, false |
|
expected, in_path, out_path, tex_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(...) end |
|
end |
|
function abort(...) |
|
log('ERROR', ...) |
|
os.exit(1) |
|
end |
|
function sure(val, ...) |
|
if val then return val, ... end |
|
abort(...) |
|
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 |
|
---------------------------------------- check DVI driver |
|
do |
|
function actual_dvi_driver(dinfo) |
|
for _, text in ipairs(dinfo.specials) do |
|
if text == 'header=l3backend-dvips.pro' then |
|
return 'dvips' |
|
end |
|
end |
|
return 'dvipdfmx' |
|
end |
|
|
|
local alarm_message = [[ |
|
|!!!!!!!!!!!!!!!!!!!!! F A I L U R E !!!!!!!!!!!!!!!!!!!!! |
|
| This DVI file is built for dvips, NOT dvipdfmx. |
|
| You must specify the global option 'dvipdfmx'. |
|
| \documentclass[dvipdfmx, ...]{...} |
|
|!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|
]] |
|
|
|
local lws_error_dvipdfmx = [[ |
|
Global driver option is MISSING! Add 'dvipdfmx' to \documentclass option list.]] |
|
local lws_error_dvips = [[ |
|
BAD global driver option! Remove 'dvipdfmx' from \documentclass option list.]] |
|
|
|
|
|
function process_check(expected, actual) |
|
info("expected dvi driver setting", expected) |
|
if expected ~= actual then |
|
alert("FAIL", "actual dvi file setting is", actual) |
|
if alarm and expected == 'dvipdfmx' then |
|
io.stderr:write(alarm_message, "\n") |
|
end |
|
os.exit(2) |
|
end |
|
end |
|
|
|
local function find_documentclass_line() |
|
local tex, current, found = io.open(tex_path), 0 |
|
if not tex then return end |
|
for line in tex:lines() do |
|
current = current + 1 |
|
if line:match('\\documentclass%f[^a-zA-Z]') then |
|
found = current |
|
break |
|
end |
|
end |
|
tex:close() |
|
return found |
|
end |
|
|
|
function process_latexmk(dinfo, expected, actual) |
|
if expected ~= actual then |
|
info("wrong dvi driver setting") |
|
if workshop then |
|
local line = find_documentclass_line() or 1 |
|
local message = (expected == 'dvipdfmx') and lws_error_dvipdfmx or |
|
lws_error_dvips |
|
local s = (dinfo.pages > 1) and 's' or '' |
|
io.stdout:write(([[ |
|
****Message for LaTeX Workshop |
|
Latexmk: applying rule 'latex'... |
|
%s:%s: %s |
|
|
|
Output written on main.dvi (%s page%s, %s bytes). |
|
Latexmk: applying rule 'dvifilter' |
|
****END |
|
]]):format( |
|
tex_path, line, message, dinfo.pages, s, #dinfo.source |
|
)) |
|
end |
|
alert("FAIL", "wrong dvi file setting", |
|
"expected="..expected, "actual="..actual) |
|
if alarm and expected == 'dvipdfmx' then |
|
io.stderr:write(alarm_message, "\n") |
|
end |
|
os.exit(2) |
|
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(dinfo.source)) |
|
outfile:close() |
|
end |
|
end |
|
---------------------------------------- main procedure |
|
do |
|
local function show_usage() |
|
io.stdout:write(([[ |
|
This is %s v%s <%s> by 'ZR' |
|
Usage: %s[.lua] [OPTION...] EXPECTED IN_PATH |
|
%s[.lua] {-s | -c} [OPTION...] IN_PATH |
|
%s[.lua] -l [OPTION...] EXPECTED IN_PATH OUT_PATH TEX_PATH |
|
Options: |
|
-s/--show Show mode (show dvi driver) |
|
-c/--count Count mode (show page count) |
|
--check Check mode (default) |
|
-l/--latexmk Latexmk filter mode |
|
-w/--workshop LaTeX Workshop mode (implies -l) |
|
-W/--no-workshop Negation of -w |
|
-a/--alarm Show loud alarm in failure |
|
-A/--no-alarm Negation of -a |
|
-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, program, program)) |
|
os.exit(0) |
|
end |
|
|
|
local function read_option() |
|
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 == '-s' or opt == '--show' then |
|
mode = 'show' |
|
elseif opt == '-c' or opt == '--count' then |
|
mode = 'count' |
|
elseif opt == '-l' or opt == '--latexmk' then |
|
mode = 'latexmk' |
|
elseif opt == '--check' then |
|
mode = 'check' |
|
elseif opt == '-w' or opt == '--workshop' then |
|
workshop, mode = true, 'latexmk' |
|
elseif opt == '-W' or opt == '--no-workshop' then |
|
workshop = false |
|
elseif opt == '-a' or opt == '--alarm' then |
|
alarm = true |
|
elseif opt == '-A' or opt == '--no-alarm' then |
|
alarm = false |
|
else abort("invalid option", opt) |
|
end |
|
idx = idx + 1 |
|
end |
|
local ac = (mode == 'show' or mode == 'count') and 1 or |
|
(mode == 'latexmk') and 4 or 2 |
|
sure(#arg == idx + ac - 1, |
|
"wrong number of arguments") |
|
if ac == 1 then |
|
in_path = arg[idx] |
|
else |
|
expected, in_path, out_path, tex_path = |
|
arg[idx], arg[idx + 1], arg[idx + 2], arg[idx + 3] |
|
sure(not expected or expected == 'dvips' or expected == 'dvipdfmx', |
|
"bad expected value", expected) |
|
end |
|
end |
|
|
|
function main() |
|
read_option() |
|
|
|
info("input file path", in_path) |
|
local infile = sure(io.open(in_path, 'rb'), |
|
"cannot open file for read", in_path) |
|
local insrc = assert(infile:read('*a')) |
|
infile:close() |
|
local dinfo = parse_dvi(insrc) |
|
info("input page count", dinfo.pages) |
|
|
|
local actual = actual_dvi_driver(dinfo) |
|
info("actual dvi driver setting", actual) |
|
|
|
if mode == 'check' then |
|
process_check(expected, actual) |
|
elseif mode == 'count' then |
|
io.write(dinfo.pages, "\n") |
|
elseif mode == 'show' then |
|
io.write(actual, "\n") |
|
elseif mode == 'latexmk' then |
|
process_latexmk(dinfo, expected, actual) |
|
end |
|
|
|
info("done") |
|
end |
|
end |
|
---------------------------------------- go to main |
|
sure(string.pack, "Lua version is old") |
|
main() |
|
-- EOF |
記事→ LaTeXのドライバオプションの扱いがカンタンになった話、もっとカンタンにする話