summaryrefslogtreecommitdiff
path: root/support/make4ht/extensions/make4ht-ext-dvisvgm_hashes.lua
blob: 94183c7dbfcbb636018315ca51c76720f4809d80 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
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 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]+)", 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("make -v", "r")
  if not make then return false end
  local content = make:read("*all")
  make:close()
  return true
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 -j" .. cpu_cnt .." -f " .. makefilename
  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.15
  dvisvgm_options = dvisvgm_options % dvisvgm_par
  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