summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/epspdf/epspdf.tlu
diff options
context:
space:
mode:
Diffstat (limited to 'Master/texmf-dist/scripts/epspdf/epspdf.tlu')
-rwxr-xr-xMaster/texmf-dist/scripts/epspdf/epspdf.tlu2703
1 files changed, 2703 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.tlu b/Master/texmf-dist/scripts/epspdf/epspdf.tlu
new file mode 100755
index 00000000000..ae793d7f344
--- /dev/null
+++ b/Master/texmf-dist/scripts/epspdf/epspdf.tlu
@@ -0,0 +1,2703 @@
+#!/usr/bin/env texlua
+
+kpse.set_program_name('texlua')
+
+-- epspdf conversion utility
+
+-- First texlua version
+
+ep_version = '0.6.0'
+ep_copyright = '2006, 2008, 2009, 2010, 2011, 2013'
+
+--[[
+
+TeX code for cropping pdfs adapted from Heiko Oberdiek's pdfcrop utility
+
+Program structure
+SETUP
+- some globals
+- utilities
+- system info
+- some infrastructure - logging, temp files
+- initializing (persistent) settings and associated utilities
+- initializing (transient) options
+MAIN FUNCTIONS/METHODS
+- boundingboxes and their methods
+- PsPdf objects:
+ - globals
+ - identify function
+ - one-step conversion methods
+ - any_to_any method
+INITIALIZATION
+- parsing and interpreting rc file
+- parsing and interpreting command-line
+- non-conversion runs
+- start of logging and creation of temp directory
+CONVERSION
+- call any_to_any
+
+TODO
+
+- duplicating epstopdf options
+- use epdf library only optionally
+- custom options for gs and pdftops
+
+Use absolute, normalized names for gs_prog and pdftops_prog but use
+input- and output files as-is.
+
+MAYBE NOT NEEDED
+
+We can probably dispense with [hr]bb:wrapper()
+--]]
+
+-- some general utilities and globals ---------------------------
+
+--[[
+
+I think we get by just fine with simple-minded error handling. At
+most, we just call a function which tries to first write the error
+message to log before re-raising the error.
+
+The gui can capture error messages if necessary.
+
+--]]
+
+eol = nil
+path_sep = nil
+if os.type=='unix' then
+ eol='\n'
+ path_sep = ':'
+else
+ eol='\r\n'
+ path_sep = ';'
+end
+
+-- whether epspdf is run from the epspsdtk gui
+
+from_gui = false
+
+-- error- and debug
+
+function errror(mess)
+ if logfile then pcall(write_log, mess) end
+ -- ignore result of pcall: we can do nothing about failure
+ error(mess, 2)
+end
+
+function dbg(mess)
+ if options.debug then
+ if logfile then write_log(mess) end
+ print(mess)
+ end
+end
+
+--[[
+
+function dbg_opt()
+ if options.debug then
+ local mess = ''
+ for _, k in ipairs({'bbox', 'gray', 'page'}) do
+ mess = mess.. ' ' .. k .. ': ' .. tostring(options.k)
+ end
+ dbg(mess)
+ end
+end
+
+--]]
+
+function ep_shortname(path)
+ if os.type=='unix' then
+ return path
+ else
+ -- shortname appears not to work under miktex
+ -- so return original path as a fallback
+ local sp = lfs.shortname(path)
+ return sp or path
+ end
+end
+
+function fw(path)
+ if os.type=='windows' then
+ return string.gsub(path, '\\', '/')
+ else
+ return path
+ end
+end
+
+cwd = fw(lfs.currentdir())
+source_dir = false -- directory of input file; to be determined
+dest_dir = false -- directory of output file; to be determined
+
+function absolute_path(path, reldir)
+
+ --[[ Return absolute normalized version of path, interpreted
+ from the directory from where the program was called.
+ If reldir, then interpret path from reldir instead.
+
+ We use the fact that lfs.currentdir() always returns an absolute and
+ normalized path. So we go to the parent directory of path, ask for
+ the current directory and then combine the current directory with
+ the base filename.
+
+ The function returns nil if there is no valid parent path.
+ This might be an issue if path is a directory,
+ but we shall apply this function only on files.
+ It is ok if path itself does not exist. --]]
+
+ path = fw(path)
+
+ local present_dir = lfs.currentdir()
+
+ lfs.chdir(cwd)
+
+ if reldir then
+ if not lfs.chdir(reldir) then return nil end
+ end
+
+ local parentdir
+ local filename
+
+ if string.match(path, '/') then
+ parentdir, filename = string.match(path,'^(.*)/([^/]*)$')
+ if parentdir=='' then
+ parentdir = '/'
+ -- on unix, this is an absolute path. on windows, it is not
+ if os.type=='windows' then
+ lfs.chdir('/')
+ parentdir = fw(lfs.currentdir())
+ end
+ elseif os.type=='windows' and string.match(parentdir,'^[a-zA-Z]:$') then
+ parentdir = string.sub(parentdir,1,2)..'/'
+ else
+ if not lfs.chdir(parentdir) then
+ parentdir = nil
+ else
+ parentdir = fw(lfs.currentdir())
+ end
+ end
+ elseif os.type=='windows' and string.match(path,'^[a-zA-Z]:') then
+ -- windows: d:file
+ parentdir = string.sub(path,1,2)
+ if not lfs.chdir(parentdir) then
+ parentdir = nil
+ else
+ parentdir = fw(lfs.currentdir())
+ filename = string.sub(path,3)
+ end
+ else
+ parentdir = fw(lfs.currentdir())
+ filename = path
+ end
+ lfs.chdir(present_dir)
+ if not parentdir then
+ return nil
+ elseif string.sub(parentdir,-1)=='/' then
+ return parentdir..filename, parentdir
+ else
+ return parentdir..'/'..filename, parentdir
+ end
+end -- absolute_path
+
+-- check whether prog is on the searchpath.
+-- we need it only under unix,
+-- so we save ourselves the trouble of accommodating windows.
+-- we return the full path, although we only need a yes or no answer
+
+function find_on_path (prog)
+ if os.type ~= 'unix' then
+ errror('find_on_path: this is a unix-only function')
+ end
+ for d in string.gmatch(os.getenv('PATH'), '[^:]+') do
+ if lfs.isfile(d..'/'..prog) then
+ return absolute_path(d..'/'..prog)
+ end
+ end
+ return false
+end -- find_on_path
+
+-- OTOH, on windows we do not rely so much on the searchpath
+-- so we just test whether the file exists and is an exe file.
+-- only used for pdftops.
+
+function is_prog (path)
+ -- 1. test for and if necessary add extension
+ -- 2. test for existence
+ -- 3. returns either false or absolute path
+ if os.type ~= 'windows' then
+ errror('is_prog: this is a Windows-only function')
+ end
+ if not path then
+ return false
+ end
+ if not string.lower(string.sub(path,-4,-1))=='.exe' then
+ path = path..'.exe'
+ end
+ path = absolute_path(path)
+ if not (path and lfs.isfile(path)) then
+ return false
+ else
+ return path
+ end
+end -- is_prog
+
+-- check whether el occurs in array lst
+function in_list (el, lst)
+ if not lst then return false end
+ for _,p in ipairs(lst) do
+ if el == p then
+ return true
+ end
+ end
+ return false
+end -- in_list
+
+-- remove leading and trailing, but not embedded spaces
+function strip_outer_spaces(s)
+ s = string.gsub(s, '%s*$', '')
+ s = string.gsub(s, '^%s*', '')
+ return s
+end -- strip_outer_spaces
+
+function join(t, sep, lastsep)
+ -- there is a table function concat which does this,
+ -- but without optional different lastsep
+ if t==nil or #t<1 then return '' end -- or should we return nil?
+ local s = t[1]
+ for i=2,#t do -- ok if #t<2
+ if i==#t and lastsep then
+ s = s .. lastsep .. t[i]
+ else
+ s = s .. sep .. t[i]
+ end
+ end
+ return s
+end -- join
+
+-- combine several tables into one.
+-- the parameter is a table of tables.
+function tab_combine (t)
+ local res = {}
+ for _,tt in ipairs(t) do
+ for __, ttt in ipairs(tt) do
+ table.insert(res, ttt)
+ end
+ end
+ return res
+end -- tab_combine
+
+-- Copy a file in chunks, with optional length and offset.
+-- Since files may be very large, we copy them piecemeal.
+-- An initial chunk of size bufsize should be plenty to include
+-- any interesting header information.
+
+bufsize=16000
+
+function slice_file(source, dest, len, offset, mode)
+ -- The final three parameters can be independently left out by
+ -- specifying false as value
+ -- Assume caller ensured parameters of correct type.
+ -- We do not allow negative offsets.
+ local sz = lfs.attributes(source).size
+ if not offset then
+ offset = 0
+ elseif offset>sz then
+ offset = sz
+ end
+ if not len or len>sz-offset then
+ len = sz - offset
+ end
+ if not mode then mode = 'wb' end
+ -- dbg('copying '..len..' bytes of '..source..' to '..dest..' from '..offset
+ -- ..' in '..mode..' mode')
+ local buffer=''
+ local s=io.open(source, 'rb')
+ s:seek('set', offset)
+ local copied = 0
+ local d=io.open(dest, mode)
+ if not d then errror('slice_file: failed to copy to '..dest) end
+ local slen = len
+ while slen>0 do
+ if slen>=bufsize then
+ buffer = s:read(bufsize)
+ slen = slen - bufsize
+ else
+ buffer = s:read(slen)
+ slen = 0
+ end
+ if not d:write(buffer) then
+ errror('slice_file: failed to copy to '..dest)
+ end
+ end
+ s:close()
+ d:close()
+end -- slice_file
+
+-- system info --------------------------------------------
+
+-- safe mode? TODO
+options = {safer = string.match(arg[0], 'repspdf')}
+
+-- Windows: miktex, TL or neither
+-- no support yet for separate ghostscript
+is_miktex = false
+is_tl_w = false
+if os.type == 'windows' then
+ if string.find (string.lower(kpse.version()), 'miktex') then
+ is_miktex = true
+ else
+ local rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '')
+ if not rt then
+ errror('Unrecognized TeX directory structure', 0)
+ elseif lfs.isfile(rt..'/release-texlive.txt') then
+ --[[
+ -- TL version is easy to determine but is not needed
+ local fin = io:open(rt..'release-texlive.txt', 'r')
+ if fin then
+ local l = fin:read('*line')
+ tl_ver = string.match(l, 'version%s+(%d+)$')
+ if tl_ver then tl_ver = tonumber(tl_ver) end
+ end -- if fin
+ --]]
+ is_tl_w = true
+ else
+ errror('Not MikTeX and no file ' .. rt ..
+ '/release-texlive.txt; TeX installation not supported.', 0)
+ end -- if isfile
+ end -- if not miktex
+end -- if windows
+
+-- without Ghostscript we are dead in the water
+gs_prog = false
+do
+ local rt=''
+ if os.type == 'unix' then
+ if find_on_path('gs') then
+ gs_prog = 'gs'
+ else
+ error('No ghostscript on searchpath!', 0)
+ end
+ elseif is_miktex then
+ -- gs_prog = fw(os.selfdir)..'/mgs.exe'
+ gs_prog = 'mgs.exe'
+ rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '')
+ if not lfs.isdir(rt..'/miktex') then
+ -- 64-bits: binaries one level deeper
+ rt = string.gsub(rt, '[\\/][^\\/]+$', '')
+ end
+ if rt=='' then errror('Unexpected MiKTeX directory layout') end
+ if not lfs.isdir(rt..'/miktex') then
+ errror('Unexpected MiKTeX directory layout')
+ end
+ os.setenv('MIKTEX_GS_LIB', rt..'/ghostscript/base;'..rt..'/fonts')
+ elseif is_tl_w then
+ -- windows/TeX Live
+ -- grandparent of texlua.exe directory .. ...
+ rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '')
+ ..'/tlpkg/tlgs'
+ os.setenv('GS_LIB', rt..'/lib;'..rt..'/fonts')
+ os.setenv('Path', rt..'/bin'..';'..os.getenv('Path'))
+ gs_prog = 'gswin32c.exe'
+ else
+ errror('Only TeX Live and MikTeX supported!', 0)
+ end
+end -- do
+
+-- directory for configuration and log
+epsdir = ''
+if os.type == 'windows' then
+ epsdir = fw(ep_shortname(os.getenv('APPDATA'))) .. '/epspdf'
+else
+ epsdir = os.getenv('HOME')..'/.epspdf'
+end
+-- dbg('epsdir: '..epsdir)
+rcfile = epsdir .. '/config'
+
+-- create epsdir if necessary
+if lfs.isfile(epsdir) then
+ error('Cannot continue; epspdf directory ' .. epsdir .. ' is a file')
+elseif not lfs.isdir(epsdir) then
+ if not lfs.mkdir(epsdir) then
+ error('Failed to create epspdf directory ' .. epsdir)
+ end
+end
+
+-- log and log rotation
+
+logfile = epsdir .. '/epspdf.log'
+log_bsl = string.gsub(logfile, '/', '\\')
+oldlog = epsdir .. '/epspdf.log.old'
+
+-- tag log entries with one random integer per epspdf run,
+-- in the absence of a lua process id built-in function
+
+logtag = math.random(0,999999) -- range is inclusive
+logtag = string.format('%06d', logtag)
+
+-- we open and close the logfile anew for each write.
+-- failure to open constitutes no error.
+function write_log(s)
+ local f = io.open(logfile, 'a')
+ if f then
+ f:write(string.format('%s %s%s',
+ os.date('%Y/%m/%d %H:%M:%S', os.time()), s, eol))
+ f:close()
+ end
+ if from_gui then
+ print(s) -- intercepted by the gui
+ end
+end
+
+function log_cmd(cmd)
+ write_log('[' .. table.concat(cmd, '] [') .. ']')
+end
+
+-- temporary files ----------------------------------------
+
+tempdir = false -- will be created later
+tempfiles = {}
+
+-- We just name our temporary files nn.<ext> with successive nn.
+-- We cannot exclude that another process uses our tempdir
+-- so we have to first check for each new file whether it already exists.
+-- Note: epspdf does all the real work from this temp directory.
+
+function mktemp(ext)
+ local froot, fname, f, g
+ for i=0,99 do
+ froot = string.format('%02d.', i)
+ fname = froot..ext
+ -- dbg('New temp file '..fname..'?')
+ if ext~='tex' then
+ if not lfs.isfile(fname) then
+ -- dbg(fname..' available')
+ f = io.open(fname, 'wb')
+ if not f then
+ errror('Cannot create temporary file '..fname)
+ end
+ f:close()
+ table.insert(tempfiles, fname)
+ return froot..ext -- no need to record pdf name
+ end
+ else
+ -- tex; we also need a pdf
+ if not lfs.isfile(fname) and not lfs.isfile(froot..'pdf') then
+ local f = io.open(fname, 'wb')
+ if not f then
+ errror('Cannot create temporary file '..fname)
+ end
+ f:close()
+ table.insert(tempfiles, fname)
+ fname = froot..'pdf'
+ g = io.open(fname, 'wb')
+ if not g then
+ errror('Cannot create temporary file '..fname)
+ end
+ g:close()
+ table.insert(tempfiles, fname)
+ table.insert(tempfiles, froot..'log')
+ return froot..ext -- no need to record pdf name
+ end
+ end -- if
+ end -- for
+ errror('Cannot create temporary file in '..tempdir)
+end
+
+function cleantemp()
+ lfs.chdir(tempdir)
+ for _,f in ipairs(tempfiles) do
+ if lfs.isfile(f) then
+ local success, mess = os.remove(f)
+ if not success then write_log(mess) end
+ end
+ end
+ local empty = true
+ for f in lfs.dir('.') do
+ if f ~= '.' and f ~= '..' then
+ empty = false
+ write_log('Temp dir '..tempdir..' contains '..f..' therefore not removed')
+ break
+ end
+ end
+ lfs.chdir('..')
+ if empty then
+ local res, mess
+ res, mess = lfs.rmdir(tempdir)
+ if not res then
+ write_log('Failed to remove empty '..tempdir..'\n'..mess)
+ end
+ end
+end
+
+--[[
+
+settings
+
+Now:
+1. initial values
+Later:
+2. try to read config file
+3. command-line option parsing, including settings that are not stored
+
+The values in the settings array have lowest priority - lower than
+autodetect and command-line options. We go for false rather than
+undefined, because this results in an actual settings entry.
+We ignore illegal settings in the config file.
+
+--]]
+
+pdf_targets = {'screen', 'ebook', 'printer', 'prepress', 'default'}
+pdf_versions = {'1.2', '1.3', '1.4', 'default'}
+
+settings = {}
+descriptions = {}
+
+settings.pdf_target = 'default'
+descriptions.pdf_target = 'One of ' .. join(pdf_targets, ', ', ' or ')
+
+settings.pdf_version = 'default'
+descriptions.pdf_version = 'One of ' .. join(pdf_versions, ', ', ' or ')
+
+--[[
+-- is bb_spread still a useful setting?
+-- look at gs options wrt boundingbox
+-- settings.bb_spread = 1
+-- descriptions.bb_spread = 'Safety margin in points for (low-res) boundingbox'
+
+settings.use_hires_bb = false
+-- descriptions.use_hires_bb = 'Use high-resolution boundingbox if available'
+-- Ignored; hires bb always used
+--]]
+
+-- because pdftops_prog is sometimes configurable, it is stored in settings.
+-- it will not be used for TeX Live and only be read and written on Windows.
+settings.pdftops_prog = false
+--[[
+if os.type == 'unix' then
+ settings.pdftops_prog = find_on_path('pdftops')
+elseif os.type == 'windows' and not is_miktex then
+ settings.pdftops_prog = os.selfdir..'/pdftops.exe'
+end
+--]]
+descriptions.pdftops_prog = 'Full path to pdftops.exe (not used with TeX Live)'
+
+settings.use_pdftops = true
+descriptions.use_pdftops = 'Use pdftops if available'
+
+-- epspdf stores ps- and pdf viewer settings on behalf of the gui interface
+-- but does not use them itself.
+-- They won't be used at all under osx or windows.
+
+settings.ps_viewer = false
+descriptions.ps_viewer =
+ 'Epspdftk: viewer for PostScript files; not used on Windows or OS X'
+
+settings.pdf_viewer = false
+descriptions.pdf_viewer =
+ 'Epspdftk: viewer for pdf files; not used on Windows or OS X'
+
+-- default_dir, which is used on all platforms, is only for the gui.
+
+if os.type == 'windows' then
+ settings.default_dir =
+ string.gsub(ep_shortname(os.getenv('USERPROFILE')), '\\', '/')
+else
+ settings.default_dir = os.getenv('HOME')
+end
+descriptions.default_dir =
+ 'Epspdftk: initial directory; ignored by epspdf itself'
+
+function write_settings (file)
+ local f
+ if file then
+ f = io.open(rcfile, 'wb')
+ if not f then
+ return
+ end
+ else -- stdout to be captured by epspdftk
+ f = io.output()
+ if os.type=='windows' and not is_tl_w then
+ f:write('tl_w = no', eol)
+ end
+ end
+ for k, v in pairs(settings) do
+ if k ~= 'pdftops_prog' or os.type=='windows' then
+ if descriptions[k] and file then
+ f:write(eol, '# ', descriptions[k], eol)
+ end
+ f:write(k, ' = ', tostring(v), eol)
+ end
+ end
+ if file then
+ f:close()
+ end
+end
+
+function read_settings(file)
+ -- read and interpret rcfile
+ -- we shall ignore illegal entries.
+ local contents
+ local f
+ if file then
+ f = io.open(rcfile, 'rb')
+ if not f then
+ return
+ end
+ else
+ f = io.input()
+ end
+ contents = f:read(10000)
+ if file then
+ f:close()
+ end
+ if not contents or contents=='' then
+ dbg('No settings read')
+ return
+ -- else
+ -- dbg(contents)
+ end
+ -- remove initial \r and \n characters
+ contents = string.gsub(contents, '^[\r\n]*', '');
+ -- gmatch chops contents into series of non-line-ending characters
+ -- possibly followed by line-ending characters.
+ local k, v, vl, vnum
+ for l in string.gmatch(contents, '[^\r\n]+[\r\n]*') do
+ l = string.match(l,'[^\r\n]*')
+ if not string.match(l, '^#') then
+ k, v = string.match(l, '^%s*([^%s]+)%s*=%s*(.*)$')
+ if v then v = string.gsub(v,'%s*$', '') end
+ -- now handle k and v
+ if k == 'pdf_target' then
+ -- ignore unless valid option
+ if in_list(v, pdf_targets) then
+ settings[k] = v
+ end
+ elseif k == 'pdf_version' then
+ -- ignore unless valid option
+ if in_list(v, pdf_versions) then
+ settings[k] = v
+ end
+ --[[
+ elseif k == 'ignore_hires_bb' then
+ vl = string.lower(string.sub(v,1,1))
+ if v == 0 or vl == 'n' or vl == 'f' then
+ settings.use_hires_bb = true
+ elseif v == 1 or vl == 'y' or vl == 't' then
+ settings.use_hires_bb = false
+ end
+ elseif k == 'use_hires_bb' then
+ vl = string.lower(string.sub(v,1,1))
+ if v == 0 or vl == 'n' or vl == 'f' then
+ settings.use_hires_bb = false
+ elseif v == 1 or vl == 'y' or vl == 't' then
+ settings.use_hires_bb = true
+ end
+ elseif k == 'bb_spread' then
+ vnum = tonumber(v)
+ if vnum and vnum >= 0 then
+ settings[k] = math.modf(v) -- truncate to integer
+ end
+ --]]
+ elseif k == 'pdftops_prog' then
+ if is_miktex then
+ settings.pdftops_prog = is_prog(v)
+ -- elseif os.type=='windows' then
+ -- settings.pdftops_prog = v
+ end -- else ignore
+ elseif k == 'ignore_pdftops' then
+ vl = string.lower(string.sub(v,1,1))
+ if v == 0 or vl == 'n' or vl == 'f' then
+ settings.use_pdftops = true
+ elseif v == 1 or vl == 'y' or vl == 't' then
+ settings.use_pdftops = false
+ end
+ elseif k == 'use_pdftops' then
+ vl = string.lower(string.sub(v,1,1))
+ if v == '0' or vl == 'n' or vl == 'f' then
+ settings.use_pdftops = false
+ elseif v == '1' or vl == 'y' or vl == 't' then
+ settings.use_pdftops = true
+ end
+ -- final three settings not used by epspdf itself but
+ -- passed along to epspdftk
+ elseif k == 'ps_viewer' then
+ settings.ps_viewer = v
+ elseif k == 'pdf_viewer' then
+ settings.pdf_viewer = v
+ elseif k == 'default_dir' then
+ settings.default_dir = v
+ elseif k == 'default_dir' then
+ settings.default_dir = v
+ end -- test for k
+ end -- not matching ^#
+ end -- for
+end -- read settings
+
+-- command-line parameters: variables and functions -------------
+
+function help (mess)
+ -- need to enforce an ordering, otherwise we could have used pairs(opts)
+ if mess then print(mess..eol) end
+ show_version()
+ print([[
+
+Convert between [e]ps and pdf formats
+Usage: epspdf[.tlu] [options] infile [outfile]
+Default for outfile is file.pdf if infile is file.eps or file.ps
+Default for outfile is file.eps if infile is file.pdf
+]])
+ -- omitted below: no-op options
+ for _, k in ipairs({'page', 'gray', 'bbox', 'pdf_target', 'pdf_version',
+ 'pdftops_prog', 'use_pdftops', 'save', 'debug', 'version', 'help' }) do
+ help_opt(k)
+ end
+ if mess then os.exit(1) else os.exit() end
+end
+
+function help_opt (o)
+ -- one line where possible
+ local indent_n = 12
+ local intent_sp = string.rep(' ', indent_n)
+ local indent_fmt = '%-' .. tostring(indent_n) .. 's'
+ v = opts[o]
+ if v=='pdftops_prog' and (os.type=='unix' or is_tl_w) then
+ return
+ end
+ if v and v.help then
+ local synt = join(v.forms, ', ')
+ if v.type ~= 'boolean' then synt = synt .. ' ' .. v.placeholder end
+ if string.len(synt)<indent_n then
+ print(string.format(indent_fmt, synt) .. v.help)
+ else
+ print(synt)
+ print(intent_sp .. v.help)
+ end
+ if v.negforms then
+ local neghelp = 'Reverses the above'
+ synt = join(v.negforms, ', ')
+ if string.len(synt)<indent_n then
+ print(string.format(indent_fmt, synt) .. neghelp)
+ else
+ print(synt)
+ print(intent_sp .. neghelp)
+ end
+ end
+ end
+end
+
+function show_version ()
+ print('Epspdf version '..ep_version..'\nCopyright (c) '
+ ..ep_copyright..' Siep Kroonenberg')
+end
+
+-- gui: reading and writing settings -----------
+
+function gui(action)
+ -- use stdin for reading settings from gui, and stdout for writing
+ if action=='config_w' then
+ -- called at start of epspdftk
+ write_settings() -- to pipe epspdf => epspdftk
+ os.exit()
+ elseif action=='config_r' then
+ read_settings() -- from 'pipe' epspdftk => epspdf
+ write_settings(rcfile)
+ os.exit()
+ else
+ from_gui = true
+ end
+end
+
+-- besides settings, which can be saved, we also use options which are not.
+-- we already have an options table with sole entry 'safer'
+-- the pdf output settings are converted to options array elements
+
+options.page = false
+options.gray = false
+options.bbox = false
+options.info = false
+options.debug = false
+options.type = false -- implied via output filename on command line
+
+-- boundingboxes ---------------------------------------------------
+
+-- Bb.coords names now same as those of epdf PDFRectangle
+
+Bb = {}
+Bb.coords = {'x1', 'y1', 'x2', 'y2'}
+
+function Bb:from_rect(r)
+ for _,k in ipairs(self.coords) do
+ if not r[k] or type(r[k])~='number' then
+ errror('from_rect called with illegal parameters')
+ end
+ -- sanity check on size
+ -- FIXME: this limit is far too high
+ if r[k]+.5==r[k] or r[k]-.5==r[k] then
+ errror('Bb:from_rect: ' .. r[k] ..' greater than maxint')
+ end
+ local b = {}
+ local eps = 0.000001
+ b.x1, b.x2 = math.floor(math.min(r.x1, r.x2) + eps),
+ math.ceil(math.max(r.x1, r.x2) - eps)
+ b.y1, b.y2 = math.floor(math.min(r.y1, r.y2) + eps),
+ math.ceil(math.max(r.y1, r.y2) - eps)
+ if b.x1==b.x2 or b.y1==b.y2 then
+ errror('from_rect: width or height is zero')
+ end
+ setmetatable(b, {__index=self})
+ return b
+ end
+end
+
+Bb.bb_pat = '^%s*%%%%BoundingBox:'
+Bb.bb_end = '^%s*%%%%BoundingBox:%s*%(%s*atend%s*%)'
+
+function Bb:from_comment(s)
+ local p = self.bb_pat..'%s*([-+%d]+)'..string.rep('%s+([-+%d]+)',3)
+ local b = {}
+ b.x1, b.y1, b.x2, b.y2 = string.match(s, p)
+ if not b.y2 then
+ errror('Bb.from_comment: illegal boundingbox string ' .. s)
+ end
+ for _,k in ipairs(self.coords) do
+ b[k] = tonumber(b[k])
+ end
+ return Bb:from_rect(b)
+end
+
+--[[
+
+function Bb:copy ()
+ local b = {}
+ for _,k in ipairs(self.coords) do b[k] = self[k] end
+ setmetatable(b, {__index=self})
+end
+
+function Bb:width()
+ return self.x2 - self.x1
+end
+
+function Bb:height()
+ return self.y2 - self.y1
+end
+
+function Bb:expand ()
+ -- in-place expansion; does not return an object.
+ -- any point in preserving non-negativity?
+ local i = settings.bb_spread
+ if i and i>0 then
+ -- if x1~=0 then x1 = x1-1 end
+ -- if y1~=0 then y1 = y1-1 end
+ self.x1 = self.x1 - 1
+ self.y1 = self.y1 - 1
+ self.x2 = self.x2 + 1
+ self.y2 = self.y2 + 1
+ end
+end
+
+-- no longer used: gs handles this
+-- call this via pcall
+function Bb:wrapper()
+ local fn = mktemp('ps')
+ local f = io.open(fn, 'wb')
+ f:write(string.format('%%%%BoundingBox: 0 0 %d %d\n',
+ self:width(), self:height())
+ .. string.format('<< /PageSize [%d %d] >> setpagedevice\n',
+ self:width(), self:height())
+ .. 'gsave\n'
+ .. string.format('%d %d translate\n', -self.x1, -self.y1))
+ f:close()
+ return fn
+end
+
+--]]
+
+function Bb:nonnegative ()
+ return self.x1>=0 and self.y1>=0
+end
+
+function Bb:comment()
+ -- if options.debug then print(debug.traceback()) end
+ return string.format('%%%%BoundingBox: %d %d %d %d',
+ self.x1, self.y1, self.x2, self.y2)
+end
+
+-- hires boundingboxes ---------------------------------------------
+
+HRBb = {}
+
+setmetatable(HRBb, {__index=Bb})
+
+function HRBb:from_rect(r)
+ for _,k in ipairs(self.coords) do
+ if not r[k] or type(r[k])~='number' then
+ errror('from_rect called with illegal parameters')
+ end
+ -- sanity check on size
+ if r[k]+.5==r[k] or r[k]-.5==r[k] then
+ errror('HRBb:from_rect: ' .. b[k] ..' greater than maxint')
+ end
+ local b = {}
+ b.x1, b.x2 = math.min(r.x1, r.x2), math.max(r.x1, r.x2)
+ b.y1, b.y2 = math.min(r.y1, r.y2), math.max(r.y1, r.y2)
+ if b.x1==b.x2 or b.y1==b.y2 then
+ errror('from_rect: width or height is zero')
+ end
+ setmetatable(b, {__index=self})
+ return b
+ end
+end
+
+HRBb.bb_pat = '^%s*%%%%HiResBoundingBox:'
+HRBb.bb_end = '^%s*%%%%HiResBoundingBox:%s*%(%s*atend%s*%)%s*$'
+
+function HRBb:from_comment(s)
+ -- dbg('hrbb from '..s)
+ local p = self.bb_pat..'%s*([-+.%deE]+)'..string.rep('%s+([-+.%deE]+)',3)
+ local b = {}
+ b.x1, b.y1, b.x2, b.y2 = string.match(s, p)
+ if not b.y2 then
+ errror('HRBb.from_comment: illegal boundingbox string ' .. s)
+ end
+ for _,k in ipairs(self.coords) do
+ b[k] = tonumber(b[k])
+ end
+ return HRBb:from_rect(b)
+end
+
+function HRBb:comment()
+ return string.format('%%%%HiResBoundingBox: %f %f %f %f',
+ self.x1, self.y1, self.x2, self.y2)
+end
+
+--[[
+
+function HRBb:expand ()
+ errror('HRBb:expand not available')
+end
+
+-- no longer used: gs handles this
+-- call this one also via pcall
+function HRBb:wrapper()
+
+ -- local fn = mktemp('ps')
+ -- local f = io.open(fn, 'wb')
+ -- f.write(string.format('<< /PageSize [%f %f] >> setpagedevice\n',
+ -- self.x2 - self.x1, self.y2 - self.y1))
+ -- f.write(string.format('gsave\n%f %f translate\n', -self.x1, -self.y1))
+ -- f:close()
+ -- return fn
+
+ return string.format(
+ '<< /PageSize [%f %f] >> setpagedevice gsave %f %f translate',
+ self.x2 - self.x1, self.y2 - self.y1, -self.x1, -self.y1)
+end
+
+--]]
+
+-- manipulating eps/ps/pdf files -----------------------------------
+
+-- command-line fragments for conversions
+-- We could make these `class attributes' for PsPdf but to what purpose?
+-- For Windows shell commands, we need to substitute `#' for `='
+-- when invoking Ghostscript. For simplicity, we do this across the board.
+
+gs_options = {gs_prog, '-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER'}
+
+-- windows: use env vars rather than additional options
+-- may add custom options later
+
+
+
+pdf_options = {'-sDEVICE#pdfwrite'} -- '-dUseCIEColor' causes serious slowdown
+-- for final conversion to pdf;
+-- will be completed after reading settings and options
+gray_options = {'-dProcessColorModel#/DeviceGray',
+ '-sColorConversionStrategy#Gray'}
+-- below, '-f' guarantees that next string is interpreted as input file
+pdf_tailoptions = false -- to be set after option parsing
+
+pdftops = false
+-- gets a value if we are going to use pdftops
+
+ps_options = {'-level3'}
+-- may add custom options later
+
+
+function identify(path)
+ local f = io.open(path, 'rb')
+ if not f then
+ errror('Failure to open '..path..' for identification')
+ end
+ local filestart= f:read(23)
+ f:close()
+ if not filestart or filestart=='' then
+ return false
+ elseif string.match(filestart,'^\197\208\211\198') then -- c5 d0 d3 c6
+ return 'epsPreview'
+ elseif string.match(filestart,'^%%!PS%-Adobe%-%d%.%d EPSF%-%d%.%d') then
+ return 'eps'
+ elseif string.match(filestart,'^%%!PS%-Adobe%-%d%.%d') then
+ for _, p in ipairs({'.eps', '.epi', '.epsi', '.epsf'}) do
+ if string.sub(string.lower(path), -1-string.len(p),-1) == p then
+ return 'eps'
+ else
+ return 'ps'
+ end
+ end
+ return 'ps'
+ elseif string.match(filestart, '^%%PDF') then
+ return 'pdf'
+ else
+ return false
+ end
+end -- identify
+
+function pdf_props(path)
+ local pdfdoc = epdf.open(path)
+ if not pdfdoc then
+ errror('epdf.open failed on '..path)
+ end
+ local cat = pdfdoc:getCatalog()
+ if not cat then
+ errror('Cannot open pdf catalog of '..path)
+ end
+ local pg = cat:getNumPages()
+ if not pg then
+ errror('Cannot read n. of pages of '..path)
+ end
+ local maver = pdfdoc:getPDFMajorVersion()
+ if not maver then
+ errror('Cannot read pdf major version of '..path)
+ end
+ local miver = pdfdoc:getPDFMinorVersion()
+ if not miver then
+ errror('Cannot read pdf minor version of '..path)
+ end
+ if maver > 1 then
+ print(path..' has pdf major version \n'..tostring(maver)..
+ ' which is unsupported;\n'..
+ 'Continuing with fingers crossed...')
+ end
+ return pg, miver, maver
+end
+
+function info (infile)
+ local intype = identify(infile)
+ if not intype then
+ print(infile..' has an unsupported filetype.')
+ elseif intype~='pdf' then
+ print(infile..' has type '..intype..'.')
+ else
+ local pg, miver, maver = pdf_props(infile)
+ print(infile..' has type pdf, version '..tostring(maver)..
+ '.'..tostring(miver)..' and has '..tostring(pg)..' pages.')
+ end
+ os.exit()
+end
+
+-- PsPdf object -------------------------------------------------
+
+PsPdf = {}
+
+-- creators
+
+function PsPdf:new(ext)
+ -- dbg('PsPdf:new')
+ local psp = {}
+ setmetatable(psp, {__index = self})
+ -- assign temp file
+ psp.path = mktemp(string.lower(ext))
+ if string.lower(ext)=='pdf' then
+ psp.type = 'pdf'
+ elseif string.lower(ext)=='eps' then
+ psp.type = 'eps'
+ elseif string.lower(ext)=='ps' then
+ psp.type = 'ps'
+ else
+ psp.type = false
+ end
+ if psp.type=='eps' then
+ psp.pages = 1
+ end
+ psp.bb = false
+ psp.hrbb = false
+ return psp
+end -- PsPdf:new
+
+function PsPdf:from_path(path)
+ -- dbg('PsPdf:from_path')
+ local psp = {}
+ setmetatable(psp, {__index = self})
+ psp.path = path
+ if lfs.isfile(path) then
+ -- turn existing file into PsPdf object.
+ psp.type = identify(psp.path)
+ if psp.type=='pdf' then
+ psp.pages, psp.miver, psp.maver = pdf_props(psp.path)
+ end
+ else
+ errror('PsPdf:from_path called with non-existant file '..path)
+ end
+ if psp.type=='eps' then
+ psp.pages = 1
+ end
+ psp.bb = false
+ psp.hrbb = false
+ -- calculate when needed
+ return psp
+end -- PsPdf:from_path
+
+--[===[ getting boundingbox property from file itself --------------
+
+find_bb_simple: use only for eps PsPdf objects we generated
+ourselves, so we can assume that the bbox comments are in the header
+and the hires bb lies within the lores bb.
+Of course the file itself is not rewritten.
+
+--]===]
+
+function PsPdf:find_bb_simple()
+ -- dbg('PsPdf:find_bb_simple')
+ if self.type~='eps' then
+ errror('find_bb_simple called with non-eps file '..self.path)
+ end
+ self.bb = false
+ self.hrbb = false
+ local slurp = false
+ local f = io.open(self.path, 'rb')
+ if f then
+ slurp = f:read(bufsize)
+ f:close()
+ end
+ lines = {}
+ for l in string.gmatch(slurp, '[^\n\r]+') do
+ if string.match(l, Bb.bb_pat) then
+ self.bb = Bb:from_comment(l)
+ elseif string.match(l, HRBb.bb_pat) then
+ self.hrbb = HRBb:from_comment(l)
+ elseif self.bb then
+ break -- stop looking; we expect hrbb next to bb
+ end
+ if self.bb and self.hrbb then break end
+ end
+ if not self.bb then
+ errror('No valid boundingbox for generated file' .. self.path)
+ end
+ return self -- no real need for a return value
+end
+
+function PsPdf:bb_from_gs(pg)
+
+ -- dbg('bb_from_gs '..pg)
+
+ if self.type=='ps' then
+ errror('bb_from_gs called with ps file '..self.path)
+ -- not needed for generic PostScript,
+ -- page selection only works with pdf files, so we save ourselves
+ -- the trouble of picking the right bbox from a list
+ end
+ if self.type=='eps' and not self.bb:nonnegative() then
+ errror('bb_from_gs called on ' .. self.path ..
+ ' which has some negative boundingbox coordinates')
+ end
+ -- A pdf can also have negative ...Box coordinates, but apparently
+ -- for pdf the bbox returned by gs is relative to the lower-left corner.
+ -- Anyhow, with pdf it all works out even with negative coordinates.
+
+ -- Since Ghostscript writes the boundingbox comments to stderr,
+ -- we need a shell to intercept this output:
+
+ local bb_file = mktemp('dsc')
+ local cmdline = table.concat(gs_options,' ')
+ if self.type=='pdf' then
+ if not pg then pg=1 end
+ cmdline = cmdline .. ' -dFirstPage#' .. tostring(pg) ..
+ ' -dLastPage#' .. tostring(pg)
+ end
+ cmdline = cmdline .. ' -sDEVICE#bbox ' .. self.path .. ' 2>'..bb_file
+
+ -- execute shell command
+
+ local r, cmd
+ if os.type=='windows' then
+ -- redirection does not work right for os.execute on TL/w32 <= 2011
+ -- but it does when calling the cmd shell explicitly
+ cmd = {'cmd', '/c', cmdline}
+ log_cmd(cmd)
+ r = os.spawn(cmd)
+ else
+ write_log('os.execute: '..cmdline)
+ r = os.execute(cmdline)
+ end
+ if not r then
+ errror('Cannot get fixed boundingbox for '..self.path)
+ end
+
+ -- read new bbox from ghostscript output
+ -- can we really count on the plain bb coming first?
+ -- OTOH, I would rather not introduce unnecessary complexity
+ -- still, it may be better to match each line with [HR]Bb_pat
+
+ local bb = false
+ local hrbb = false
+ local fin = io.open(bb_file, 'r')
+ if fin then
+ for i=1,10 do -- actually, 2 should suffice
+ local l = fin:read("*line")
+ if not l then break end
+ if string.match(l, Bb.bb_pat) then
+ bb = Bb:from_comment(l)
+ end
+ if string.match(l, HRBb.bb_pat) then
+ hrbb = HRBb:from_comment(l)
+ end
+ end
+ fin:close()
+ end
+ if not bb or not hrbb then
+ errror('Cannot get fixed boundingbox for '..self.path)
+ end
+ return bb, hrbb
+end
+
+-- eps_clean: remove some problem features from eps (new file & object)
+
+function PsPdf:eps_clean()
+
+ -- return a PsPdf object referring to a new file
+ -- without a preview header and with boundingbox(es) in the header
+
+ local function bytes2num (s, i)
+ -- convert substring s[i..i+3] to a number.
+ -- by working byte for byte we avoid endian issues
+ local n = string.byte(s, i+3)
+ for j=2,0,-1 do n = 256*n + string.byte(s, i+j) end
+ return n
+ -- somehow the explicit expression below didn't work
+ -- return ((256 * (256 * (256 * string.byte(s,i+3)) + string.byte(s,i+2))
+ -- + string.byte(s,i+1)) + string.byte(s,i))
+ end
+
+ dbg('PsPdf:eps_clean '..self.path)
+ if self.type~='eps' and self.type~='epsPreview' then
+ errror('epsclean called with non-eps file ' .. self.path)
+ end
+ local offset, ps_length = false, false
+ local fin, fout
+ if self.type=='eps' then
+ offset = 0
+ ps_length = lfs.attributes(self.path, 'size')
+ else
+ -- read TOC; see Adobe EPS specification
+ -- interpret byte for byte, in case the platform is not little-endian
+ fin = io.open(self.path, 'rb')
+ if fin then
+ local toc = fin:read(12)
+ fin:close()
+ if toc and string.len(toc)==12 then
+ offset = bytes2num(toc, 5)
+ ps_length = bytes2num(toc, 9)
+ end
+ end
+ if not offset then
+ errror('Could not read preview header of ' .. self.path)
+ end
+ -- dbg(tostring(offset)..' '..tostring(ps_length))
+ end
+
+ -- create the PsPdf object which is to be returned
+
+ local psp
+ psp = PsPdf:new('eps')
+ -- dbg(psp.path)
+
+ -- read an initial and if necessary a final chunk of the file
+ -- to find boundingbox comments.
+
+ local atend = false
+ local hr_atend = false
+ local slurp -- the read buffer
+ local l -- contains current scanned line; split off from slurp
+ -- pre_lines: scanned header lines; alternately lines and eols
+ local pre_lines = {}
+ -- new_offset: offset plus combined length of scanned header lines
+ local new_offset = offset
+ -- post_lines: scanned trailer lines
+ local post_lines = {}
+ -- middle_length: ps_length minus scanned header- and and maybe trailer parts
+ -- this is the length of file that will be copied wholesale.
+ local middle_length
+ local i, i_bb, i_hrbb
+ local j, j_bb, j_hrbb, j_end
+ -- j_end: index of final scanned trailer line
+ -- no i_end necessary: for header lines we can use #pre_lines.
+
+ fin = io.open(self.path, 'rb')
+ if not fin then errror('Cannot read '..self.path) end
+ fin:seek('set', offset)
+
+ -- remaining, unscanned length of input buffer slurp
+ local unscanned = math.min(ps_length,bufsize)
+ -- dbg('bytes to be read: '..tostring(unscanned))
+ slurp = fin:read(unscanned)
+ -- dbg('Read from '..self.path..': '..string.len(slurp)..' bytes')
+
+ -- unnecessary:
+ psp.bb = nil
+ psp.hrbb = nil
+
+ i, i_bb, i_hrbb = 0, false, false
+ while unscanned>0 do
+ i = i+1
+ if string.find(slurp,'[\n\r]')==1 then
+ l,slurp = string.match(slurp, '^([\n\r]+)(.*)$')
+ else
+ l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$')
+ if string.match(l, Bb.bb_end) then
+ atend = true
+ i_bb = i
+ elseif string.match(l, Bb.bb_pat) then
+ -- dbg(l)
+ psp.bb = Bb:from_comment(l)
+ -- dbg(psp.bb:comment())
+ -- from_comment errors out on failure; no need to check return value
+ i_bb = i
+ elseif string.match(l, HRBb.bb_end) then
+ hr_atend = true
+ i_hrbb = i
+ elseif string.match(l, HRBb.bb_pat) then
+ -- dbg(l)
+ psp.hrbb = HRBb:from_comment(l)
+ -- dbg(psp.hrbb:comment())
+ i_hrbb = i
+ end -- bbox line
+ end -- eol/non-eol
+ pre_lines[i] = l
+ unscanned = unscanned - string.len(l)
+ if (i_bb and (i_hrbb or (i_bb<(i-1)))) or unscanned<=0 then
+ -- condition i_bb<i-1:
+ -- We do not want to find the hrbb of an included eps.
+ -- Therefore we stop looking for hrbb if it is not next to bb
+ -- (with an intervening eol)
+ -- Note that the header comments are not necessarily terminated
+ -- with a %%EndComments line; see Adobe DSC spec 5001
+ break
+ end -- deciding whether to stop
+ end -- while
+ new_offset = offset + string.len(table.concat(pre_lines))
+ middle_length = ps_length - string.len(table.concat(pre_lines))
+
+ if atend or hr_atend then
+ -- find boundingbox comments, starting from end of postscript
+ if ps_length>bufsize then
+ fin:seek('set',offset+ps_length-bufsize)
+ unscanned = bufsize
+ slurp = fin:read(unscanned)
+ else
+ -- use what is left from old slurp
+ unscanned = string.len(slurp)
+ end
+ j = 1 -- count down from 0
+ j_bb, j_hrbb, j_end = false, false, false
+ while unscanned>0 do
+ j = j - 1
+ -- dbg(j)
+ if string.find(slurp,'[\n\r]', string.len(slurp)) then
+ -- dbg('eol(s)')
+ slurp,l = string.match(slurp, '^(.-)([\n\r]+)$')
+ -- '-': non-greedy matching
+ else
+ slurp,l = string.match(slurp, '^(.-)([^\n\r]+)$')
+ -- dbg(l)
+ if string.match(l, Bb.bb_pat) then
+ psp.bb = Bb:from_comment(l)
+ j_bb = j
+ elseif string.match(l, HRBb.bb_pat) then
+ psp.hrbb = HRBb:from_comment(l)
+ j_hrbb = j
+ end -- bbox line
+ end -- eol/non-eol
+ post_lines[j] = l
+ unscanned = unscanned - string.len(l)
+ if (psp.bb and
+ (psp.hrbb or not hr_atend or j_bb>(j+1))) or unscanned<=0 then
+ -- stop looking
+ j_end = j
+ break
+ end -- deciding whether to stop
+ end -- while
+ middle_length = middle_length -
+ string.len(table.concat(post_lines, '', j_end, 0))
+ end --if atend
+ fin:close()
+ -- fix boundingbox lines
+ if atend and j_bb then
+ -- pre_lines[i_bb] = post_lines[j_bb]
+ pre_lines[i_bb] = psp.bb:comment() -- WHY DOESNT THIS WORK ????
+ post_lines[j_bb] = ''
+ post_lines[j_bb+1] = ''
+ end
+ if hr_atend and j_hrbb then
+ -- dbg(psp.hrbb:comment())
+ -- pre_lines[i_hrbb] = post_lines[j_hrbb]
+ pre_lines[i_hrbb] = psp.hrbb:comment()
+ post_lines[j_hrbb] = ''
+ post_lines[j_hrbb+1] = ''
+ end
+ -- create cleaned eps file
+ fout = io.open(psp.path, 'wb')
+ if not fout then errror('Cannot create new file '..psp.path) end
+ fout:write(table.concat(pre_lines))
+ fout:close()
+ slice_file(self.path, psp.path, middle_length, new_offset, 'ab')
+ fout = io.open(psp.path, 'ab')
+ fout:write(table.concat(post_lines, '', j_end, 0))
+ fout:close()
+ return psp
+end -- eps_clean
+
+-- tight boundingbox (new file & object)
+
+function PsPdf:eps_crop()
+
+ -- not a proper conversion, although
+ -- we use the Ghostscript bbox device for a tight boundingbox.
+ -- We use both the regular and the hires boundingbox from gs.
+ -- The eps should already have been cleaned up by eps_clean,
+ -- and the current boundingbox should not contain negative coordinates,
+ -- otherwise the bbox output device may give incorrect results.
+ -- Only the boundingbox in the eps is rewritten.
+
+ dbg('PsPdf:eps_crop '..self.path)
+ if self.type~='eps' then
+ errror('eps_crop called with non-eps file ' .. self.path)
+ end
+
+ -- create the PsPdf object which is to be returned
+
+ local psp = PsPdf:new('eps')
+
+ -- read new bbox from ghostscript output
+
+ psp.bb, psp.hrbb = self:bb_from_gs()
+
+ -- rewrite header with new boundingboxes
+
+ local slurp -- the read buffer
+ local l -- contains current scanned line; split off from slurp
+ -- pre_lines: scanned header lines; alternately lines and eols
+ local pre_lines = {}
+ -- offset: combined length of scanned header lines
+ local offset = 0
+ local ps_length = lfs.attributes(self.path, 'size')
+ local i, i_bb, i_hrbb
+
+ fin = io.open(self.path, 'rb')
+ if not fin then errror('Cannot read '..self.path) end
+
+ -- remaining, unscanned length of input buffer slurp
+ local unscanned = math.min(ps_length,bufsize)
+ -- dbg('bytes to be read: '..tostring(unscanned))
+ slurp = fin:read(unscanned)
+ -- dbg('Read from '..self.path..': '..string.len(slurp)..' bytes')
+ i, i_bb, i_hrbb = 0, false, false
+ while unscanned>0 do
+ i = i+1
+ if string.find(slurp,'[\n\r]')==1 then
+ l,slurp = string.match(slurp, '^([\n\r]+)(.*)$')
+ else
+ l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$')
+ if string.match(l, Bb.bb_pat) then
+ i_bb = i
+ elseif string.match(l, HRBb.bb_pat) then
+ i_hrbb = i
+ end -- bbox line
+ end -- eol/non-eol
+ pre_lines[i] = l
+ unscanned = unscanned - string.len(l)
+ if (i_bb and (i_hrbb or (i_bb<(i-1)))) or unscanned<=0 then
+ break
+ end
+ end -- while
+ fin:close()
+ offset = string.len(table.concat(pre_lines))
+
+ if i_hrbb then
+ pre_lines[i_bb] = psp.bb:comment()
+ pre_lines[i_hrbb] = psp.hrbb:comment()
+ else
+ -- jam both bbox comments into one slot, with an intervening eol.
+ -- for the sake of conformity, we copy an existing eol.
+ pre_lines[i_bb] = psp.bb:comment() .. pre_lines[i_bb-1] ..
+ psp.hrbb:comment()
+ end
+
+ -- write a new eps file
+
+ fout = io.open(psp.path, 'wb')
+ if not fout then errror('Cannot write new file '.. psp.path) end
+ fout:write(table.concat(pre_lines))
+ fout:close()
+ slice_file(self.path, psp.path,
+ lfs.attributes(self.path,'size') - offset, offset, 'ab')
+ options.bbox = false
+ -- dbg('eps_crop from '..self.path..' to '..psp.path)
+ return psp
+end -- eps_crop
+
+--[===[ real conversions involving a single call of gs or pdftops --------
+
+Each conversion fullfills all options that it can: gray, bbox and
+page. gray when converting to pdf, bbox when converting from eps or
+from pdf to pdf and page when converting from pdf. It then sets the
+fullfilled option(s) to false.
+
+We like to preserve fonts as fonts. gs does this when generating
+pdf, but may fail for fonts such as cid and large truetype when
+generating PostScript. In such cases, pdftops may succeed. However,
+it seems that if the page contains an element that does not cleanly
+convert, pdftops simply rasterizes the entire page, and that this
+choice is made per page.
+
+TODO: pdf => pdf with bbox via pdftex, as in pdfcrop utility
+
+--]===]
+
+-- TODO: multiple pages
+-- (means additional parameter checking)
+
+-- Converting from pdf to pdf using luatex; no grayscaling
+
+function PsPdf:pdf_crop()
+
+ -- options to be fulfilled: page, boundingbox
+ -- only called directly.
+ -- embeds the pdf with crop parameters into a new (lua)tex document
+ if not (options.bbox or options.page) then
+ return self
+ end
+ if options.page and options.page > self.pages then
+ errror('PsPdf:pdf_crop called with non-existent page '.. options.page)
+ end
+ local pg = options.page or 1
+ local bb, hrbb
+ if options.bbox then
+ bb, hrbb = self:bb_from_gs(pg)
+ else
+ -- use [Trim|Crop|Media]Box instead
+ local dummy = epdf.open(self.path)
+ if not dummy then
+ errror('Epdf: cannot open '..self.path)
+ end
+ dummy = dummy:getCatalog()
+ if not dummy then
+ errror('Cannot open catalog of '..self.path)
+ end
+ dummy = dummy:getPage(pg)
+ if not dummy then
+ errror('Epdf: cannot open page object '..tostring(pg)..' of '..self.path)
+ end
+ hrbb = dummy:getTrimBox()
+ if not hrbb then
+ hrbb = dummy:getCropBox()
+ end
+ if not hrbb then
+ hrbb = dummy:getMediaBox()
+ end
+ -- further checks, including for non-nil, by Bb:from_rect,
+ -- which errors out on failures
+ hrbb = HRBb:from_rect(hrbb)
+ end
+
+ -- location of luatex
+ local luatex_prog = fw(os.selfdir) .. '/luatex' -- absolute path
+ if os.type == 'windows' then
+ luatex_prog = luatex_prog .. '.exe'
+ end
+
+ -- write TeX file which includes cropped pdf page
+ -- adapted from Heiko Oberdiek's pdfcrop utility.
+ -- first, create a table with the component strings for the tex source
+ dummy = {}
+ dummy[1] = [[
+\catcode37 14 % percent
+\catcode33 12 % exclam
+\catcode34 12 % quote
+\catcode35 6 % hash
+\catcode39 12 % apostrophe
+\catcode40 12 % left parenthesis
+\catcode41 12 % right parenthesis
+\catcode45 12 % minus
+\catcode46 12 % period
+\catcode60 12 % less
+\catcode61 12 % equals
+\catcode62 12 % greater
+\catcode64 12 % at
+\catcode91 12 % left square
+\catcode93 12 % right square
+\catcode96 12 % back tick
+\catcode123 1 % left curly brace
+\catcode125 2 % right curly brace
+\catcode126 12 % tilde
+\catcode`\#=6 %
+\escapechar=92 %
+\def\IfUndefined#1#2#3{%
+ \begingroup\expandafter\expandafter\expandafter\endgroup
+ \expandafter\ifx\csname#1\endcsname\relax
+ #2%
+ \else
+ #3%
+ \fi
+}
+\begingroup
+ \newlinechar=10 %
+ \endlinechar=\newlinechar %
+ \ifnum0%
+ \directlua{%
+ if tex.enableprimitives then
+ tex.enableprimitives('TEST', {
+ 'luatexversion',
+ 'pdfoutput',
+ 'pdfcompresslevel',
+ 'pdfhorigin',
+ 'pdfvorigin',
+ 'pdfpagewidth',
+ 'pdfpageheight',
+ 'pdfmapfile',
+ 'pdfximage',
+ 'pdflastximage',
+ 'pdfrefximage',
+ 'pdfminorversion',
+ 'pdfobjcompresslevel',
+ })
+ tex.print('1')
+ end
+ }%
+ \ifx\TESTluatexversion\UnDeFiNeD\else 1\fi %
+ =11 %
+ \global\let\luatexversion\luatexversion %
+ \global\let\pdfoutput\TESTpdfoutput %
+ \global\let\pdfcompresslevel\TESTpdfcompresslevel %
+ \global\let\pdfhorigin\TESTpdfhorigin %
+ \global\let\pdfvorigin\TESTpdfvorigin %
+ \global\let\pdfpagewidth\TESTpdfpagewidth %
+ \global\let\pdfpageheight\TESTpdfpageheight %
+ \global\let\pdfmapfile\TESTpdfmapfile %
+ \global\let\pdfximage\TESTpdfximage %
+ \global\let\pdflastximage\TESTpdflastximage %
+ \global\let\pdfrefximage\TESTpdfrefximage %
+ \global\let\pdfminorversion\TESTpdfminorversion %
+ \global\let\pdfobjcompresslevel\TESTpdfobjcompresslevel %
+ \else %
+ \errmessage{%
+ Missing \string\luatexversion %
+ }%
+ \fi %
+\endgroup %
+
+\pdfoutput=1 %
+\pdfcompresslevel=9 %
+\csname pdfmapfile\endcsname{}
+\def\setpdfversion#1#2{%
+ \ifnum#2>1 %
+ \pdfobjcompresslevel=2 %
+ % including unsupported pdf version!
+ \pdfinclusionerrorlevel=0
+ \pdfminorversion=9\relax
+ \else
+ \ifnum#1>4 %
+ \pdfobjcompresslevel=2 %
+ \else
+ \pdfobjcompresslevel=0 %
+ \fi
+ \pdfminorversion=#1\relax
+ \fi
+}
+\def\page #1 [#2 #3 #4 #5]{%
+ \count0=#1\relax
+ \setbox0=\hbox{%
+ \pdfximage page #1 mediabox{]]
+ dummy[2] = self.path
+ dummy[3] = [[}%
+ \pdfrefximage\pdflastximage
+ }%
+ \pdfhorigin=-#2bp\relax
+ \pdfvorigin=#3bp\relax
+ \pdfpagewidth=#4bp\relax
+ \advance\pdfpagewidth by -#2bp\relax
+ \pdfpageheight=#5bp\relax
+ \advance\pdfpageheight by -#3bp\relax
+ \ht0=\pdfpageheight
+ \shipout\box0\relax
+}
+]]
+ -- pdf minor version to write to tex header
+ local tex_miver = false
+ if settings.pdf_version=='default' then
+ tex_miver = self.miver
+ else
+ -- in this case, gs should already have converted to
+ -- a sufficiently low version
+ tex_miver = tonumber(settings.pdf_version)
+ if tex_miver>self.miver then
+ errror('Pdf_crop: forgot to reduce pdf version')
+ end
+ end
+ dummy[4] = string.format([[
+\setpdfversion{%d}{%d}
+\page %d [%f %f %f %f]
+\csname @@end\endcsname
+\end
+]],
+ tex_miver, self.maver, options.page or 1,
+ hrbb.x1, hrbb.y1, hrbb.x2, hrbb.y2)
+
+ local textemp = mktemp('tex') -- this also takes care of pdf:
+ local pdftemp = string.gsub(textemp, 'tex$', 'pdf')
+ local f = io.open(textemp, 'w')
+ f:write(table.concat(dummy, ''))
+ f:close()
+ local cmd, res, psp
+ cmd = {luatex_prog, '--safer', '--no-shell-escape', textemp}
+ log_cmd(cmd)
+ res = os.spawn(cmd)
+ if res and res==0 and lfs.attributes(pdftemp, 'size')>0 then
+ psp = PsPdf:from_path(pdftemp)
+ return psp
+ else
+ errror('pdf_crop failed on '..self.path)
+ end
+end
+
+function PsPdf:eps_to_pdf()
+
+ -- option to be fulfilled: gray
+ -- set target and pdf version if applicable
+ -- dbg('PsPdf:eps_to_pdf')
+ if self.type~='eps' then
+ errror('PsPdf:eps_to_pdf called for non-eps file '.. self.path)
+ end
+ local cmd
+ if options.bbox and self.bb:nonnegative() then
+ self = self:eps_crop() -- this sets options.bbox to false
+ end
+ cmd = tab_combine({gs_options, pdf_options})
+ -- dbg(table.concat(cmd,' '))
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ -- dbg(table.concat(cmd,' '))
+ options.gray = false
+ end
+ table.insert(cmd, '-dEPSCrop') -- always hires bb
+ -- dbg(table.concat(cmd,' '))
+ local psp = PsPdf:new('pdf')
+ table.insert(cmd, '-sOutputFile#'..psp.path)
+ -- dbg(table.concat(cmd,' '))
+ cmd = tab_combine({cmd, pdf_tailoptions, {self.path}})
+ -- dbg(table.concat(cmd,' '))
+ log_cmd(cmd)
+ local res = os.spawn(cmd)
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ psp.pages, psp.miver, psp.maver = pdf_props(psp.path)
+ return psp
+ else
+ errror('eps_to_pdf failed on '..self.path)
+ end
+end -- eps_to_pdf
+
+-- Converting from pdf to pdf with grayscaling and/or page selection
+
+function PsPdf:pdf_to_pdf()
+
+ -- option to be fulfilled: gray and optionally page.
+ -- do not call this just for page selection because
+ -- pdf_crop can do this in a less invasive manner
+ -- dbg('PsPdf:pdf_to_pdf')
+ if self.type~='pdf' then
+ errror('PsPdf:pdf_to_pdf called for non-pdf file '.. self.path)
+ end
+ local cmd
+ if options.page and options.page > self.pages then
+ errror('PsPdf:pdf_to_pdf called with non-existent page '.. options.page)
+ end
+ cmd = tab_combine({gs_options, pdf_options})
+ -- dbg(table.concat(cmd,' '))
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ -- dbg(table.concat(cmd,' '))
+ options.gray = false
+ end
+ if options.page then
+ table.insert(cmd, '-dFirstPage#'..tostring(options.page))
+ table.insert(cmd, '-dLastPage#'..tostring(options.page))
+ -- dbg(table.concat(cmd,' '))
+ options.page = false
+ end
+ local psp = PsPdf:new('pdf')
+ table.insert(cmd, '-sOutputFile#'..psp.path)
+ cmd = tab_combine({cmd, pdf_tailoptions})
+ -- dbg(table.concat(cmd,' '))
+ table.insert(cmd, self.path)
+ -- dbg(table.concat(cmd,' '))
+ log_cmd(cmd)
+ local res = os.spawn(cmd)
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ psp.pages, psp.miver, psp.maver = pdf_props(psp.path)
+ return psp
+ else
+ errror('pdf_to_pdf failed on '..self.path)
+ end
+end -- pdf_to_pdf
+
+function PsPdf:pdf_to_eps()
+
+ -- options to be fulfilled: bbox and page
+ -- dbg(tostring(settings.pdftops_prog))
+ local psp = PsPdf:new('eps')
+ local cmd, res
+ local page = false
+ if self.pages>1 then
+ page = 1
+ if options.page then page = options.page end
+ if options.page and options.page > self.pages then
+ errror('PsPdf:pdf_to_eps called with non-existant page '.. options.page)
+ end
+ page = tostring(page)
+ end
+ if pdftops then
+ if page then
+ cmd = tab_combine({{pdftops}, ps_options,
+ {'-f', page, '-l', page,
+ '-eps', self.path, psp.path}})
+ else
+ cmd = tab_combine({{pdftops}, ps_options,
+ {'-eps', self.path, psp.path}})
+ end
+ options.page = false
+ log_cmd(cmd)
+ if os.type=='windows' then
+ -- suppress console output of 'No display font for...' messages,
+ -- which are usually harmless and for which I know no easy fix
+ res = os.spawn({'cmd', '/c', table.concat(cmd, ' ')..' 2>>'..log_bsl})
+ else
+ res = os.spawn(cmd)
+ end
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ psp.pages = 1
+ else
+ errror('pdf_to_eps failed on '..self.path)
+ end
+ -- fix for incorrect DSC header produced by some versions of pdftops:
+ -- if necessary, change line `% Produced by ...' into `%%Produced by ...'
+ -- this is usually the second line.
+ -- otherwise the DSC header would be terminated before the bbox comment.
+ -- this problem exists with pdftops from TL2011/w32.
+ local slurp -- input buffer
+ local fin = io.open(psp.path, 'rb')
+ if not fin then errror('Cannot read '..psp.path) end
+
+ -- remaining, unscanned length of input buffer slurp
+ local unscanned = math.min(lfs.attributes(psp.path, 'size'),bufsize)
+ slurp = fin:read(unscanned)
+ local i, i_bb = 0, false
+ local needs_fixing = false
+ local pre_lines = {}
+ local offset = 0
+ while unscanned>0 do
+ i = i+1
+ if string.find(slurp,'[\n\r]')==1 then
+ l,slurp = string.match(slurp, '^([\n\r]+)(.*)$')
+ else
+ l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$')
+ if string.match(l, Bb.bb_pat) then
+ -- bbox line
+ i_bb = i
+ elseif string.match(l, '^%%%s') then -- `%' is escape char: doubled
+ -- %X with X printable would be ok
+ needs_fixing = true
+ -- fix rightaway
+ l = string.gsub(l, '^%%%s', '%%%%') -- same length
+ end
+ end -- eol/non-eol
+ pre_lines[i] = l
+ unscanned = unscanned - string.len(l)
+ offset = offset + string.len(l)
+ if i_bb then break end
+ end -- while
+ fin:close()
+ if needs_fixing then
+ -- write a new eps file
+ local newfile = mktemp('eps')
+ fout = io.open(newfile, 'wb')
+ if not fout then errror('Cannot write new file '.. newfile) end
+ fout:write(table.concat(pre_lines))
+ fout:close()
+ slice_file(psp.path, newfile,
+ lfs.attributes(psp.path,'size') - offset, offset, 'ab')
+ psp.path = newfile
+ end -- needs_fixing
+ else -- use ghostscript
+ cmd = tab_combine({gs_options,
+ {'-sDEVICE#epswrite', '-dLanguageLevel#3'}})
+ -- the restrictions on eps files are apparently
+ -- incompatible with grayscaling
+ if options.page then
+ table.insert(cmd, '-dFirstPage='..page)
+ table.insert(cmd, '-dLastPage='..page)
+ end
+ table.insert(cmd, '-sOutputFile='..psp.path)
+ table.insert(cmd, self.path)
+ options.page = false
+ log_cmd(cmd)
+ res = os.spawn(cmd)
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ psp.pages = 1
+ else
+ errror('pdf_to_eps failed on '..self.path)
+ end
+ end -- use ghostscript
+ psp:find_bb_simple()
+ if options.bbox then psp = psp:eps_crop() end
+ return psp
+
+end -- pdf_to_eps
+
+function PsPdf:ps_to_pdf()
+
+ -- options to be fulfilled: gray
+ -- dbg('PsPdf:ps_to_pdf')
+ if self.type~='ps' then
+ errror('PsPdf:ps_to_pdf called for non-ps file '.. self.path)
+ end
+ local cmd
+ cmd = tab_combine({gs_options, pdf_options})
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ options.gray = false
+ end
+ local psp = PsPdf:new('pdf')
+ table.insert(cmd, '-sOutputFile#'..psp.path)
+ cmd = tab_combine({cmd, pdf_tailoptions})
+ table.insert(cmd, self.path)
+ log_cmd(cmd)
+ local res = os.spawn(cmd)
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ psp.pages, psp.miver, psp.maver = pdf_props(psp.path)
+ return psp
+ else
+ errror('ps_to_pdf failed on '..self.path)
+ end
+
+end -- PsPdf:ps_to_pdf
+
+function PsPdf:pdf_to_ps()
+
+ -- options to be fulfilled: page and, if not using pdftops, also gray
+ -- dbg('PsPdf:pdf_to_ps')
+ local psp = PsPdf:new('ps')
+ local page = false
+ if self.pages>1 then
+ if options.page and options.page > self.pages then
+ errror('PsPdf:pdf_to_ps called with non-existant page '.. options.page)
+ elseif options.page then
+ page = tostring(options.page)
+ psp.pages = 1
+ end
+ else
+ psp.pages = self.pages
+ end
+ local cmd, res
+ if pdftops then
+ cmd = tab_combine({{pdftops}, ps_options})
+ if page then
+ cmd = tab_combine({cmd, {'-f', page, '-l', page}})
+ end
+ cmd = tab_combine({cmd, {'-paper', 'match', self.path, psp.path}})
+ -- cmd[0] = pdftops
+ else -- use ghostscript
+ cmd = tab_combine({gs_options,
+ {'-sDEVICE#ps2write', '-dLanguageLevel#3'}})
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ -- dbg(table.concat(cmd,' '))
+ options.gray = false
+ end
+ if page then
+ cmd = tab_combine({cmd, {'-dFirstPage#'..page, '-dLastPage#'..page}})
+ end
+ table.insert(cmd, '-sOutputFile#'..psp.path)
+ -- table.insert(cmd, '-f')
+ table.insert(cmd, self.path)
+ end
+ options.page = false
+ log_cmd(cmd)
+ -- if os.type=='windows' and pdftops and not is_miktex then
+ -- if os.type=='windows' and pdftops then
+ -- -- suppress console output of 'No display font for...' messages,
+ -- -- which are usually harmless and for which I know no easy fix
+ -- res = os.spawn({'cmd', '/c', table.concat(cmd, ' ')..' 2>>'..log_bsl})
+ -- else
+ res = os.spawn(cmd)
+ -- end
+ if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ return psp
+ else
+ errror('pdf_to_ps failed on '..self.path)
+ end
+end -- PsPdf:pdf_to_ps
+
+function PsPdf:any_to_any()
+
+ -- weed out nonsense options
+
+ -- dbg('PsPdf:any_to_any')
+ if options.type=='ps' then
+ options.bbox = false
+ -- dbg('Ignoring bbox option for ps output')
+ end
+ if options.bbox and not options.page then
+ options.page = 1
+ -- dbg('Selecting page 1 for bbox')
+ end
+ if self.pages==1 then
+ options.page = false
+ -- dbg('dropping page selection; source is already a 1-page document')
+ end
+ -- for _,o in ipairs({'page', 'gray', 'bbox'}) do
+ -- -- if options[o] then dbg('Do option '..o) end
+ -- end
+
+ -- check source and destination filetypes
+
+ if not self.type then
+ errror('any_to_any: cannot convert; unsupported source filetype')
+ end
+ if not options.type or options.type=='epsPreview' then
+ errror('any_to_any: cannot convert; unsupported destination filetype')
+ end
+
+ -- `distiller' settings depend on whether final output is pdf
+ if options.type=='pdf' then
+ table.insert(pdf_options, '-dPDFSETTINGS#/'..settings.pdf_target)
+ if settings.pdf_version~='default' then
+ table.insert(pdf_options, '-dCompatibilityLevel#'..settings.pdf_version)
+ end
+ -- below, try <</NeverEmbed [/Times-Roman /TimesBold ...]>>
+ if settings.pdf_target=='screen' or settings.pdf_target=='ebook' then
+ pdf_tailoptions = {'-c',
+ '.setpdfwrite', '-f'}
+ -- -f ensures that the input filename is not added to the -c string
+ else
+ pdf_tailoptions = {'-c',
+ '.setpdfwrite <</NeverEmbed [ ]>> setdistillerparams', '-f'}
+ end
+ else
+ table.insert(pdf_options, '-dPDFSETTINGS#/default')
+ pdf_tailoptions = {'-c',
+ '.setpdfwrite <</NeverEmbed [ ]>> setdistillerparams', '-f'}
+ end
+
+ -- each single-step conversion takes care of options it can handle
+ -- and sets those options to false.
+ -- for boundingboxes, eps_crop is either called explicitly
+ -- or called implicitly by another converter.
+ -- pdf_crop is always called explicitly and always as the last step
+
+ local psp = self
+
+ if psp.type=='eps' or psp.type=='epsPreview' then
+ -- As a side effect of eps_clean, the modified source file is copied
+ -- to the temp subdirectory.
+ psp = psp:eps_clean()
+ if options.bbox and psp.bb:nonnegative() then
+ psp = psp:eps_crop()
+ end
+ if options.type=='eps' then
+ if options.gray or options.bbox then
+ -- bbox: eps_crop was apparently not applicable: pdf roundtrip
+ psp = psp:eps_to_pdf():pdf_to_eps()
+ end
+ elseif options.type=='pdf' then
+ psp = psp:eps_to_pdf()
+ if options.bbox then
+ psp = psp:pdf_crop()
+ end
+ elseif options.type=='ps' then
+ psp = psp:eps_to_pdf():pdf_to_ps()
+ end
+ return psp
+
+ elseif psp.type=='ps' then
+ -- preliminary:
+ -- copy infile to a file in the temp directory, for gs -dSAFER
+ psp.path = mktemp(psp.type)
+ slice_file(infile, psp.path)
+
+ -- actual conversion
+ if options.type=='eps' then
+ return psp:ps_to_pdf():pdf_to_eps()
+ elseif options.type=='pdf' then
+ if options.bbox or options.page then
+ return psp:ps_to_pdf():pdf_crop()
+ else
+ return psp:ps_to_pdf()
+ end
+ elseif options.type=='ps' then
+ if options.gray or options.page then
+ return psp:ps_to_pdf():pdf_to_ps()
+ else
+ return psp -- no conversion necessary
+ end
+ end -- pdf => ps
+
+ elseif psp.type=='pdf' then
+ -- preliminary:
+ -- copy infile to a file in the temp directory, for gs -dSAFER
+ psp.path = mktemp(psp.type)
+ slice_file(infile, psp.path)
+
+ -- actual conversion
+ if options.type=='eps' then
+ if options.gray then
+ -- one-step grayscaling available for gs/ps but not for gs/eps
+ return psp:pdf_to_pdf():pdf_to_eps()
+ else
+ return psp:pdf_to_eps()
+ end
+ elseif options.type=='pdf' then
+ -- pdf_crop can take care of bbox and page,
+ -- but not of gray and not of target use or pdf version
+ do
+ local need_gs = false
+ -- compare actual and required versions,
+ -- allowing for rounding differences
+ if settings.pdf_version~='default' and
+ (psp.maver+0.1*psp.miver)>tonumber(settings.pdf_version)-0.01 then
+ need_gs = true
+ end
+ if settings.pdf_target~='default' then
+ need_gs = true
+ end
+ if options.gray then
+ need_gs = true
+ end
+ local need_crop = false
+ if options.bbox then
+ need_crop = true
+ end
+ if (not need_gs) and options.page then
+ need_crop = true
+ end
+ if need_gs then
+ psp = psp:pdf_to_pdf()
+ end
+ if need_crop or (psp.pages>1 and options.page) then
+ psp = psp:pdf_crop()
+ end
+ return psp
+ end
+ elseif options.type=='ps' then
+ if options.gray and pdftops then
+ return psp:pdf_to_pdf():pdf_to_ps()
+ else
+ return psp:pdf_to_ps()
+ end
+ end -- pdf => ps
+ end -- psp.type=='ps'|'pdf'
+end -- any_to_any
+
+-- start logging ---------------------------------
+
+-- log rotate if logfile too big
+if lfs.attributes(logfile) and lfs.attributes(logfile).size > 100000 then
+ if lfs.attributes(oldlog) then
+ if os.remove(oldlog) then os.rename(logfile,oldlog) end
+ elseif lfs.attributes(logfile) then do
+ -- separate epsdir runs with empty lines
+ local f = io.open(logfile, 'ab')
+ f:write(eol)
+ f:close()
+ end end -- do elseif
+end -- if lfs...logfile
+
+write_log('epspdf '..table.concat(arg, ' '))
+
+infile = false
+outfile = false
+
+-- some debug output
+
+-- dbg ('os is ' .. os.type .. ' and ' .. os.name)
+-- dbg ('texlua in ' .. os.selfdir)
+-- dbg('Ghostscript: ' .. gs_prog)
+
+-- dbg('\nSettings are:\n')
+-- for k,v in pairs(settings) do dbg(k .. ' = ' .. tostring(v)) end
+
+-- Handle command-line
+
+do
+
+ read_settings(rcfile)
+
+ -- dbg('Defining cmdline options')
+ opts = {}
+
+ opts.page = {
+ type = 'string', val = nil,
+ forms = {'-p', '--page', '--pagenumber'},
+ placeholder = 'PNUM',
+ negforms = nil,
+ help = 'Page number; must be a positive integer'
+ }
+
+ opts.gray = {
+ type = 'boolean', val = nil,
+ forms = {'-g', '--grey', '--gray', '-G', '--GREY', '--GRAY'},
+ negforms = nil,
+ help = 'Convert to grayscale'
+ }
+
+ opts.bbox = {
+ type = 'boolean', val = nil,
+ forms = {'-b', '--bbox', '--BoundingBox'},
+ negforms = nil,
+ help = 'Compute tight boundingbox'
+ }
+
+---[[ ignored; included for backward compatibility
+ opts.use_hires_bb = {
+ type = 'boolean', val = nil,
+ forms = {'-r', '--hires'},
+ negforms = {'-n', '--no-hires'},
+ }
+
+ opts.custom = {
+ type = 'string', val = nil,
+ forms = {'-C', '--custom', '-P', '--psoptions'},
+ negforms = nil
+ }
+ --]]
+
+ opts.pdf_target = {
+ type = 'string', val = nil,
+ forms = {'-T', '--target'},
+ placeholder = 'TARGET',
+ negforms = nil,
+ help = descriptions.pdf_target
+ }
+ opts.pdf_version = {
+ type = 'string', val = nil,
+ forms = {'-N', '--pdfversion'},
+ placeholder = 'VERSION',
+ negforms = nil,
+ help = descriptions.pdf_version
+ }
+
+ if os.type=='windows' and not is_tl_w then
+ opts.pdftops_prog = {
+ type = 'string', val = nil,
+ forms = {'--pdftops'},
+ placeholder = 'PATH',
+ negforms = nil,
+ help = descriptions.pdftops_prog
+ }
+ end
+
+ opts.use_pdftops = {
+ type = 'boolean', val = nil,
+ forms = {'-U'},
+ negforms = {'-I'},
+ help = descriptions.use_pdftops
+ }
+
+ opts.info = {
+ type = 'boolean', val = nil,
+ forms = {'-i', '--info'},
+ negforms = nil,
+ help = 'Info: display detected filetype and exit'
+ }
+
+ opts.help = {
+ type = 'boolean', val = nil,
+ forms = {'-h', '--help'},
+ negforms = nil,
+ help = 'Display this help message and exit'
+ }
+
+ opts.version = {
+ type = 'boolean', val = nil,
+ forms = {'-v', '--version'},
+ negforms = nil,
+ help = 'Display version info and exit'
+ }
+
+ opts.save = {
+ type = 'boolean', val = nil,
+ forms = {'-s', '--save'},
+ negforms = nil,
+ help = 'Save some settings to configuration file'
+ }
+
+ opts.debug = {
+ type = 'boolean', val = nil,
+ forms = {'-d'},
+ negforms = nil,
+ help = 'Debug: do not remove temp files'
+ }
+
+ opts.gui = {
+ type = 'string', val = nil,
+ forms = {'--gui'},
+ negforms = nil,
+ help = nil -- reserved for use by epspdftk
+ }
+
+ if #arg < 1 then help('No parameters') end
+
+ -- command-line parsing
+
+ -- -r="tata tata" is parsed by [tex]lua as a single argument
+ -- lua/linux retains the quotes,
+ -- lua/windows strips them.
+ -- texlua strips them, both on unix and on windows.
+
+ local i=1
+ while i<=#arg and string.sub(arg[i],1,1)=='-' do
+ -- dbg('parse argument '..tostring(i)..': '..arg[i])
+ local parsed = false
+ local kk, vv = string.match(arg[i],'([^=]+)=(.*)$')
+ if kk==nil then
+ kk = arg[i] -- also vv==nil
+ else
+ vv = strip_outer_spaces(vv)
+ end
+ for p, o in pairs(opts) do
+ -- dbg(' try '..p)
+ if in_list(kk, o.forms) or in_list(kk, o.negforms) then
+ parsed = true
+ if o.type == 'boolean' then
+ if vv then help(kk..' should not have a parameter.') end
+ if in_list(kk, o.forms) then
+ o.val = true
+ else
+ o.val = false
+ end
+ elseif vv then
+ o.val = vv
+ else
+ i = i + 1
+ if i>#arg then
+ help('Missing parameter to '..kk)
+ end
+ o.val = strip_outer_spaces(arg[i])
+ end -- testing for o.type or vv
+ break -- for
+ end -- if in_list
+ end -- for
+ if not parsed then help('illegal parameter '..kk) end
+ i = i + 1
+ end -- while
+
+ -- some debug output
+
+ --[[
+ if i<=#arg then
+ dbg('non-option arguments:')
+ for j=i,#arg do dbg(arg[j]) end
+ dbg(eol)
+ else
+ dbg('no non-option arguments')
+ end
+
+ for i=1,#arg do dbg(arg[i]) end
+
+ dbg(eol..'Options from command-line:')
+ for p, o in pairs(opts) do
+ if o.val==nil then
+ dbg(p..': undefined')
+ else
+ dbg(p..': '..tostring(o.val))
+ end
+ end
+ --]]
+
+ -- check and interpret opts.
+ -- Copy to either settings or to options table.
+ -- abort (via help function) at syntax error.
+
+ -- page
+
+ if opts.page.val then
+ local pnum = tonumber(opts.page.val)
+ if pnum<=0 or math.floor(pnum) ~= pnum then
+ help(opts.page.val..' not a positive integer')
+ else
+ options.page = pnum
+ end
+ end
+
+ -- grayscaling
+
+ if opts.gray.val then
+ options.gray = true
+ else
+ options.gray = false
+ end
+
+ -- boundingbox
+
+ if opts.bbox.val then
+ options.bbox = true
+ else
+ options.bbox = false
+ end
+
+ --[[
+ -- using hires boundingbox
+
+ if opts.use_hires_bb.val~=nil then
+ settings.use_hires_bb = opts.use_hires_bb.val
+ end
+ --]]
+
+ -- using pdftops
+
+ if opts.use_pdftops.val~=nil then
+ settings.use_pdftops = opts.use_pdftops.val
+ end
+
+ -- pdf target use
+
+ if opts.pdf_target.val~=nil then
+ if in_list(opts.pdf_target.val, pdf_targets) then
+ settings.pdf_target = opts.pdf_target.val
+ else
+ help('Illegal value '..opts.pdf_target.val..' for pdf_target')
+ end
+ end
+
+ -- pdf version
+
+ if opts.pdf_version.val~=nil then
+ if in_list(opts.pdf_version.val, pdf_versions) then
+ settings.pdf_version = opts.pdf_version.val
+ else
+ help('Illegal value '..opts.pdf_version.val..' for pdf_version')
+ end
+ end
+
+ -- pdftops program
+
+ -- pdftops has already been been initialized to false
+ if os.type=='windows' and not is_tl_w and opts.pdftops_prog.val then
+ settings.pdftops_prog = is_prog(opts.pdftops_prog.val)
+ if settings.use_pdftops then
+ pdftops = settings.pdftops_prog
+ end
+ elseif os.type=='windows' and not is_tl_w then
+ if settings.use_pdftops then
+ pdftops = is_prog(settings.pdftops_prog)
+ end
+ elseif os.type=='windows' then
+ if settings.use_pdftops then
+ pdftops = os.selfdir..'/pdftops.exe'
+ end
+ else
+ if settings.use_pdftops then
+ pdftops = find_on_path('pdftops')
+ end
+ end
+ -- dbg('Option handling; pdftops is '..tostring(pdftops))
+
+ -- other options
+
+ if opts.save.val then
+ write_settings(rcfile)
+ end
+
+ if opts.debug.val then
+ options.debug = true
+ end
+
+ if opts.info.val then
+ options.info = true
+ end
+
+ if opts.help.val then
+ help()
+ end
+
+ if opts.version.val then
+ show_version()
+ os.exit()
+ end
+
+ if opts.gui.val then
+ gui(opts.gui.val)
+ end
+
+ -- now we need 1 or 2 filenames, unless the user really only
+ -- wanted to save options without further action.
+
+ if i>#arg then
+ if opts.save.val then os.exit() else help('No filenames') end
+ end
+
+ infile = arg[i]
+ if i<#arg then
+ outfile = arg[i+1]
+ else
+ outfile = false
+ end
+ if (#arg>i and options.info) or (#arg>i+1) then
+ help('Surplus non-option parameters')
+ end
+
+ -- one final quick option
+ if opts.info.val then
+ info(infile)
+ end
+
+ -- add pdf_version and pdf_target to the options array,
+ -- from where it will be set to false when realized
+ if settings.pdf_target == 'default' then
+ options.pdf_target = false
+ else
+ options.pdf_target = settings.pdf_target
+ end
+ if settings.pdf_version == 'default' then
+ options.pdf_version = false
+ else
+ options.pdf_version = tonumber(settings.pdf_version)
+ end
+
+end -- decoding command-line
+
+-- dbg('After command-line processing\n Settings')
+-- -- print settings- and options array with dbg
+-- for k, v in pairs(settings) do
+-- dbg(k..': '..tostring(v))
+-- end
+-- dbg(' Options')
+-- for k, v in pairs(options) do
+-- dbg(k..': '..tostring(v))
+-- end
+
+--[[
+
+Once it becomes clear that real work needs to be done,
+we shall create a temp directory in the parent directory of the output file
+and use that as working directory.
+
+1. consistent with the ghostscript -dSAFER option
+2. we can move/rename rather than copy the final temp file
+ to the output file
+
+ because of gs -dSAFER restrictions, infile must be in (a
+ subdirectory of) the directory of the output file, e.g. in the
+ temp directory.
+
+ Also because of -dSAFER, we copy infile to the temp directory of
+ it is not in the same directory as outfile.
+
+--]]
+
+do
+ local source = io.open(infile)
+ if not source then
+ error(infile .. ' not readable')
+ end
+ source:close()
+ local in_dir
+ infile, in_dir = absolute_path(infile)
+
+ -- we need a writable dest_dir as parent for a temp directory,
+ -- in some cases even for option info
+ if not outfile then
+ dest_dir = in_dir
+ else
+ outfile, dest_dir = absolute_path(outfile)
+ end
+ lfs.chdir(dest_dir)
+ tempdir = os.tmpdir() -- relative path!
+ local c, e
+ c, e = lfs.chdir(tempdir)
+ if not c then
+ write_log(e)
+ tempdir = false
+ -- errror('Failure to create temporary directory')
+ else
+ tempdir = lfs.currentdir() -- better for logging: absolute path
+ write_log('Working directory: '..tempdir)
+ end
+
+ infile, source_dir = absolute_path(infile)
+ intype = identify(infile)
+
+ -- remaining cases: want a real conversion
+ if not intype then
+ error(infile..' has an unsupported filetype')
+ end
+
+ if not outfile then
+ -- derive outfile from infile: [e]ps => pdf, pdf => eps
+ if intype=='pdf' then
+ outfile = string.gsub(infile,'%.[^%.]*$','eps')
+ else
+ outfile = string.gsub(infile,'%.[^%.]*$','.pdf')
+ end
+ end
+
+ --sanity check on output filetype
+ options.type = string.match(outfile, '.*%.([^%.]+)$')
+ if not options.type or (options.type~='ps'
+ and options.type~='eps' and options.type~='pdf') then
+ errror('Output file '..outfile..
+ ' should have extension .eps, .ps or .pdf')
+ end
+
+ -- if outfile equal to infile, copy to temp directory, then remove
+ if outfile==infile then
+ infile = mktemp(intype)
+ slice_file(outfile, infile)
+ write_log('Copying '..outfile..' to temporary file '..infile..'.')
+ end
+
+ -- had some trouble under msw when removing outfile later so do it now
+ if lfs.isfile(outfile) then
+ os.remove(outfile)
+ if lfs.attributes(outfile) then
+ errror('Cannot overwrite '..outfile)
+ end
+ end
+
+ local fout = io.open(outfile, 'w')
+ if not fout then
+ errror('Output file '..outfile..' not writable; aborting')
+ else
+ fout:close()
+ end
+
+ source = PsPdf:from_path(infile)
+ dest = source:any_to_any()
+ -- options will be read from the global options table
+ -- and turned off after they have been satisfied.
+ -- irrelevant options are quietly ignored.
+
+ if os.type=='unix' then
+ write_log('Rename '..dest.path..' to '..outfile)
+ os.rename(dest.path, outfile) -- we picked our temp dir to make this possible
+ else
+ write_log('Copying '..dest.path..' to '..outfile)
+ slice_file(dest.path, outfile)
+ end
+ if not options.debug then
+ cleantemp()
+ end
+ if lfs.isfile(outfile) and lfs.attributes(outfile, 'size')>0 then
+ os.exit()
+ else
+ errror('Conversion failed')
+ end
+end