local dvireader = require "make4ht-dvireader" local mkutils = require "mkutils" local filter = require "make4ht-filter" local log = logging.new "dvisvgm_hashes" local dvisvgm_par = {} local M = {} -- mapping between tex4ht image names and hashed image names local output_map = {} local dvisvgm_options = "-n --exact -c ${scale},${scale}" local parallel_size = 64 local make_command = "make -j ${process_count} -f ${make_file}" local test_make_command = "make -v" -- local parallel_size = 3 local function make_hashed_name(base, hash) return base .. "-" ..hash..".svg" end -- detect the number of available processors local cpu_cnt = 3 -- set a reasonable default for non-Linux systems if os.name == 'linux' then cpu_cnt = 0 local cpuinfo=assert(io.open('/proc/cpuinfo', 'r')) for line in cpuinfo:lines() do if line:match('^processor') then cpu_cnt = cpu_cnt + 1 end end -- set default number of threds if no CPU core have been found if cpu_cnt == 0 then cpu_cnt = 1 end cpuinfo:close() elseif os.name == 'cygwin' or os.type == 'windows' then -- windows has NUMBER_OF_PROCESSORS environmental value local nop = os.getenv('NUMBER_OF_PROCESSORS') if tonumber(nop) then cpu_cnt = nop end end -- process output of dvisvgm and find output page numbers and corresponding files local function get_generated_pages(output, pages) local pages = pages or {} local pos = 1 local pos, finish, page = string.find(output, "processing page (%d+)", pos) while(pos) do pos, finish, file = string.find(output, "output written to ([^\n^\r]+)", finish) pages[tonumber(page)] = file if not finish then break end pos, finish, page = string.find(output, "processing page (%d+)", finish) end return pages end local function make_ranges(pages) local newpages = {} local start, stop for i=1,#pages do local current = pages[i] local next_el = pages[i+1] or current + 100 -- just select a big number local diff = next_el - current if diff == 1 then if not start then start = current end else local element if start then element = start .. "-" .. current else element = current end newpages[#newpages+1] = element start = nil end end return newpages end local function read_log(dvisvgmlog) local f = io.open(dvisvgmlog, "rb") if not f then return nil, "Cannot read dvisvgm log" end local output = f:read("*all") f:close() return output end -- test the existence of GNU Make, which can execute tasks in parallel local function test_make() local make = io.popen(test_make_command, "r") local content = make:read("*all") make:close() -- io.popen always returns valid handle, so we can find that the command doesn't exists only by checking that the -- content is empty return content~=nil and content ~= "" end local function save_file(filename, text) local f = io.open(filename, "w") f:write(text) f:close() end local function make_makefile_command(idvfile, page_sequences) local logs = {} local all = {} -- list of targets in the "all:" makefile target local targets = {} local basename = idvfile:gsub(".idv$", "") local makefilename = basename .. "-images" .. ".mk" -- build make targets for i, ranges in ipairs(page_sequences) do local target = basename .. "-" .. i local logfile = target .. ".dlog" logs[#logs + 1] = logfile all[#all+1] = target local chunk = target .. ":\n\tdvisvgm -v4 " .. dvisvgm_options .. " -p " .. ranges .. " " .. idvfile .. " 2> " .. logfile .. "\n" targets[#targets + 1] = chunk end -- construct makefile and save it local makefile = "all: " .. table.concat(all, " ") .. "\n\n" .. table.concat(targets, "\n") save_file(makefilename, makefile) local command = make_command % {process_count = cpu_cnt, make_file = makefilename} log:debug("Makefile command: " .. command) return command, logs end local function prepare_command(idvfile, pages) local logs = {} if #pages > parallel_size and test_make() then local page_sequences = {} for i=1, #pages, parallel_size do local current_pages = {} for x = i, i+parallel_size -1 do current_pages[#current_pages + 1] = pages[x] end table.insert(page_sequences,table.concat(make_ranges(current_pages), ",")) end return make_makefile_command(idvfile, page_sequences) end -- else local pagesequence = table.concat(make_ranges(pages), ",") -- the stderr from dvisvgm must be redirected and postprocessed local dvisvgmlog = idvfile:gsub("idv$", "dlog") -- local dvisvgm = io.popen("dvisvgm -v4 -n --exact -c 1.15,1.15 -p " .. pagesequence .. " " .. idvfile, "r") local command = "dvisvgm -v4 " .. dvisvgm_options .. " -p " .. pagesequence .. " " .. idvfile .. " 2> " .. dvisvgmlog return command, {dvisvgmlog} -- end end local function execute_dvisvgm(idvfile, pages) if #pages < 1 then return nil, "No pages to convert" end local command, logs = prepare_command(idvfile, pages) log:info(command) os.execute(command) local generated_pages = {} for _, dvisvgmlog in ipairs(logs) do local output = read_log(dvisvgmlog) generated_pages = get_generated_pages(output, generated_pages) end return generated_pages end local function get_dvi_pages(arg) -- list of pages to convert in this run local to_convert = {} local idv_file = arg.input .. ".idv" -- set extension options local extoptions = mkutils.get_filter_settings "dvisvgm_hashes" or {} dvisvgm_options = arg.options or extoptions.options or dvisvgm_options parallel_size = arg.parallel_size or extoptions.parallel_size or parallel_size cpu_cnt = arg.cpu_cnt or extoptions.cpu_cnt or cpu_cnt dvisvgm_par.scale = arg.scale or extoptions.scale or 1.4 dvisvgm_options = dvisvgm_options % dvisvgm_par make_command = arg.make_command or extoptions.make_command or make_command test_make_command = arg.test_make_command or extoptions.test_make_command or test_make_command local f = io.open(idv_file, "rb") if not f then return nil, "Cannot open idv file: " .. idv_file end local content = f:read("*all") f:close() local dvi_pages = dvireader.get_pages(content) -- we must find page numbers and output name sfor the generated images local lg = mkutils.parse_lg(arg.input ..".lg", arg.builddir) for _, name in ipairs(lg.images) do local page = tonumber(name.page) local hash = dvi_pages[page] local tex4ht_name = name.output local output_name = make_hashed_name(arg.input, hash) output_map[tex4ht_name] = output_name if not mkutils.file_exists(output_name) then log:debug("output file: ".. output_name) to_convert[#to_convert+1] = page end end local generated_files, msg = execute_dvisvgm(idv_file, to_convert) if not generated_files then return nil, msg end -- rename the generated files to the hashed filenames for page, file in pairs(generated_files) do os.rename(file, make_hashed_name(arg.input, dvi_pages[page])) end end function M.test(format) -- ODT format doesn't support SVG if format == "odt" then return false end return true end function M.modify_build(make) -- this must be used in the .mk4 file as -- Make:dvisvgm_hashes {} make:add("dvisvgm_hashes", function(arg) get_dvi_pages(arg) end, { }) -- insert dvisvgm_hashes command at the end of the build sequence -- it needs to be called after t4ht make:dvisvgm_hashes {} -- replace original image names with hashed names local executed = false make:match(".*", function(arg) if not executed then executed = true local lgfiles = make.lgfile.files for i, filename in ipairs(lgfiles) do local replace = output_map[filename] if replace then lgfiles[i] = replace end end -- tex4ebook process also the images table, so we need to replace generated filenames here as well local lgimages = make.lgfile.images for _, image in ipairs(lgimages) do local replace = output_map[image.output] if replace then image.output = replace end end end end) -- fix src attributes local process = filter({ function(str, filename) return str:gsub('src=["\'](.-)(["\'])', function(filename, endquote) local newname = output_map[filename] or filename log:debug("newname", newname) return 'src=' .. endquote .. newname .. endquote end) end }, "dvisvgmhashes") make:match("htm.?$", process) -- disable the image processing for _,v in ipairs(make.build_seq) do if v.name == "t4ht" then local t4ht_par = v.params.t4ht_par or make.params.t4ht_par or "" v.params.t4ht_par = t4ht_par .. " -p" end end make:image(".", function() return "" end) return make end return M