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.tlu1238
1 files changed, 563 insertions, 675 deletions
diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.tlu b/Master/texmf-dist/scripts/epspdf/epspdf.tlu
index 709a2d9b691..0007ade0742 100755
--- a/Master/texmf-dist/scripts/epspdf/epspdf.tlu
+++ b/Master/texmf-dist/scripts/epspdf/epspdf.tlu
@@ -2,17 +2,22 @@
kpse.set_program_name('texlua')
--- epspdf conversion utility
+--[[
+epspdf conversion utility
--- 0.6.0: first texlua version
--- 0.6.1: allow TeX installation on path with spaces
--- 0.6.2: compatibility fix for luatex 0.81
--- 0.6.3: compatibility fixes for luatex 0.9x
+0.6.0: first texlua version
+0.6.1: allow TeX installation on path with spaces
+0.6.2: compatibility fix for luatex 0.81
+0.6.3: compatibility fixes for luatex 0.9x
+0.6.4: adaptations for newer versions of LuaTeX and ghostscript;
+ some refactoring; better handling of some corner cases
-ep_version = '0.6.3'
-ep_copyright = '2006, 2008, 2009, 2010, 2011, 2013, 2014, 2015, 2016'
+Copyright (C) 2006-2019 Siep Kroonenberg
---[[
+siepo at bitmuis nl
+
+This program is free software, licensed under the GNU GPL, >=2.0.
+This software comes with absolutely NO WARRANTY. Use at your own risk!
Note.
TeX code for cropping pdfs adapted from Heiko Oberdiek's pdfcrop utility
@@ -26,14 +31,15 @@ Program structure
- other general utilities
- infrastructure: logging and temporary files
- reading and writing settings
- - gui function
+ - gui function for communicating with frontend
- boundingboxes
- manipulating [e]ps- and pdf files
- the PsPdf object:
- creator functions
- boundingbox handling
- one-step conversion methods
- - any_to_any method
+- any_to_any function.
+ this function checks options, the one-step converters check for success.
- main initialization section:
- collecting system information
- infrastructure: setting up logging and temp directory
@@ -71,6 +77,9 @@ else
path_sep = ';'
end
+infile = false
+outfile = false
+
bufsize=16000 -- for reading and writing files
-- these `declarations' are not really needed;
@@ -84,8 +93,8 @@ cwd = ''
-- Actual values determined during system-dependent initialization
is_miktex = false
is_tl_w32 = false
--- recent versions of miktex include pdftops
-auto_psp = true -- configuration of pdftops_prog not needed or not allowed
+
+-- Luatex 1.09 and later replace epdf with pdfe
-- some global file- and directory names
gs_prog = false
@@ -111,7 +120,7 @@ gray_options = false
-- logging ------------------------
-- we open and close the logfile anew for each write.
--- failure to open constitutes no error.
+-- failure to open does not constitute an error.
function print_log(s)
local f = io.open(logfile, 'a')
@@ -141,6 +150,9 @@ end
-- When run from the Tcl/Tk gui, this gui will capture error messages.
function errror(mess)
+ if infile and outfile then
+ mess = 'Failure to convert '..infile..' to '..outfile..':\n'..mess
+ end
if logfile then pcall(write_log, mess) end
-- ignore result of pcall: we can do nothing about failure
error(mess, 2)
@@ -151,38 +163,24 @@ function warn(mess)
print(mess)
end
-function dbg(mess)
- if options.debug then
- warn(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 dbg(mess)
+-- if options.debug then
+-- warn(mess)
+-- end
+-- end
-- file- and path utilities ----------------
-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 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
-- prepend or append dir to path if necessary
function maybe_add_path(dir, append)
@@ -288,41 +286,22 @@ end -- absolute_path
-- we return the original string, 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 prog
+ if os.type == 'unix' then
+ for d in string.gmatch(os.getenv('PATH'), '[^:]+') do
+ if lfs.isfile(d..'/'..prog) then
+ return prog
+ end
+ end
+ else
+ for d in string.gmatch(os.getenv('PATH'), '[^;]+') do
+ if lfs.isfile(d..'\\'..prog) then
+ return prog
+ end
end
end
return false
end -- find_on_path
--- On Windows, we do not count so much on the existing searchpath
--- so is_prog tests whether the file exists and is an exe file.
-
-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
-
function dir_writable(d)
-- because directory attributes do not tell the whole story,
-- we actually try to create a file in the directory.
@@ -368,10 +347,8 @@ function system_tempdir ()
d = '/tmp'
end
end
- -- if d then dbg('system tempdir: '..d) end
-- cygwin: $TEMP=/tmp, root '/' being root of cygwin installation
if d and not dir_writable(d) then
- dbg('unfortunately, '..d..' not writable')
d = false
end
return d
@@ -424,6 +401,7 @@ function tab_combine (t)
return res
end -- tab_combine
+-- files ----------------------------------------------------
-- 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
@@ -434,6 +412,22 @@ function slice_file(source, dest, len, offset, mode)
-- specifying false as value
-- Assume caller ensured parameters of correct type.
-- We do not allow negative offsets.
+
+ local copy2self = false
+ if source==dest then copy2self = true end
+ if os.type=='unix' and
+ lfs.attributes(source,'ino')==lfs.attributes(dest,'ino') then
+ copy2self = true
+ end
+ if copy2self and not len and not offset and mode~='ab' then
+ return -- nothing to do
+ elseif copy2self then
+ errror('slice_file invoked with identical source and destination '..
+ source..' and non-simple copy')
+ end
+ -- in practice any_to_any already checks for this.
+ -- in addition, the main program makes a backup if infile==outfile
+
local sz = lfs.attributes(source, 'size')
if not offset then
offset = 0
@@ -444,8 +438,6 @@ function slice_file(source, dest, len, offset, mode)
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)
@@ -474,6 +466,12 @@ function move_or_copy(source, dest)
warn('Removing old '..dest)
os.remove(dest) -- in case of failure, go ahead anyway
end
+ -- Windows: try first renaming in-place before moving to the right directory
+ if os.type == 'windows' then
+ if os.rename(source, source..'.renamed') then
+ source = source..'.renamed'
+ end
+ end
if not os.rename(source, dest) then
slice_file(source, dest) -- bails out on failure
local ok, err_mess = os.remove(source)
@@ -498,10 +496,8 @@ function mktemp(ext)
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)
@@ -535,11 +531,9 @@ function mktemp(ext)
end
function waitasec()
- -- stupid windows file locking
- if os.type=='windows' and (tonumber(os.uname().version) or 0)>=6 then
+ -- stupid windows file locking; assume vista or later
+ if os.type=='windows' then
os.execute('timeout /t 1 /nobreak >nul')
- elseif os.type=='windows' then
- os.execute('ping -n 1 localhost >NUL')
-- else do nothing
end
-- error checking pointless
@@ -573,9 +567,9 @@ function cleantemp()
end
end
--- epsdevice -----------------------
+-- gs_epsdevice -----------------------
-function epsdevice()
+function gs_epsdevice()
local gh = io.popen(gs_prog..' -help')
local s = gh:read("*a")
gh:close()
@@ -599,17 +593,12 @@ function write_settings (file)
end
else -- stdout to be captured by epspdftk
f = io.output()
- if os.type=='windows' and not auto_psp then
- f:write('psp_config = yes', 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)
+ if descriptions[k] and file then
+ f:write(eol, '# ', descriptions[k], eol)
end
+ f:write(k, ' = ', tostring(v), eol)
end
if file then
f:close()
@@ -634,15 +623,12 @@ function read_settings(file)
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.
+ -- possibly followed by line-ending characters i.e. in lines
local k, v, vl, vnum
for l in string.gmatch(contents, '[^\r\n]+[\r\n]*') do
l = string.match(l,'[^\r\n]*')
@@ -660,31 +646,6 @@ function read_settings(file)
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)
- 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
@@ -707,13 +668,27 @@ function read_settings(file)
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
+function version_added()
+ for _, v in pairs(pdf_options) do
+ if string.find(v, '-dCompatibilityLevel') then
+ return true
+ end
+ end
+ return false
+end
+
+function maybe_add_version_parameter()
+ if options.type=='pdf' and not options.bbox and settings.pdf_version and
+ settings.pdf_version~='default' and not version_added() then
+ table.insert(pdf_options, '-dCompatibilityLevel#'..settings.pdf_version)
+ end
+end
+
-- gui: reading and writing settings -----------
function gui(action)
@@ -733,12 +708,19 @@ end
-- boundingboxes ---------------------------------------------------
--- [HR]Bb.coords names now same as those of epdf PDFRectangle
+-- [HR]Bb.coords names same as those of epdf PDFRectangle
+-- but the new pdfe simply uses an array
Bb = {}
Bb.coords = {'x1', 'y1', 'x2', 'y2'}
function Bb:from_rect(r)
+ -- also handle the case that r is a 4-element array:
+ if not r.x1 then r.x1 = r[1] end
+ if not r.y1 then r.y1 = r[2] end
+ if not r.x2 then r.x2 = r[3] end
+ if not r.y2 then r.y2 = r[4] end
+
for _,k in ipairs(self.coords) do
if not r[k] or type(r[k])~='number' then
errror('from_rect called with illegal parameters')
@@ -778,53 +760,6 @@ function Bb:from_comment(s)
return Bb:from_rect(b)
end
---[[ some no-longer-used bbox functions
-
-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
-
--- gs handles this now
--- 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
@@ -842,6 +777,12 @@ HRBb = {}
setmetatable(HRBb, {__index=Bb})
function HRBb:from_rect(r)
+ -- also handle the case that r is a 4-element array:
+ if not r.x1 then r.x1 = r[1] end
+ if not r.y1 then r.y1 = r[2] end
+ if not r.x2 then r.x2 = r[3] end
+ if not r.y2 then r.y2 = r[4] end
+
for _,k in ipairs(self.coords) do
if not r[k] or type(r[k])~='number' then
errror('from_rect called with illegal parameters')
@@ -865,7 +806,6 @@ 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)
@@ -878,6 +818,10 @@ function HRBb:from_comment(s)
return HRBb:from_rect(b)
end
+function HRBb:nonnegative ()
+ return self.x1>=0 and self.y1>=0
+end
+
function HRBb:comment()
return string.format('%%%%HiResBoundingBox: %f %f %f %f',
self.x1, self.y1, self.x2, self.y2)
@@ -885,10 +829,6 @@ 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()
@@ -910,10 +850,10 @@ end
-- manipulating eps/ps/pdf files -----------------------------------
-function identify(path)
- local f = io.open(path, 'rb')
+function identify()
+ local f = io.open(infile, 'rb')
if not f then
- errror('Failure to open '..path..' for identification')
+ errror('Failure to open '..infile..' for identification')
end
local filestart= f:read(23)
f:close()
@@ -925,7 +865,7 @@ function identify(path)
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
+ if string.sub(string.lower(infile), -1-string.len(p),-1) == p then
return 'eps'
else
return 'ps'
@@ -940,45 +880,52 @@ function identify(path)
end -- identify
function pdf_props(path)
- local pdfdoc = epdf.open(path)
- if not pdfdoc then
- errror('epdf.open failed on '..path)
+ local pdfdoc, pgs, maver, miver
+ if pdfe then
+ pdfdoc = pdfe.open(path)
+ if pdfdoc then
+ pgs = pdfe.getnofpages(pdfdoc)
+ maver, miver = pdfe.getversion(pdfdoc)
+ end
+ if not (pdfdoc and pgs and maver and miver) then
+ errror('pdfe failed to get information about '..path)
+ end
+ pdfe.close(pdfdoc)
+ else
+ local cat
+ pdfdoc = epdf.open(path)
+ if pdfdoc then
+ cat = pdfdoc:getCatalog()
+ if cat then
+ pgs = cat:getNumPages()
+ end
+ maver = pdfdoc:getPDFMajorVersion()
+ miver = pdfdoc:getPDFMinorVersion()
+ end
+ if not (pdfdoc and pgs and maver and miver) then
+ errror('epdf failed to get information about '..path)
+ end
+ -- epdf.close(pdfdoc)
end
-- if os.type=='windows' then waitasec() 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
+ return pgs, miver, maver
+end -- pdf_props
-function info (infile)
- local intype = identify(infile)
+function info()
+ local intype = identify()
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)
+ local pgs, miver, maver = pdf_props(infile)
print(infile..' has type pdf, version '..tostring(maver)..
- '.'..tostring(miver)..' and has '..tostring(pg)..' pages.')
+ '.'..tostring(miver)..' and has '..tostring(pgs)..' pages.')
end
os.exit()
end
@@ -990,7 +937,6 @@ PsPdf = {}
-- creators
function PsPdf:new(ext)
- -- dbg('PsPdf:new')
local psp = {}
setmetatable(psp, {__index = self})
-- assign temp file
@@ -1013,7 +959,6 @@ function PsPdf:new(ext)
end -- PsPdf:new
function PsPdf:from_path(path)
- -- dbg('PsPdf:from_path')
local psp = {}
setmetatable(psp, {__index = self})
psp.path = path
@@ -1031,23 +976,40 @@ function PsPdf:from_path(path)
end
psp.bb = false
psp.hrbb = false
- -- calculate when needed
+ -- only calculate when needed
return psp
end -- PsPdf:from_path
---[===[ getting boundingbox property from file itself --------------
+-- do we need to downgrade the pdf to a lower version?
+-- consider [e]ps lower than any pdf version
+function PsPdf:to_downgrade()
+ if self.type~='pdf' then
+ return false
+ elseif options.type~='pdf' then
+ return true
+ elseif settings.pdf_version=='default' then
+ return false
+ elseif settings.pdf_version~='default' and
+ self.maver+0.1*self.miver-0.001 > tonumber(settings.pdf_version) then
+ -- -0.001: exact binary representation of pdf_version not guaranteed
+ return true
+ else
+ return false
+ end
+end
+
+--[[ getting boundingbox property from file itself --------------
-find_bb_simple: use only for eps PsPdf objects we generated
+get_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')
+function PsPdf:get_bb_simple()
if self.type~='eps' then
- errror('find_bb_simple called with non-eps file '..self.path)
+ errror('get_bb_simple called with non-eps file '..self.path)
end
self.bb = false
self.hrbb = false
@@ -1074,9 +1036,7 @@ function PsPdf:find_bb_simple()
return self -- no real need for a return value
end
-function PsPdf:bb_from_gs(pg)
-
- -- dbg('bb_from_gs '..pg)
+function PsPdf:bb_from_gs()
if self.type=='ps' then
errror('bb_from_gs called with ps file '..self.path)
@@ -1086,19 +1046,25 @@ function PsPdf:bb_from_gs(pg)
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')
+ ' which has some negative boundingbox coordinates')
+ -- any_to_any should guard against such an invocation
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 = gs_prog .. ' ' .. table.concat(gs_options,' ')
+ -- a somewhat low resolution parameter may help gs
+ -- deal with eps files with large coordinates
+ -- but this may impact the accuracy of the HRBb
+ local cmdline
+ if self.type=='eps' and (self.bb.x2 > 850 or self.bb.y2 > 850) then
+ cmdline = gs_prog .. ' -r300 ' .. table.concat(gs_options,' ')
+ else
+ cmdline = gs_prog .. ' ' .. table.concat(gs_options,' ')
+ end
if self.type=='pdf' then
- if not pg then pg=1 end
+ pg = options.page or 1
cmdline = cmdline .. ' -dFirstPage#' .. tostring(pg) ..
' -dLastPage#' .. tostring(pg)
end
@@ -1145,7 +1111,8 @@ end
function PsPdf:eps_clean()
-- return a PsPdf object referring to a new file
- -- without a preview header and with boundingbox(es) in the header
+ -- without a preview header and with boundingbox(es) in the header.
+ -- return a new file even if no changes were needed.
local function bytes2num (s, i)
-- convert substring s[i..i+3] to a number.
@@ -1158,7 +1125,6 @@ function PsPdf:eps_clean()
-- + 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
@@ -1182,14 +1148,12 @@ function PsPdf:eps_clean()
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.
@@ -1218,9 +1182,7 @@ function PsPdf:eps_clean()
-- 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
@@ -1237,18 +1199,14 @@ function PsPdf:eps_clean()
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
@@ -1281,14 +1239,11 @@ function PsPdf:eps_clean()
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
@@ -1318,7 +1273,6 @@ function PsPdf:eps_clean()
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] = ''
@@ -1340,19 +1294,14 @@ end -- eps_clean
function PsPdf:eps_crop()
- -- not a proper conversion, although
- -- we use the Ghostscript bbox device for a tight boundingbox.
+ -- conversion is not done by an external program, although
+ -- we invoke Ghostscript with a 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')
@@ -1377,9 +1326,7 @@ function PsPdf:eps_crop()
-- 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
@@ -1421,17 +1368,21 @@ function PsPdf:eps_crop()
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, pdftops or texlua
+--[[
+
+most of these conversions involve a single invocation of gs, pdftops or texlua
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 make sure to do the tight boundingbox before a file format downgrade:
+rasterization of the page or graphic frustrates boundingbox calculation.
+
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,
@@ -1439,66 +1390,82 @@ 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: multiple pages?
--- (means additional parameter checking)
+-- Converting from pdf to pdf using luatex; no grayscaling here
--- Converting from pdf to pdf using luatex; no grayscaling
+function PsPdf:getpgbox()
+ if options.page and options.page > self.pages then
+ errror('PsPdf:getpgbox called with non-existent page '.. options.page)
+ end
+ local pg = options.page or 1
+ local bb, hrbb, pgbox, pdfdoc, ppage
+ if pdfe then
+ pdfdoc = pdfe.open(self.path)
+ if pdfdoc then
+ ppage = pdfe.getpage(pdfdoc, pg)
+ if not ppage then errror('did not get page') end
+ if ppage then
+ pgbox = pdfe.getbox(ppage, 'MediaBox')
+ if not pgbox then
+ pgbox = pdfe.getbox(ppage, 'CropBox')
+ if not pgbox then
+ pgbox = pdfe.getbox(ppage, 'TrimBox')
+ if not pgbox then
+ errror('No box acquired')
+ end
+ end
+ end
+ end
+ end
+ pdfe.close(pdfdoc)
+ else
+ local cat
+ pdfdoc = epdf.open(self.path)
+ if pdfdoc then
+ cat = pdfdoc:getCatalog()
+ if cat then
+ ppage = cat:getPage(pg)
+ if ppage then
+ pgbox = ppage:getMediaBox()
+ if not pgbox then
+ pgbox = ppage:getCropBox()
+ if not pgbox then
+ pgbox = ppage:getTrimBox()
+ end
+ end
+ end
+ end
+ end
+ end
+ -- normalization and further checks, including for non-nil,
+ -- by HRBb:from_rect, which errors out on failures
+ if pgbox then
+ pgbox = HRBb:from_rect(pgbox)
+ -- this also converts numeric array indices to x1 ... y2 if necessary
+ else
+ errror('Cannot get page box from '..self.path..' page '..pg)
+ end
+ return pgbox
+end -- PsPdf:getpgbox
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
- -- dbg('PsPdf:pdf_crop '..self.path)
- if not (options.bbox or options.page) then
- return self
- end
+ -- options to be fulfilled: page, boundingbox if possible
+ -- embeds the pdf with boundingbox parameters into a new (lua)tex document
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, pgbox, pgboxname
- do
- local pdoc, pcat, ppage
- -- we already have the pages, miver and maver properties
- -- but need properties of the desired page as well
- pdoc = epdf.open(self.path)
- if not pdoc then
- errror('Epdf: cannot open '..self.path)
- end
- -- dbg('about to get catalog')
- pcat = pdoc:getCatalog()
- if not pcat then
- errror('Cannot open catalog of '..self.path)
- end
- -- dbg('got catalog')
- ppage = pcat:getPage(pg)
- if not ppage then
- errror('Epdf: cannot open page object '..tostring(pg)..' of '..self.path)
- end
- pgboxname = 'trimbox'
- pgbox = ppage:getTrimBox()
- if not pgbox then
- pgboxname = 'cropbox'
- pgbox = ppage:getCropBox()
- end
- if not pgbox then
- pgboxname = 'mediabox'
- pgbox = ppage:getMediaBox()
- end
- -- normalization and further checks, including for non-nil,
- -- by Bb:from_rect, which errors out on failures
- pgbox = HRBb:from_rect(pgbox)
- end
- if options.bbox then
- bb, hrbb = self:bb_from_gs(pg)
+ local bb, hrbb, pgbox
+ pgbox = self:getpgbox()
+ if options.bbox and pgbox:nonnegative() then
+ bb, hrbb = self:bb_from_gs()
+ options.bbox = false
else
hrbb = pgbox
end
- -- luatex on searchpath
+ -- luatex is on searchpath
local luatex_prog = 'luatex'
-- write TeX file which includes cropped pdf page
@@ -1552,12 +1519,10 @@ function PsPdf:pdf_crop()
pieces[6] = [[}%
\pdfrefximage\pdflastximage
}%
- \pdfhorigin=-#2bp\relax
+ \pdfhorigin=#2bp\relax
\pdfvorigin=#3bp\relax
\pagewidth=#4bp\relax
- \advance\pagewidth by -#2bp\relax
\pageheight=#5bp\relax
- \advance\pageheight by -#3bp\relax
\ht0=\pageheight
\shipout\box0\relax
}
@@ -1567,7 +1532,7 @@ function PsPdf:pdf_crop()
\csname @@end\endcsname
\end
]],
- options.page or 1, hrbb.x1, hrbb.y1, hrbb.x2, hrbb.y2)
+ options.page or 1, -hrbb.x1, hrbb.y1, hrbb.x2-hrbb.x1, hrbb.y2-hrbb.y1)
local textemp = mktemp('tex') -- this also took care of pdf:
local pdftemp = string.gsub(textemp, 'tex$', 'pdf')
@@ -1577,18 +1542,20 @@ function PsPdf:pdf_crop()
f:write(table.concat(pieces, ''))
f:close()
local cmd, res, psp
- if os.type=='unix' then
+ -- if os.type=='unix' then
cmd = {luatex_prog, '--safer', '--no-shell-escape', textemp}
log_cmd(cmd)
res = os.spawn(cmd)
- else
- cmd = luatex_prog..' --safer --no-shell-escape '..textemp
- log_cmd({cmd})
- -- os.execute('timeout /t 1 /nobreak >nul')
- res = os.execute(cmd)
- end
- if res and res==0 and lfs.attributes(pdftemp, 'size')>0 then
+ -- else
+ -- cmd = luatex_prog..' --safer --no-shell-escape '..textemp
+ -- log_cmd({cmd})
+ -- -- os.execute('timeout /t 1 /nobreak >nul')
+ -- res = os.execute(cmd)
+ -- end
+ if res and res==0 and lfs.attributes(pdftemp, 'size')>0 then
psp = PsPdf:from_path(pdftemp)
+ options.bbox = false
+ options.page = false
return psp
else
errror('pdf_crop failed on '..self.path)
@@ -1598,33 +1565,26 @@ end
function PsPdf:eps_to_pdf()
-- option to be fulfilled: gray
- -- set target and pdf version if applicable
- -- dbg('PsPdf:eps_to_pdf '..self.path)
+ -- set target and maybe pdf version if applicable
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
+ maybe_add_version_parameter()
cmd = tab_combine({{gs_prog}, 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,' '))
+ table.insert(cmd, '-dEPSCrop') -- uses existing hires bb
local psp = PsPdf:new('pdf')
table.insert(cmd, '-sOutputFile#'..psp.path)
- -- dbg(table.concat(cmd,' '))
- cmd = tab_combine({cmd, pdf_tail_options, {self.path}})
- -- dbg(table.concat(cmd,' '))
+ cmd = tab_combine({cmd, pdf_tail_options})
+ 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)
+ options.gray = false
return psp
else
errror('eps_to_pdf failed on '..self.path)
@@ -1632,13 +1592,14 @@ function PsPdf:eps_to_pdf()
end -- eps_to_pdf
-- Converting from pdf to pdf with grayscaling and/or page selection
+-- or just eliminating negative boundingbox
function PsPdf:pdf_to_pdf()
- -- option to be fulfilled: gray and optionally page.
+ -- option to be fulfilled: gray, optionally page
+ -- side effect: makes bbox non-negative.
-- do not call this just for page selection because
-- pdf_crop can do this in a less invasive manner
- -- dbg('PsPdf:pdf_to_pdf '..self.path)
if self.type~='pdf' then
errror('PsPdf:pdf_to_pdf called for non-pdf file '.. self.path)
end
@@ -1647,28 +1608,24 @@ function PsPdf:pdf_to_pdf()
errror('PsPdf:pdf_to_pdf called with non-existent page '.. options.page)
end
cmd = tab_combine({{gs_prog}, 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
+ maybe_add_version_parameter()
local psp = PsPdf:new('pdf')
table.insert(cmd, '-sOutputFile#'..psp.path)
cmd = tab_combine({cmd, pdf_tail_options})
- -- 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)
+ options.gray = false
+ options.page = false
return psp
else
errror('pdf_to_pdf failed on '..self.path)
@@ -1677,34 +1634,19 @@ end -- pdf_to_pdf
function PsPdf:pdf_to_eps()
- -- options to be fulfilled: bbox and page
- -- dbg(tostring(settings.pdftops_prog))
- -- dbg('pdf_to_eps '..self.path)
+ -- options to be fulfilled: page
local psp = PsPdf:new('eps')
local cmd, res
- local page = false
- if self.pages > 1 then
- if options.page then
- page = options.page
- if page > self.pages then
- errror('PsPdf:pdf_to_eps called with non-existant page '..
- tostring(page))
- end
- else
- page = 1
- end
- page = tostring(page)
- end -- self.pages > 1
+ -- any_to_any already checked the validity and relevance of options.page
if pdftops then
- if page then
+ if options.page then
cmd = tab_combine({{pdftops}, ps_options,
- {'-f', page, '-l', page,
+ {'-f', options.page, '-l', options.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,
@@ -1718,6 +1660,7 @@ function PsPdf:pdf_to_eps()
end
if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
psp.pages = 1
+ options.page = false
else
errror('pdf_to_eps failed on '..self.path)
end
@@ -1766,37 +1709,37 @@ function PsPdf:pdf_to_eps()
if not fout then errror('Cannot write new file '.. newfile) end
fout:write(table.concat(pre_lines))
fout:close()
- -- dbg('fixing '..psp.path..' to '..newfile)
slice_file(psp.path, newfile,
lfs.attributes(psp.path,'size') - offset, offset, 'ab')
psp.path = newfile
end -- needs_fixing
else -- use ghostscript
- local epsdev = epsdevice()
+ local epsdev = gs_epsdevice()
if not epsdev then
errror('Conversion to eps not supported by this ghostscript')
end
cmd = tab_combine({{gs_prog}, gs_options,
{'-sDEVICE#'..epsdev, '-dHaveTrueTypes=true', '-dLanguageLevel#3'}})
- -- the restrictions on eps files are apparently
- -- incompatible with grayscaling
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ end
if options.page then
table.insert(cmd, '-dFirstPage='..options.page)
table.insert(cmd, '-dLastPage='..options.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
+ options.page = false
+ options.gray = false
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
+ psp:get_bb_simple()
return psp
end -- pdf_to_eps
@@ -1804,7 +1747,6 @@ end -- pdf_to_eps
function PsPdf:ps_to_pdf()
-- options to be fulfilled: gray
- -- dbg('PsPdf:ps_to_pdf '..self.path)
if self.type~='ps' then
errror('PsPdf:ps_to_pdf called for non-ps file '.. self.path)
end
@@ -1812,7 +1754,6 @@ function PsPdf:ps_to_pdf()
cmd = tab_combine({{gs_prog}, 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)
@@ -1822,6 +1763,7 @@ function PsPdf:ps_to_pdf()
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)
+ options.gray = false
return psp
else
errror('ps_to_pdf failed on '..self.path)
@@ -1832,24 +1774,14 @@ 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 '..self.path)
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
+ -- options.page checked by any_to_any
+ psp.pages = self.pages
local cmd, res
if pdftops then
cmd = tab_combine({{pdftops}, ps_options})
- if page then
- cmd = tab_combine({cmd, {'-f', page, '-l', page}})
+ if options.page then
+ cmd = tab_combine({cmd, {'-f', options.page, '-l', options.page}})
end
cmd = tab_combine({cmd, {'-paper', 'match', self.path, psp.path}})
else -- use ghostscript
@@ -1857,17 +1789,14 @@ function PsPdf:pdf_to_ps()
{'-sDEVICE#ps2write', '-dHaveTrueTypes=true', '-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}})
+ if options.page then
+ cmd = tab_combine({cmd,
+ {'-dFirstPage#'..options.page, '-dLastPage#'..options.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 then
-- suppress console output of 'No display font for...' messages,
@@ -1880,79 +1809,123 @@ function PsPdf:pdf_to_ps()
res = os.spawn(cmd)
end
if res and res==0 and lfs.attributes(psp.path, 'size')>0 then
+ options.page = false
+ -- gs will have grayscaled if requested but pdftops will not
+ if not pdftops then options.gray = false end
return psp
else
errror('pdf_to_ps failed on '..self.path)
end
end -- PsPdf:pdf_to_ps
-function PsPdf:any_to_any()
+function PsPdf:ps_to_ps()
+ -- we do not accept a page option since we do not know
+ -- which pages are available.
+ -- if no gray option then there is no point in invoking this function.
+ local psp = PsPdf:new('ps')
+ local cmd = tab_combine({{gs_prog}, gs_options,
+ {'-sDEVICE#ps2write', '-dHaveTrueTypes=true', '-dLanguageLevel#3'}})
+ if options.gray then
+ cmd = tab_combine({cmd, gray_options})
+ end
+ table.insert(cmd, '-sOutputFile#'..psp.path)
+ 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
+ options.gray = false
+ return psp
+ else
+ errror('ps_to_ps failed on '..self.path)
+ end
+end -- PsPdf:ps_to_ps
+
+function any_to_any()
+
+ local psp = PsPdf:from_path(infile)
- -- weed out nonsense options
+ -- sanitize some options
- -- dbg('PsPdf:any_to_any '..self.path)
if options.type=='ps' then
options.bbox = false
- -- dbg('Ignoring bbox option for ps output')
end
- if options.bbox and not options.page then
+
+ if (options.bbox or options.type=='eps') and not options.page then
options.page = 1
- -- dbg('Selecting page 1 for bbox')
end
- if self.pages==1 then
+
+ if psp.type=='pdf' then
+ if options.page then
+ local pgs = pdf_props(infile)
+ if pgs<options.page then
+ errror(infile..' has only '..pgs..' page(s).')
+ elseif pgs==1 then
+ options.page = false
+ end
+ end
+ elseif psp.type=='eps' 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
+ -- psp.type=='ps': pages not yet known but maybe not needed
- -- `distiller' settings depend on whether final output is pdf
+ local do_something = false
+ if psp.type=='eps' then
+ do_something = true -- do at least cleanup
+ elseif psp.type~=options.type then
+ do_something = true
+ elseif options.page then
+ do_something = true
+ elseif options.bbox then
+ do_something = true
+ elseif options.gray then
+ do_something = true
+ elseif options.type=='pdf' then
+ if psp:to_downgrade() then
+ do_something = true
+ elseif settings.pdf_target and settings.pdf_target~=default then
+ do_something = true
+ end
+ end
+ if not do_something then
+ if infile~=outfile then
+ slice_file(infile, outfile)
+ end
+ return true
+ end
+
+ -- `distiller' settings
+ -- these depend on whether final output is pdf
-- '.setpdfwrite' is just some optimization option for ghostscript
if options.type=='pdf' then
- if settings.pdf_version~='default' then
- table.insert(pdf_options, '-dCompatibilityLevel#'..settings.pdf_version)
+ if settings.pdf_target and (settings.pdf_target~='default') then
+ table.insert(pdf_options, '-dPDFSETTINGS#/'..settings.pdf_target)
end
- -- below, consider adding <</NeverEmbed [/Times-Roman /TimesBold ...]>>
if settings.pdf_target=='screen' or settings.pdf_target=='ebook' then
- pdf_tailoptions = {'-c', '.setpdfwrite', '-f'}
+ pdf_tail_options = {'-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
- pdf_tailoptions = {
+ pdf_tail_options = {
'-c', '.setpdfwrite <</NeverEmbed [ ] >> setdistillerparams', '-f'}
- end
-
- 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
- else
- table.insert(pdf_options, '-dPDFSETTINGS#/default')
end
- --[[
+ --[[ Actual conversions
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
+ Cropping a pdf is best be done before converting to postscript or
+ a low (<1.4) pdf version.
- all calls to external programs work on temporary files
+ all invocations of external programs work on temporary files
in the then-current temporary directory, with a simple generated
- filename. So no need to quote names of input- and output filenames.
+ filename. So no need to quote names of input- or output filenames.
--]]
- local psp = self
local newfile
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.
+ -- As a side effect of eps_clean, the modified or unmodified source file
+ -- is copied to our temp subdirectory.
+ -- We always create a new file.
psp = psp:eps_clean()
if options.bbox and psp.bb:nonnegative() then
psp = psp:eps_crop()
@@ -1960,99 +1933,153 @@ function PsPdf:any_to_any()
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()
+ psp = psp:eps_to_pdf()
+ if options.bbox then
+ psp = psp:pdf_crop()
+ end
+ psp = psp:pdf_to_eps()
end
elseif options.type=='pdf' then
psp = psp:eps_to_pdf()
if options.bbox then
psp = psp:pdf_crop()
end
+ if psp:to_downgrade() then
+ maybe_add_version_parameter()
+ psp = psp:pdf_to_pdf()
+ end
elseif options.type=='ps' then
+ -- often, the eps file is fine as a postscript file.
+ -- however, it may lack a showpage operator, or have a weird
+ -- boundingbox. converting back-and-forth to pdf solves both problems.
+ -- eps_to_pdf will take care of grayscaling
+ -- a tight boundingbox option is not supported for ps output.
psp = psp:eps_to_pdf():pdf_to_ps()
end
- return psp
+ slice_file(psp.path, outfile)
+ return true
elseif psp.type=='ps' then
+
-- preliminary:
-- copy infile to a file in the temp directory, needed for gs -dSAFER
newfile = mktemp(psp.type)
slice_file(psp.path, newfile)
- -- dbg(psp.path..' copied to '..newfile..' in '..lfs.currentdir())
psp.path = newfile
+ if options.type=='ps' and not options.page then
+ psp = psp:ps_to_ps()
+ slice_file(psp.path, outfile)
+ return true
+ end
+
+ -- remaining options require initial conversion to pdf
+ psp = psp:ps_to_pdf()
+ -- AFAIK, all high-level ps constructs are covered by any pdf version
+ -- so this option should not cause unnecessary loss of structure
+ maybe_add_version_parameter()
+
+ if options.page and options.page>1 then
+ -- could not check page option before
+ local pgs = pdf_props(psp.path)
+ if pgs<options.page then
+ errror(infile..' has only '..pgs..' page(s).')
+ elseif pgs==1 then
+ options.page = false
+ end
+ end
+
-- actual conversion
if options.type=='eps' then
- return psp:ps_to_pdf():pdf_to_eps()
+ if options.bbox then
+ psp = psp:pdf_crop()
+ end
+ psp = psp: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()
+ psp = psp:pdf_crop()
+ -- else no action needed
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
+ psp = psp:pdf_to_ps()
end -- pdf => ps
+ slice_file(psp.path, outfile)
+ return true
elseif psp.type=='pdf' then
+
+ if options.type=='pdf' and settings.pdf_target=='default' and
+ not options.gray and not options.bbox and not options.page and
+ not psp:to_downgrade() then
+ if infile~=outfile then
+ slice_file(infile, outfile)
+ end
+ return true
+ end
+
-- preliminary:
- -- copy infile to a file in the temp directory, for gs -dSAFER
+ -- copy infile to a file in the temp directory, needed for gs -dSAFER
newfile = mktemp(psp.type)
slice_file(psp.path, newfile)
- -- dbg(psp.path..' copied to '..newfile..' in '..lfs.currentdir())
- psp.path = newfile
+ psp = PsPdf:from_path(newfile)
+ local pgbox
-- 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()
+ if options.bbox then
+ pgbox = psp:getpgbox() -- page n. available from options table
+ if not pgbox:nonnegative() or options.gray then
+ -- fix in extra pdf-to-pdf step
+ psp = psp:pdf_to_pdf()
+ end
+ -- we want to calculate a tight boundingbox before conversion to eps,
+ -- because this conversion may cause rasterization and baffle
+ -- gs' boundingbox calculations
+ psp = psp:pdf_crop()
+ elseif options.gray and pdftops then
+ psp = psp:pdf_to_pdf()
end
+ psp = psp:pdf_to_eps()
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
+ -- but not of gray and not of some pdf options
+ if options.bbox then
+ pgbox = psp:getpgbox()
+ if not pgbox:nonnegative() then
+ -- pdf_to_pdf fixes negative bbox parameters
+ -- and also takes care of page and gray
psp = psp:pdf_to_pdf()
end
- if need_crop or (psp.pages>1 and options.page) then
- psp = psp:pdf_crop()
- end
- return psp
+ psp = psp:pdf_crop()
+ end
+ need_gs = false
+ if psp:to_downgrade() then
+ need_gs = true
+ maybe_add_version_parameter()
+ end
+ if settings.pdf_target~='default' then
+ need_gs = true
+ end
+ if options.gray then
+ need_gs = true
+ end
+ if (not need_gs) and options.page then
+ psp = psp:pdf_crop()
+ -- less invasive that page selection by gs
+ end
+ if need_gs then
+ psp = psp:pdf_to_pdf() -- will handle page selection too
end
elseif options.type=='ps' then
if options.gray and pdftops then
- return psp:pdf_to_pdf():pdf_to_ps()
+ psp = psp:pdf_to_pdf():pdf_to_ps()
else
- return psp:pdf_to_ps()
+ psp = psp:pdf_to_ps()
end
end -- pdf => ps
- end -- psp.type=='ps'|'pdf'
+ slice_file(psp.path, outfile)
+ return true
+ end
end -- any_to_any
-- system-dependent initialization -----------------------------------
@@ -2067,11 +2094,10 @@ if os.type == 'windows' then cwd = string.gsub(cwd, '\\', '/') end
-- prepend (lua)tex directory to searchpath, if not already there
maybe_add_path(os.selfdir, false)
--- Windows: miktex, TL or neither. is_miktex, is_tl_w32, auto_psp
+-- Windows: miktex, TL or neither. is_miktex, is_tl_w32
-- no support yet for separate ghostscript
is_miktex = false
is_tl_w32 = false
-auto_psp = true -- only false for old miktex without included pdftops
if os.type == 'windows' then
if string.find (string.lower(kpse.version()), 'miktex') then
@@ -2096,14 +2122,9 @@ if os.type == 'windows' then
'/release-texlive.txt; TeX installation not supported.', 0)
end -- if isfile
end -- if not miktex
- -- recent miktex and any windows tl include pdftops
- if not lfs.isfile(string.gsub(os.selfdir, '\\', '/') .. '/pdftops.exe') then
- auto_psp = false
- end
end -- if windows
-- without Ghostscript we are dead in the water.
--- TL/w32: add to searchpath
gs_prog = false
do
local rt=''
@@ -2119,11 +2140,11 @@ do
elseif is_tl_w32 then
-- windows/TeX Live
-- grandparent of texlua.exe directory .. ...
- rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '')
- ..'/tlpkg/tlgs'
- maybe_add_path(rt..'/bin', false)
- gs_prog = 'gswin32c.exe'
- --[[ problems with (at least) grayscaling
+ -- rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '')
+ -- ..'/tlpkg/tlgs'
+ -- maybe_add_path(rt..'/bin', false)
+ -- -- gs_prog = 'gswin32c.exe'
+ -- ---[[ problems with (at least) grayscaling
gs_prog = 'rungs.exe'
--]]
else
@@ -2134,11 +2155,10 @@ end -- do
-- directory for configuration and log
epsdir = ''
if os.type == 'windows' then
- epsdir = fw(ep_shortname(os.getenv('APPDATA'))) .. '/epspdf'
+ epsdir = fw(os.getenv('APPDATA')) .. '/epspdf'
else
epsdir = os.getenv('HOME')..'/.epspdf'
end
--- dbg('epsdir: '..epsdir)
rcfile = epsdir .. '/config'
logfile = epsdir .. '/epspdf.log'
@@ -2166,26 +2186,29 @@ end -- if lfs...logfile
write_log('epspdf '..table.concat(arg, ' '))
---[[ some debug output
-
-dbg ('os is ' .. os.type .. ' and ' .. os.name)
-dbg ('texlua in ' .. os.selfdir)
-dbg('Ghostscript: ' .. gs_prog)
---]]
-
--[[
settings, initial values
-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.
+priority, from low to high:
+
+- built-in defaults
+- settings read from and written to the configuration file
+- command-line options, defined in the opts table
+
+The options- and settings tables are initialized from built-in
+defaults. We go for false rather than undefined, because this
+results in an actual settings- or options entry.
+
+Command-line options are copied to either options or settings.
We ignore illegal settings in the config file.
--]]
pdf_targets = {'screen', 'ebook', 'printer', 'prepress', 'default'}
-pdf_versions = {'1.2', '1.3', '1.4', 'default'}
+pdf_versions = {'1.2', '1.3', '1.4', '1.5', '1.6', '1.7', 'default'}
+-- ghostscript will substitute higher versions with
+-- the highest-supported version
settings = {}
descriptions = {}
@@ -2196,30 +2219,12 @@ 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
-descriptions.pdftops_prog =
- 'Full path to pdftops.exe (not used if TeX bin dir contains pdftops)'
-
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.
+-- They will not be used at all under macos or windows.
settings.ps_viewer = false
descriptions.ps_viewer =
@@ -2227,13 +2232,13 @@ descriptions.ps_viewer =
settings.pdf_viewer = false
descriptions.pdf_viewer =
- 'Epspdftk: viewer for pdf files; not used on Windows or OS X'
+ 'Epspdftk: viewer for pdf files; not used on Windows or Mac OS'
-- 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')), '\\', '/')
+ string.gsub(os.getenv('USERPROFILE'), '\\', '/')
else
settings.default_dir = os.getenv('HOME')
end
@@ -2257,6 +2262,7 @@ options.type = false -- implied via output filename on command line
-- 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.
+-- -P- : do not look first in current directory
gs_options = {'-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER'}
@@ -2273,25 +2279,18 @@ pdftops = false
-- gets a value only if we are going to use pdftops
ps_options = {'-level3'}
--- may add custom options later
-- `main program' inside scope-creating block ----------------------
do -- main program
- local infile = false
local in_dir = false -- directory of infile
- local outfile = false
local out_dir = false -- directory of outfile
- -- dbg('\nSettings are:\n')
- for k,v in pairs(settings) do dbg(k .. ' = ' .. tostring(v)) end
-
do -- Handle settings and command-line inside nested scope --------------
read_settings(rcfile)
- -- dbg('Defining cmdline options')
local opts = {}
opts.page = {
@@ -2345,16 +2344,6 @@ do -- main program
help = descriptions.pdf_version
}
- if os.type=='windows' and not auto_psp 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'},
@@ -2407,22 +2396,24 @@ do -- main program
-- a couple of functions only available during command-line parsing
local function show_version ()
- print('Epspdf version '..ep_version..'\nCopyright (c) '
- ..ep_copyright..' Siep Kroonenberg')
+ print('Epspdf version 0.6.4\nCopyright (c) 2006-2019 Siep Kroonenberg')
end
local function help (mess) -- requires opts array
- if mess then print(mess..eol) end
+ if mess then
+ print(mess..eol)
+ end
show_version()
-- below, string.gsub unindents its long-string parameter.
-- string.format removes the second return value of string.gsub.
- print(string.format('%s', string.gsub([[
+ print( -- string.format('%s',
+ string.gsub([[
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
- ]], '([\r\n]+) ', '%1')))
+ ]], '([\r\n]+) ', '%1')) --)
-- need to enforce an ordering, otherwise we could have used pairs(opts)
-- omitted below: no-op options
@@ -2431,31 +2422,28 @@ do -- main program
local intent_sp = string.rep(' ', indent_n)
local indent_fmt = '%-' .. tostring(indent_n) .. 's'
for _, o in ipairs({'page', 'gray', 'bbox', 'pdf_target', 'pdf_version',
- 'pdftops_prog', 'use_pdftops', 'save', 'info', 'debug',
- 'version', 'help'}) do
+ 'use_pdftops', 'save', 'info', 'debug', 'version', 'help'}) do
local v = opts[o]
- if v~='pdftops_prog' or not auto_psp then
- if v and v.help then
- local synt = join(v.forms, ', ')
- if v.type ~= 'boolean' then synt = synt .. ' ' .. v.placeholder 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) .. v.help)
+ print(string.format(indent_fmt, synt) .. neghelp)
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
+ print(intent_sp .. neghelp)
end
end
- end -- ~=pdftops_prog or not auto_psp
+ end
end -- for
if mess then os.exit(1) else os.exit() end
end -- help
@@ -2471,7 +2459,6 @@ do -- main program
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
@@ -2480,7 +2467,6 @@ do -- main program
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
@@ -2506,29 +2492,6 @@ do -- main program
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.
-- at syntax error, abort via help function.
@@ -2594,34 +2557,14 @@ do -- main program
end
end
- -- pdftops program; pdftops has already been been initialized to false
-
- -- pdftops_prog as command-line option
- if os.type=='windows' and not auto_psp and
- opts.pdftops_prog.val then
- settings.pdftops_prog = is_prog(opts.pdftops_prog.val)
- end
-
- -- pdftops should be on the path. otherwise, make it so if possible.
- if os.type=='windows' and not auto_psp then
- if settings.use_pdftops then
- pdftops = is_prog(settings.pdftops_prog)
- if pdftops then
- -- strip path and modify searchpath, to avoid paths with spaces
- maybe_add_path(string.gsub(pdftops, '[\\/][^\\/]*$', ''), 'append')
- pdftops = string.gsub(settings.pdftops_prog, '^.*[\\/]', '')
- end
- end
- elseif os.type=='windows' then
- if settings.use_pdftops then
- pdftops = 'pdftops.exe'
- end
- else
- if settings.use_pdftops then
+ -- pdftops should be on the path
+ if settings.use_pdftops then
+ if os.type=='windows' then
+ pdftops = find_on_path('pdftops.exe')
+ else
pdftops = find_on_path('pdftops')
end
end
- -- dbg('Option handling; pdftops is '..tostring(pdftops))
-- other options
@@ -2675,58 +2618,25 @@ do -- main program
-- 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
+ info()
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
- --]]
+ end -- do (decoding command-line)
--[[
Once it becomes clear that real work needs to be done,
we shall create a temp directory.
- 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.
+ because of gs -dSAFER restrictions, infile must be in (a
+ subdirectory of) the directory of the output file, e.g. in the
+ temp directory. So we copy infile to the temp directory.
--]]
- source = io.open(infile)
- if not source then
- error(infile .. ' not readable')
+ if not lfs.isfile(infile) or lfs.attributes(infile, 'size')==0 then
+ errror(infile..' does not exist or is empty.')
end
- source:close()
- -- if options.debug then
- -- warn('in: '..infile..'\nout: '..outfile..'\n\ncwd: '..cwd)
- -- end
infile, in_dir = absolute_path(infile)
outfile, out_dir = absolute_path(outfile)
if not out_dir then
@@ -2746,58 +2656,36 @@ do -- main program
tempdir = os.tmpdir()
if not tempdir then
errror('Cannot create directory for temporary files')
- else
- -- dbg('temp directory '..tempdir)
end
lfs.chdir(tempdir)
+ -- determine filetype from first few bytes of file
intype = identify(infile)
-- remaining cases: want a real conversion
if not intype then
- error(infile..' has an unsupported filetype')
+ errror(infile..' has an unsupported filetype')
end
- --sanity check on output filetype
+ -- valid output filetype?
options.type = string.match(outfile, '.*%.([^%.]+)$')
- if not options.type or (options.type~='ps'
- and options.type~='eps' and options.type~='pdf') then
+ 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==infile then
- local insave = infile .. '.luasave'
- move_or_copy(infile, insave)
- infile = insave
- end
-
- -- had some trouble under msw when removing outfile later so try it now
- if lfs.isfile(outfile) then
- os.remove(outfile)
- -- if removal fails but outfile is overwritable then no real problem
+ slice_file(infile, infile..'.backup')
+ -- copy, not move, since outfile may be infile if there was nothing to do
end
- local fout = io.open(outfile, 'wb')
- if not fout then
- errror('Output file '..outfile..' not writable; aborting')
- else
- fout:close()
+ result = any_to_any(infile, outfile)
+ if not lfs.isfile(outfile) or lfs.attributes(outfile, 'size')==0 then
+ errror('Failed to generate '..outfile)
end
- source = PsPdf:from_path(infile)
- dest = source:any_to_any()
- if not lfs.isfile(dest.path) then
- errror('Failed to generate '..dest.path)
- end
- write_log('Copying or moving '..dest.path..' to '..outfile)
- move_or_copy(dest.path, outfile)
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