local M = {}
local mkutils = require "mkutils"
local lfs = require "lfs"
local os = require "os"
local kpse = require "kpse"
local filter = require "make4ht-filter"
local domfilter = require "make4ht-domfilter"
local domobject = require "luaxml-domobject"
local xtpipeslib = require "make4ht-xtpipes"
local log = logging.new "odt"
-- object for working with the ODT file
local Odtfile = {}
Odtfile.__index = Odtfile
Odtfile.new = function(archivename)
local self = setmetatable({}, Odtfile)
-- create a temporary file
local tmpname = os.tmpname()
-- remove a temporary file, we are interested only in the unique file name
os.remove(tmpname)
-- get the unique dir name
tmpname = tmpname:match("([a-zA-Z0-9_%-%.]+)$")
local status, msg = lfs.mkdir(tmpname)
if not status then return nil, msg end
-- make picture dir
lfs.mkdir(tmpname .. "/Pictures")
self.archivelocation = tmpname
self.name = archivename
return self
end
function Odtfile:copy(src, dest)
mkutils.cp(src, self.archivelocation .. "/" .. dest)
end
function Odtfile:move(src, dest)
mkutils.mv(src, self.archivelocation .. "/" .. dest)
end
function Odtfile:create_dir(dir)
local currentdir = lfs.currentdir()
lfs.chdir(self.archivelocation)
lfs.mkdir(dir)
lfs.chdir(currentdir)
end
function Odtfile:make_mimetype()
self.mimetypename = "mimetype"
local m, msg = io.open(self.mimetypename, "w")
if not m then
log:error(msg)
return nil, msg
end
m:write("application/vnd.oasis.opendocument.text")
m:close()
end
function Odtfile:remove_mimetype()
os.remove(self.mimetypename)
end
function Odtfile:pack()
local currentdir = lfs.currentdir()
local zip_command = mkutils.find_zip()
lfs.chdir(self.archivelocation)
-- make temporary mime type file
self:make_mimetype()
mkutils.execute(zip_command .. ' -q0X "' .. self.name .. '" ' .. self.mimetypename)
-- remove it, so the next command doesn't overwrite it
self:remove_mimetype()
mkutils.execute(zip_command ..' -r "' .. self.name .. '" *')
lfs.chdir(currentdir)
mkutils.cp(self.archivelocation .. "/" .. self.name, mkutils.file_in_builddir(self.name, Make.params))
mkutils.delete_dir(self.archivelocation)
end
--- *************************
-- *** fix picture sizes ***
-- *************************
--
local function add_points(dimen)
if type(dimen) ~= "string" then return dimen end
-- convert SVG dimensions to points if only number is provided
if dimen:match("[0-9]$") then return dimen .. "pt" end
return dimen
end
local function get_svg_dimensions(filename)
local width, height
if mkutils.file_exists(filename) then
for line in io.lines(filename) do
width = line:match("width%s*=%s*[\"'](.-)[\"']") or width
height = line:match("height%s*=%s*[\"'](.-)[\"']") or height
-- stop parsing once we get both width and height
if width and height then break end
end
end
width = add_points(width)
height = add_points(height)
return width, height
end
local function get_xbb_dimensions(filename)
local f = io.popen("ebb -x -O " .. filename)
if f then
local content = f:read("*all")
local width, height = content:match("%%BoundingBox: %d+ %d+ (%d+) (%d+)")
return add_points(width), add_points(height)
end
return nil
end
--
local function fix_picture_sizes(tmpdir)
local filename = tmpdir .. "/content.xml"
local f = io.open(filename, "r")
if not f then
log:warning("Cannot open ", filename, "for picture size fixes")
return nil
end
local content = f:read("*all") or ""
f:close()
local status, dom= pcall(function()
return domobject.parse(content)
end)
if not status then
log:warning("Cannot parse DOM, the resulting ODT file will be most likely corrupted")
return nil
end
for _, pic in ipairs(dom:query_selector("draw|image")) do
local imagename = pic:get_attribute("xlink:href")
-- update SVG images dimensions
log:debug("image", imagename)
local parent = pic:get_parent()
local width = parent:get_attribute("svg:width")
local height = parent:get_attribute("svg:height")
-- if width == "0.0pt" then width = nil end
-- if height == "0.0pt" then height = nil end
if not width or not height then
local imgfilename = tmpdir .. "/" .. imagename
if imagename:match("svg$") then
width, height = get_svg_dimensions(imgfilename) -- or width, height
elseif imagename:match("png$") or imagename:match("jpe?g$") then
width, height = get_xbb_dimensions(imgfilename)
end
end
log:debug("new dimensions", width, height)
parent:set_attribute("svg:width", width)
parent:set_attribute("svg:height", height)
-- if
end
-- save the modified DOM again
log:debug("Fixed picture sizes")
local domcontent = dom:serialize()
local f, msg = io.open(filename, "w")
if not f then
log:error(msg)
return nil, msg
end
f:write(domcontent)
f:close()
end
-- fix font records in the lg file that don't correct Font_Size record
local lg_fonts_processed=false
local patched_lg_fonts = {}
local function fix_lgfile_fonts(ignored_name, params)
-- this function is called from file match. we must use the name of the .lg file
local filename = mkutils.file_in_builddir(params.input .. ".lg", params)
if not lg_fonts_processed then
local lines = {}
-- default font_size
local font_size = "10"
if mkutils.file_exists(filename) then
--
for line in io.lines(filename) do
-- default font_size can be set in the .lg file
if line:match("Font_Size") then
font_size = line:match("Font_Size:%s*(%d+)")
elseif line:match("Font%(") then
-- match Font record
local name, size, size2, size3 = line:match('Font%("([^"]+)","([%d]*)","([%d]+)","([%d]+)"')
-- find if the first size is not set, and add the default font_size then
if size == "" then
line = string.format('Font("%s","%s","%s","%s")', name, font_size, size2, size3)
-- we must also save the font name and size for later post-processing, because
-- we will need to fix styles in content.xml too
patched_lg_fonts[name .. "-" .. font_size] = true
end
end
lines[#lines+1] = line
end
-- save changed lines to the lg file
local f = io.open(filename, "w")
for _,line in ipairs(lines) do
f:write(line .. "\n")
end
f:close()
end
filter_settings "odtfonts" {patched_lg_fonts = patched_lg_fonts}
end
lg_fonts_processed=true
return true
end
local move_matches = xtpipeslib.move_matches
local function insert_lgfile_fonts(make)
local params = make.params
local first_file = mkutils.file_in_builddir(params.input .. ".4oo", params)
-- find the last file and escape it so it can be used
-- in filename match
make:match(first_file, fix_lgfile_fonts)
move_matches(make)
end
-- escape string to be used in the gsub search
local function escape_file(filename)
local quotepattern = '(['..("%^$().[]*+-?"):gsub("(.)", "%%%1")..'])'
return filename:gsub(quotepattern, "%%%1")
end
-- call xtpipes from Lua
local function call_xtpipes(make)
-- we must find root of the TeX distribution
local selfautoparent = xtpipeslib.get_selfautoparent()
if selfautoparent then
local matchfunction = xtpipeslib.get_xtpipes(selfautoparent)
make:match("4oo", matchfunction)
make:match("4om", matchfunction)
-- move last match to a first place
-- we need to move last two matches, for 4oo and 4om files
move_matches(make)
move_matches(make)
-- fix font records in the lg file
insert_lgfile_fonts(make)
else
log:warning "Cannot locate xtpipes. Try to set TEXMFROOT variable to a root directory of your TeX distribution"
end
end
-- sort output files according to their extensions
local function prepare_output_files(lgfiles)
local groups = {}
for _, name in ipairs(lgfiles) do
local basename, extension = name:match("(.-)%.([^%.]+)$")
local group = groups[extension] or {}
table.insert(group, basename)
groups[extension] = group
log:debug("prepare output file", basename, extension)
end
return groups
end
-- execute function on all files in the group
-- function fn takes current filename and table with various attributes
local function exec_group(groups, name, fn)
for _, basename in ipairs(groups[name] or {}) do
fn{basename = basename, extension=name, filename = basename .. "." .. name}
end
end
-- remove <?xtpipes XML instructions, because they cause issues in some ODT processing
-- applications
local function remove_xtpipes(text)
-- remove <?x
return text:gsub("%<%?xtpipes.-%?%>", "")
end
function M.modify_build(make)
local executed = false
-- execute xtpipes from the build file, instead of t4ht. this fixes issues with wrong paths
-- expanded in tex4ht.env in Miktex or Debian
call_xtpipes(make)
-- fix the image dimensions wrongly set by xtpipes
local domfilters = domfilter({"t4htlinks", "odtpartable"}, "odtfilters")
make:match("4oo$", domfilters)
-- execute it before xtpipes, because we don't want xtpipes to mess with t4htlink elements
move_matches(make)
-- fixes for mathml
local mathmldomfilters = domfilter({"joincharacters","mathmlfixes"}, "mathmlfilters")
make:match("4om$", mathmldomfilters)
-- DOM filters that should be executed after xtpipes
local latedom = domfilter({"odtfonts"}, "lateodtfilters")
make:match("4oo$", latedom)
-- convert XML entities for Unicode characters produced by Xtpipes to characters
local fixentities = filter {"entities-to-unicode", remove_xtpipes}
make:match("4oo", fixentities)
make:match("4om", fixentities)
-- we must handle outdir. make4ht copies the ODT file before it was packed, so
-- we will copy it again after packing later in this format file
local outdir = make.params["outdir"]
-- build the ODT file. This match must be executed as a last one
-- this will be executed as a first match, just to find the last filename
-- in the lgfile
make:match(".*", function()
-- execute it only once
if not executed then
-- this is list of processed files
local lgfiles = make.lgfile.files
for k,v in ipairs(lgfiles) do
if v:match("odt$") then table.remove(lgfiles, k) end
end
-- find the last file and escape it so it can be used
-- in filename match
local lastfile = escape_file(lgfiles[#lgfiles]) .."$"
-- make match for the last file
-- odt packing will be done here
make:match(lastfile, function(filename, par)
local groups = prepare_output_files(make.lgfile.files)
-- we must remove any path from the basename
-- local basename = groups.odt[1]:match("([^/]+)$")
local basename = make.params.input
local odtname = basename .. ".odt"
local odt,msg = Odtfile.new(odtname)
if not odt then
log:error("Cannot create ODT file: " .. msg)
end
-- helper function for simple file moving
local function move_file(group, dest)
exec_group(groups, group, function(par)
odt:move("${filename}" % par, dest)
end)
end
-- the document text
exec_group(groups, "4oo", function(par)
odt:move("${filename}" % par, "content.xml")
odt:create_dir("Pictures")
end)
-- math
exec_group(groups, "4om", function(par)
odt:create_dir(par.basename)
odt:move("${filename}" % par, "${basename}/content.xml" % par)
-- copy the settings file to math subdir
local settings = groups["4os"][1]
odt:copy(settings .. ".4os", "${basename}/settings.xml" % par)
end)
-- these files are created only once, so it doesn't matter that they are
-- copied to one file
move_file("4os", "settings.xml")
move_file("4ot", "meta.xml")
move_file("4oy", "styles.xml")
-- pictures
exec_group(groups, "4og", function(par)
-- add support for images in the TEXMF tree
if not mkutils.file_exists(par.basename) then
par.basename = kpse.find_file(par.basename, "graphic/figure")
if not par.basename then return nil, "Cannot find picture" end
end
-- the Pictues dir is flat, without subdirs
odt:copy("${basename}" % par, "Pictures")
end)
-- fix picture sizes in the content file
fix_picture_sizes(odt.archivelocation)
-- remove some spurious file
exec_group(groups, "4od", function(par)
os.remove(par.filename)
end)
odt:pack()
local build_filename = mkutils.file_in_builddir(odt.name, make.params)
if outdir and outdir ~= "" then
local outfilename = outdir .. "/" .. odt.name
log:info("Copying ODT file to the output dir: " .. outfilename)
mkutils.copy(build_filename,outfilename)
elseif build_filename ~= odt.name then
mkutils.cp(build_filename, odt.name)
end
end)
end
executed = true
end)
return make
end
return M