diff options
author | Peter Breitenlohner <peb@mppmu.mpg.de> | 2013-02-08 08:13:09 +0000 |
---|---|---|
committer | Peter Breitenlohner <peb@mppmu.mpg.de> | 2013-02-08 08:13:09 +0000 |
commit | 00fb7e2695e55961f249a46965ce64506d297893 (patch) | |
tree | e14ebea76fb343b677682de631930cafaa0b3def /Build/source | |
parent | 16c40936344c67164a6ab224a3429780d4d62736 (diff) |
Epspdf updated to 0.6 (sync Master => Build)
git-svn-id: svn://tug.org/texlive/trunk@29057 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Build/source')
6 files changed, 3055 insertions, 1728 deletions
diff --git a/Build/source/texk/texlive/linked_scripts/Makefile.am b/Build/source/texk/texlive/linked_scripts/Makefile.am index 67266c915b9..cf80123c599 100644 --- a/Build/source/texk/texlive/linked_scripts/Makefile.am +++ b/Build/source/texk/texlive/linked_scripts/Makefile.am @@ -100,7 +100,7 @@ texmf_dist_other_scripts = \ dosepsbin/dosepsbin.pl \ dviasm/dviasm.py \ ebong/ebong.py \ - epspdf/epspdf.rb \ + epspdf/epspdf.tlu \ epspdf/epspdftk.tcl \ epstopdf/epstopdf.pl \ exceltex/exceltex \ diff --git a/Build/source/texk/texlive/linked_scripts/Makefile.in b/Build/source/texk/texlive/linked_scripts/Makefile.in index f2d61a3fd54..369af9b0e42 100644 --- a/Build/source/texk/texlive/linked_scripts/Makefile.in +++ b/Build/source/texk/texlive/linked_scripts/Makefile.in @@ -281,7 +281,7 @@ texmf_dist_other_scripts = \ dosepsbin/dosepsbin.pl \ dviasm/dviasm.py \ ebong/ebong.py \ - epspdf/epspdf.rb \ + epspdf/epspdf.tlu \ epspdf/epspdftk.tcl \ epstopdf/epstopdf.pl \ exceltex/exceltex \ diff --git a/Build/source/texk/texlive/linked_scripts/epspdf/epspdf.rb b/Build/source/texk/texlive/linked_scripts/epspdf/epspdf.rb deleted file mode 100755 index 47a0c1f65b9..00000000000 --- a/Build/source/texk/texlive/linked_scripts/epspdf/epspdf.rb +++ /dev/null @@ -1,1479 +0,0 @@ -#!/usr/bin/env ruby - -# epspdf conversion utility, main source - -##### -# Copyright (C) 2006, 2008, 2009, 2010, 2011 Siep Kroonenberg -# n dot s dot kroonenberg at rug dot 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! -##### - -# Operations on a PostScript- or pdf file. -# The focus is on converting between eps and pdf. - -# Code is organized as a set of single-step conversion methods plus an -# any-to-any conversion chaining them together. - -# `Target' is not a parameter; all conversions write to a new temp -# file. Conversions can be chained: object.conversion1( params -# ).conversion2( params ) ... The final temp file is moved or copied to -# the desired destination by the main program (which can be epspdf.rb -# itself). - -# The use of exception handling makes it unnecessary to inspect return -# values. - -########################################### - -# some initialization - -# add directory of this source to loadpath -# WARNING -# readlink apparently only works right from the directory of the symlink - -$SCRIPTDIR = File.dirname( File.expand_path( __FILE__ )) -if RUBY_PLATFORM !~ /win32|mingw/ and File.symlink?( __FILE__ ) - savedir = Dir.pwd - Dir.chdir( $SCRIPTDIR ) - # puts File.readlink( __FILE__ ) - $SCRIPTDIR = File.dirname( File.expand_path( File.readlink( __FILE__ ))) - Dir.chdir( savedir ) -end -$:.unshift( $SCRIPTDIR ) -# puts $: - -# turn on warnings -#$VERBOSE = 1 - -$from_gui = nil - - -########################################### - -# Error handling - -# internal error: method should not have been called -# under the current conditions -class EPCallError < StandardError; end - -# Can't get a valid boundingbox for an eps file -class EPBBError < StandardError; end - -# copying failed -class EPCopyError < StandardError; end - -# system call failed -class EPSystemError < StandardError; end - -########################################### - -# PostScript header file for grayscaling - -$GRAYHEAD = File.join( $SCRIPTDIR, "makegray.pro" ) - -########################################### - -# handle auto-detected and saved settings - -#require 'epspdfrc' -require 'epspdfrc' - -########################################### - -# Transient options. These will never be saved. -# We set default values here. - -$options = { - 'type' => nil, - 'page' => nil, - 'bbox' => false, - 'gray' => false, - 'gRAY' => false, - 'info' => false -} - -class << $options - - # create shortcut methods $options.x and $options.x= - # for reading and writing hash elements. - - $options.each_key { |k| - eval "def #{k} ; self[\'#{k}\'] ; end" - eval "def #{k}=(v) ; self[\'#{k}\']=v ; end" - } - -end # class - -########################################### - -def hash_prn( h ) - if h.empty? - s = "No parameters" + $/ - else - s = "Parameters were:" + $/ - h.each_key { |k| s = s + ' ' + k.to_s + ' => ' + h[ k ].to_s + $/ } - end - s -end - -########################################### - -require 'fileutils' -#include FileUtils::Verbose -include FileUtils - -########################################### - -# Mode strings for file i/o - -if ARCH == 'w32' - $W = 'wb' - $A = 'ab' - $R = 'rb' -else - $W = 'w' - $A = 'a' - $R = 'r' -end - -########################################### - -# copy a slice of a file; is there no such standard function? -# some eps files are very large; don't slurp the file at one go. -# Mode: can be set to append 'a' rather than write 'w'. - -def sliceFile( source, dest, len, offs, mode=$W ) - buffer = '' - File.open( source ) do |s| - s.binmode if ARCH == 'w32' - s.seek( offs, IO::SEEK_SET ) - if s.pos == offs - begin - File.open( dest, mode ) do |d| - d.binmode if ARCH == 'w32' - tocopy = len - while tocopy>0 and s.read( [ tocopy, 16384 ].min, buffer ) - tocopy = tocopy - d.write( buffer ) - end # while - end # do - rescue - fail EPCopyError, "Failure to copy to #{dest}" - end - end # if s.seek - end # |s| (automatic closing) - # return value true if anything has been copied - File.size?( dest ) # nil if zero-length -end # def - -# write our own file copy, to bypass bug in FileUtils.cp - -def ccp( source, dest ) - sliceFile( source, dest, File.size( source ), 0, $W ) -end - -########################################### - -# logging - -def write_log( s ) - if test( ?e, LOGFILE ) and File.size( LOGFILE ) > 100000 - rm( LOGFILE_OLD ) if test( ?e, LOGFILE_OLD ) - ccp( LOGFILE, LOGFILE_OLD ) - File.truncate( LOGFILE, 0 ) - end - File.open( LOGFILE, 'a' ) { |f| - f.print( "#{$$} #{Time.now.strftime('%Y/%m/%d %H:%M:%S')} #{s}\n" ) - } - puts( s ) if $from_gui - -end - -########################################### - -require 'tmpdir' - -#$DEBUG=1 - -# save filenames for cleanup at end -$tempfiles = [] - -def mktemp( ext ) - isdone = nil - (0..99).each do |i| - fname = Dir.tmpdir + File::SEPARATOR + \ - sprintf( '%d_%02d.%s', $$, i, ext ) # $$ is process id - next if test( ?e, fname ) - File.open( fname, 'w' ) do |f|; end # creates empty file - isdone = 1 - $tempfiles.unshift( fname ) - return fname - end # each do |i| - fail StandardError, "Cannot create temp file" unless isdone -end # def - -def cleantemp - write_log( "Cleaning tempfiles" + $/ + $tempfiles.join( $/ ) ) - $tempfiles.each{ |tf|; rm( tf ) } -end - -########################################### - -# identifying file type - -# EPS with preview starts with 0xC5D0D3C6 -# Encapsulated PostScript starts with %!PS-Adobe-n.n EPSF-n.n -# but %!PS-Adobe-n.n will also be identified as Encapsulated PostScript -# if the filename extension suggests it. -# PostScript starts with %! -# PDF starts with %PDF-version - -def identify( path ) - filestart = nil - File.open( path, $R ) do |f| - filestart = f.read( 23 ) - end # this syntax automatically closes the file at end of block - case filestart - when /^\xc5\xd0\xd3\xc6/ - 'epsPreview' - when /^%!PS-Adobe-\d\.\d EPSF-\d\.\d/ - 'eps' - when /^%!PS-Adobe-\d\.\d/ - ( path =~ /\.ep(i|s|si|sf)$/i ) ? 'eps' : 'ps' - when /^%!/ - 'ps' - when /^%PDF/ - 'pdf' - else - 'other' - end # case -end # def - -########################################### - -# Boundingboxes; first standard, then hires - -#changes: -#hires boundingboxes -#numbers may start with a plus-sign. DSC manual is ambiguous, -# PostScript manual allows it, but of course PS != DSC -#left-right and lower-upper need not be in natural order - -BB_PAT = /^\s*%%BoundingBox:\s*([-+]?\d+)((\s+[-+]?\d+){3})\s*$/ -BB_END = /^\s*%%BoundingBox:\s*\(\s*atend\s*\)\s*$/ - -class Bb - - attr_accessor :llx, :lly, :urx, :ury # strings - - def initialize( llx, lly, urx, ury ) - @llx, @lly, @urx, @ury = llx, lly, urx, ury - # guarantee valid syntax: - [@llx, @lly, @urx, @ury].each { |l| l = l.to_i.to_s } - @llx, @urx = @urx, @llx if @llx.to_i > @urx.to_i - @lly, @ury = @ury, @lly if @lly.to_i > @ury.to_i - end - - def Bb.from_comment( s ) - return nil unless s =~ BB_PAT - llx, lly, urx, ury = - s.sub( /^\s*%%BoundingBox:\s*/, '' ).split( /\s+/ ) - Bb.new( llx, lly, urx, ury ) - end - - def height - ( @ury.to_i - @lly.to_i ).to_s - end - - def width - ( @urx.to_i - @llx.to_i ).to_s - end - - def valid - @llx.to_i < @urx.to_i and @lly.to_i < @ury.to_i - end - - def non_negative - valid and @llx.to_i >= 0 and @lly.to_i >= 0 - end - - def copy - Bb.new( @llx, @lly, @urx, @ury ) - end - - def prn - "#{@llx} #{@lly} #{@urx} #{@ury}" - end - - def expand - i = ( $settings.bb_spread ).to_i - return if i <= 0 - @llx = ( [ 0, @llx.to_i - i ].max ).to_s - @lly = ( [ 0, @lly.to_i - i ].max ).to_s - @urx = ( @urx.to_i + i ).to_s - @ury = ( @ury.to_i + i ).to_s - end - - ################## - - # wrapper code for a boundingbox; - # moves lower left corner of eps to (0,0) - # and defines a page size identical to the eps width and height. - # The gsave in the wrapper code should be matched by - # a grestore at the end of the PostScript code. - # This grestore can be specified on the Ghostscript command-line. - - def wrapper - fail EPBBError, prn unless valid - fname = mktemp( 'ps' ) - File.open( fname, $W ) do |f| - f.binmode if ARCH == 'w32' - f.write( "%%BoundingBox: 0 0 #{width} #{height}\n" + - "<< /PageSize [#{width} #{height}] >>" + - " setpagedevice\n" + - "gsave #{(-(@llx.to_i)).to_s} #{(-(@lly.to_i)).to_s}" + - " translate\n" ) > 0 - end # open - return fname - end # wrapper - - ################## - - # convert boundingbox to boundingbox comment - - def comment - fail EPBBError, prn unless valid - "%%BoundingBox: #{@llx} #{@lly} #{@urx} #{@ury}" - end - -end # class Bb - -# [-+](\d+(\.\d*)?|\.\d+)([eE]\d+)? PostScript number -HRBB_PAT = /^\s*%%HiResBoundingBox:\s*[-+]?(\d+(\.\d*)?|\.\d+)([eE]\d+)?((\s[-+]?(\d+(\.\d*)?|\.\d+)([eE]\d+)?){3})\s*$/ -HRBB_END = /^\s*%%HiResBoundingBox:\s*\(\s*atend\s*\)\s*$/ - -class HRBb - - attr_accessor :llx, :lly, :urx, :ury - - def initialize( llx, lly, urx, ury ) - @llx, @lly, @urx, @ury = llx, lly, urx, ury - [@llx, @lly, @urx, @ury].each do |l| - if l =~ /\./ - # make floats conform to Ruby syntax: - # decimal dots must be padded with digits on either side - l.sub!( /^\./, '0.' ) - l.sub!( /\.(?!\d)/, '.0' ) # (?!\d): zero-width neg. lookahead - end - l = l.to_f.to_s - end - @llx, @urx = @urx, @llx if @llx.to_f > @urx.to_f - @lly, @ury = @ury, @lly if @lly.to_f > @ury.to_f - end - - def HRBb.from_hrcomment( s ) - return nil unless s =~ HRBB_PAT - llx, lly, urx, ury = - s.sub( /^\s*%%HiResBoundingBox:\s*/, '' ).split( /\s+/ ) - HRBb.new( llx, lly, urx, ury ) - end - - def height - ( @ury.to_f - @lly.to_f ).to_s - end - - def width - ( @urx.to_f - @llx.to_f ).to_s - end - - def valid - @llx.to_f < @urx.to_f and @lly.to_f < @ury.to_f - end - - def non_negative - valid and @llx.to_f >= 0 and @lly.to_f >= 0 - end - - def copy - HRBb.new( @llx, @lly, @urx, @ury ) - end - - def prn - "#{@llx} #{@lly} #{@urx} #{@ury}" - end - - ################## - - # wrapper code for a hires boundingbox; - # moves lower left corner of eps to (0,0) - # and defines a page size identical to the eps width and height. - # The gsave in the wrapper code should be matched by - # a grestore at the end of the PostScript code. - # This grestore can be specified on the Ghostscript command-line. - - def wrapper - fail EPBBError, prn unless valid - fname = mktemp( 'ps' ) - File.open( fname, $W ) do |f| - f.binmode if ARCH == 'w32' - f.write( - "%%BoundingBox: 0 0 #{width.to_f.ceil} #{height.to_f.ceil}\n" + - "%%HiResBoundingBox: 0 0 #{width.to_f} #{height.to_f}\n" + - "<< /PageSize [#{width} #{height}] >>" + - " setpagedevice\n" + - "gsave #{(-(@llx.to_f)).to_s} #{(-(@lly.to_f)).to_s}" + - " translate\n" ) > 0 - end # open - return fname - end # wrapper - - ################## - - # convert hiresboundingbox to hires boundingbox comment - - def hrcomment - fail EPBBError, prn unless valid - "%%HiResBoundingBox: #{@llx} #{@lly} #{@urx} #{@ury}" - end - -end # class HRBb - -########################################### - -# PsPdf class definition - -class PsPdf - - protected - - SAFESIZE = 16000 - - public - - # class methods ### - - def PsPdf.pdf_options - "-dPDFSETTINGS=/#{$settings.pdf_target}" + \ - ( $settings.pdf_version == 'default' ? '' : \ - " -dCompatibilityLevel=#{$settings.pdf_version}" ) + \ - ($settings.pdf_custom ? ' ' + $settings.pdf_custom : '') - end - - def PsPdf.gs_options - "-dNOPAUSE -dBATCH -q -dSAFER" - end - - def PsPdf.ps_options( sep ) - # the sep(arable_color) option forces a cmyk color model, - # which should improve chances of grayscaling later on. - if sep - case $settings.ps_options - when /-level\dsep\b/ - $settings.ps_options - when /-level\d\b/ - $settings.ps_options.sub( /(-level\d)/, '\1sep' ) - else - $settings.ps_options + " -level3sep" - end # case - else - $settings.ps_options - end # ifthenelse sep - end - - # instance methods ### - - attr_accessor :path, :bb, :hrbb, :type, :npages, :atfront, :hr_atfront - - ################## - - def initialize( params={} ) - - ext = params[ 'ext' ] - file = params[ 'file' ] - - if not ext and not file - @path = nil - @type = nil - elsif not file - @path = mktemp( ext ) - @type = case ext.downcase - when 'pdf' - 'pdf' - when 'eps' - 'eps' - when 'ps' - 'ps' - else - 'other' - end - else - @path = file - @type = identify( file ) - @npages = pdf_pages if @type == 'pdf' # should we do this? - @npages = 1 if @type =~ /^eps/ - end - end # initialize - - def file_info - if @npages and @type !~ /^eps/ - return "File type of #{@path} is #{@type} with #{@npages} pages" - else - return "File type of #{@path} is #{@type}" - end - end - - ################## - - # debug string for EPCallError - - def buginfo( params = nil ) - b = "Source was: " + @path + $/ - b = b + param_hash_prn( params ) if params - b - end - - ################## - - # Find boundingbox, simple case. - # We shall call this method only for eps PsPdf objects which were - # converted by pdftops or Ghostscript, so we can be sure that - # the boundingbox is not (atend). - # We also assume that the hrbb lies within the bb. - # The file is not rewritten. - - def find_bb_simple - - fail EPCallError, buginfo unless @type == 'eps' - @bb = nil - @hrbb = nil - slurp = '' - File.open( @path, $R ) do |fl| - slurp = fl.read( [File.size(@path), SAFESIZE].min ) - end - lines = slurp.split( /\r\n?|\n/ ) - # look for a bb or a hrbb - # if a valid bb is found, check the next line for hrbb - # but look no further; we don't want to mistake a hrbb of - # an included eps for the hrbb of the outer eps. - lines.each do |l| - if l =~ BB_PAT - @bb = Bb.from_comment( l ) - elsif l =~ HRBB_PAT - @hrbb = HRBb.from_hrcomment( l ) - elsif @bb - break # stop looking; we expect hrbb next to bb - end - break if @bb and @hrbb - end # do |l| - fail EPBBError, @path unless @bb and @bb.valid - - end # def find_bb_simple - - ################## - - def pdf_pages - - fail EPCallError, buginfo unless @type == 'pdf' - - @npages = nil - - # get n. of pages; the Ghostscript pdf2dsc.ps script will - # create a list of pages. It seems to ignore logical p. numbers. - - dsc = mktemp( "dsc" ) - cmd = "\"#{$settings.gs_prog}\" -dNODISPLAY -q" + - " -sPDFname=\"#{@path}\" -sDSCname=\"#{dsc}\" pdf2dsc.ps" - write_log cmd # if $DEBUG - fail EPSystemError, cmd unless system( cmd ) and test( ?s, dsc ) - lines = [] - File.open( dsc, $R ) do |f| - lines = f.read( SAFESIZE ).split( /\r\n?|\n/ ) - end - lines.each do |l| - if l =~ /^%%Pages:\s+(\d+)\s*$/ - @npages = $1.to_i - break - end # if =~ - end # do |l| - - return @npages - - end # pdf_pages - - ############################################################# - - # direct conversions. These methods return a PsPdf object, - # and raise an exception in case of failure. - # Direct conversions convert at most once between PostScript and pdf. - # They always write to a temporary file. - - ################## - - # eps_clean: write source eps to an eps without preview, and - # with a boundingbox in the header. - # clean up any potential problems - # the eps is always written to a new file. - - def eps_clean - - fail EPCallError, buginfo if @type != 'eps' and @type != 'epsPreview' - atend = nil - hr_atend = nil - slurp = '' - offset, ps_len = nil, nil - if @type == 'eps' - offset = 0 - ps_len = File.size( @path ) - else - # read ToC; see Adobe EPS spec - File.open( @path, $R ) do |fl| - # bug workaround for unpack - if "\001\000\000\000".unpack( 'V' )[0] == 1 - dummy, offset, ps_len = fl.read( 12 ).unpack( 'VVV' ) - else - dummy, offset, ps_len = fl.read( 12 ).unpack( 'NNN' ) - end - end # File - end # ifthenelse @type - - # [hires] boundingbox unknown and possibly atend - @bb, @atfront, @hrbb, @hr_atfront = nil, nil, nil, nil - - # limit search for boundingbox comments. - # For very large eps files, we don't want to scan the entire file. - # A boundingbox comment should be in the header or trailer, - # so scanning the first and last few KB should be plenty. - - File.open( @path, $R ) do |fl| - fl.seek( offset, IO::SEEK_SET ) - slurp = fl.read( [ps_len,SAFESIZE].min ) - end - - # We capture both lines and separators, as an easy way to - # keep track of how many bytes have been read. - # we assume that if there is a hires bb then - # bb and hires bb are on consecutive lines. - # Otherwise the logic would get too messy. - - # The epsfile will be reconstituted from: - # a series of lines and line separators; then either - # - a bbox or - # - (bbox or hrbbox), separator, (hrbbox or bbox) - # the big blob - # possibly a trailer with removed bb comments - - pre_lines = slurp.split( /(\r\n?|\n)/ ) - bb_comment = '' - # initialize indices of bb comments to less than smallest index - i_bb = -1 - i_hrbb = -1 - i = -1 - i_end = -1 - pre_length = 0 - pre_lines.each do |l| - pre_length += l.length - i += 1 - next if l =~ /(\r\n?|\n)/ - if l =~ BB_PAT - @bb = Bb.from_comment( l ) - @atfront = true - i_bb = i - elsif l =~ BB_END - atend = true - i_bb = i - elsif l =~ HRBB_PAT - @hrbb = HRBb.from_hrcomment( l ) - @hr_atfront = true - i_hrbb = i - elsif l =~ HRBB_END - hr_atend = true - i_hrbb = i - elsif @bb or atend - i_end = i - break # stop looking; we expect hrbb next to bb - end # =~ BB_PAT - end # do |l| - if atend or hr_atend - if ps_len > SAFESIZE - File.open( @path, $R ) do |fl| - fl.seek( offset + ps_len - SAFESIZE, IO::SEEK_SET ) - slurp = fl.read( SAFESIZE ) - end - end # else use old slurp - post_lines = slurp.split( /(\r\n?|\n)/ ) - # initialize indices of atend bb comments to more than largest index - j = post_lines.length - j_bb = j - j_hrbb = j - j_end = j - post_length = 0 - post_lines.reverse_each do |l| - post_length += l.length - j -= 1 - next if l =~ /(\r\n?|\n)/ - if l =~ BB_PAT - bb_comment = l - @bb = Bb.from_comment( bb_comment ) - j_bb = j - elsif l =~ HRBB_PAT - bb_comment = l - @hrbb = HRBb.from_hrcomment( bb_comment ) - j_hrbb = j - end - if (@bb or !atend) and (@hrbb or !hr_atend) - j_end = j - break - end - end # do - #post_lines.slice([j_bb,j_hrbb].min .. -1).each do |l| - # post_block = post_block + l - end # if atend - - fail EPBBError, @path unless @bb.valid - # in case of discrepancy, drop @hrbb. - # we accept a `safety margin': a difference >1pt is not a discrepancy. - @hrbb = nil if @hrbb and - (( @bb.llx.to_i > @hrbb.llx.to_f.floor ) or - ( @bb.lly.to_i > @hrbb.lly.to_f.floor ) or - ( @bb.urx.to_i < @hrbb.urx.to_f.ceil ) or - ( @bb.ury.to_i < @hrbb.ury.to_f.ceil )) - - retVal = PsPdf.new( 'ext' => 'eps' ) - - # always rewrite the eps, to get normalized, ruby-compatible bb syntax. - # we modify part of the header, and possibly of the trailer. - - # offset, length of middle part, which can be copied byte for byte - cp_start = offset + pre_length - cp_len = ps_len - pre_length - cp_len -= post_length if ( atend or hr_atend ) - # replace boundingbox comments - if atend - pre_lines[i_bb].sub!( BB_END, @bb.comment ) - post_lines[j_bb].sub!( BB_PAT, '%%' ) - else - pre_lines[i_bb].sub!( BB_PAT, @bb.comment ) - end - if @hrbb - # replace valid hires boundingbox comments - if hr_atend - pre_lines[i_hrbb].sub!( HRBB_END, @hrbb.hrcomment ) - post_lines[j_hrbb].sub!( HRBB_PAT, '%%' ) - else - pre_lines[i_hrbb].sub!( HRBB_PAT, @hrbb.hrcomment ) - end - elsif i_hrbb >= 0 # invalid hires bb - # erase invalid hr boundingbox comments - if atend - pre_lines[i_hrbb].sub!( HRBB_END, '%%' ) - post_lines[j_hrbb].sub!( HRBB_PAT, '%%' ) - else - pre_lines[i_hrbb].sub!( HRBB_PAT, '%%' ) - end - end # test for @hrbb - - File.open( retVal.path, $W ) do |fl| - fl.write( pre_lines[ 0 .. i_end ].join ) - end - sliceFile( @path, retVal.path, cp_len, cp_start, $A ) - if ( atend or hr_atend ) - File.open( retVal.path, $A ) do |fl| - fl.write( post_lines[ j_end .. -1 ].join ) - end - end - retVal.bb = @bb.copy - retVal.hrbb = @hrbb.copy if @hrbb - retVal.atfront = true - retVal.hr_atfront = true - retVal.npages = 1 - return retVal - end # eps_clean - - ################## - - # Use the Ghostscript bbox device to give an eps a tight boundingbox. - # Here, we don't test for use_hires_bb. - # 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. - # Maybe we should test whether gs' bbox device can handle - # nonnegative coordinates. - # The boundingbox in the eps is rewritten, but the eps is - # not otherwise converted. - # We don't create a new PsPdf object. - - def fix_bb - - fail EPCallError, buginfo \ - unless @type == 'eps' and @bb.non_negative - # let ghostscript calculate new boundingbox - cmd = $settings.gs_prog + ' ' + PsPdf.gs_options + - " -sDEVICE=bbox \"" + @path + "\" 2>&1" - write_log cmd # if $DEBUG - bb_output = `#{cmd}` - # inspect the result - fail EPSystemError, cmd unless $? == 0 and bb_output - bb_output.split(/\r\n?|\n/).each do |b| - if b =~ BB_PAT - bb_temp = Bb.from_comment( b ) - fail EPBBError, bb_output unless bb_temp.valid - bb_temp.expand unless $settings.use_hires_bb - @bb = bb_temp.copy - elsif b =~ HRBB_PAT - bb_temp = HRBb.from_hrcomment( b ) - @hrbb = bb_temp.valid ? bb_temp.copy : nil - end - end # do |b| - fail EPBBError, bb_output unless @bb - # this won't happen, but we deal with it anyway: - @hrbb = HRBb.new( @bb.llx, @bb.lly, @bb.urx, @bb.ury ) unless @hrbb - - # locate current [hr]boundingbox, which ha[s|ve] to be replaced - # assumptions: both in header, and hrbb no later than - # first line after bb. - slurp = '' - File.open( @path, $R ) do |fl| - slurp = fl.read( [File.size(@path),SAFESIZE].min ) - end - pre_lines = slurp.split( /(\r\n?|\n)/ ) - i_bb = -1 - i_hrbb = -1 - i = -1 - i_end = -1 - pre_length = 0 - pre_lines.each do |l| - pre_length += l.length - i += 1 - next if l =~ /(\r\n?|\n)/ - if l =~ BB_PAT - i_bb = i - elsif l =~ HRBB_PAT - i_hrbb = i - elsif i_bb >= 0 - i_end = i - break # stop looking; we expect hrbb next to bb - end # =~ BB_PAT - end # do |l,i| - fail EPBBError, "No boundingbox found in #{@path}" if i_bb < 0 - - # replace boundingbox[es] by editing initial part pre_block - # and copying the rest byte for byte - if i_hrbb < 0 - # no old hrbb; replace bb with new bb and new hrbb - # pre_lines[i_bb+1] should match /\r\n?|\n/ - pre_lines[i_bb].sub!( BB_PAT, @bb.comment + - pre_lines[i_bb+1] + @hrbb.hrcomment ) - else - pre_lines[i_bb].sub!( BB_PAT, @bb.comment ) - pre_lines[i_hrbb].sub!( HRBB_PAT, @hrbb.hrcomment ) - end - oldpath = @path - @path = mktemp( 'eps' ) - File.open( @path, $W ) { |fl| - fl.write( pre_lines[ 0 .. i_end ].join ) } - sliceFile( oldpath, @path, \ - File.size( oldpath ) - pre_length, pre_length, $A ) - return self - - end # fix_bb - - ################## - - # Convert eps to pdf. - # The eps should already have a boundingbox in the header. - - def eps_to_pdf( params={} ) - - gray = params[ 'gray' ] - - fail EPCallError, buginfo( params ) unless @type == 'eps' and @atfront - - wrp = ( $settings.use_hires_bb and @hrbb ) ? @hrbb.wrapper : @bb.wrapper - - retVal = PsPdf.new( 'ext' => 'pdf' ) - cmd = "\"#{$settings.gs_prog}\" #{PsPdf.gs_options}" + - " -sDEVICE=pdfwrite #{PsPdf.pdf_options}" + - " -sOutputFile=\"#{retVal.path}\"" + - ( gray ? (' "' + $GRAYHEAD + '"') : "" ) + - " \"#{wrp}\" \"#{@path}\" -c grestore" - write_log cmd # if $DEBUG - fail EPSystemError, cmd \ - unless system( cmd ) and test( ?s, retVal.path ) - retVal.npages = 1 - return retVal - - end # eps_to_pdf - - ################## - - # Convert source pdf to eps. - # The option sep_color is ignored if pdftops is not available. - - def pdf_to_eps( params={} ) - - page = params[ 'page' ] ? params[ 'page' ].to_i : 1 - sep = params[ 'sep' ] - - fail EPCallError, buginfo( params ) unless @type == 'pdf' - fail EPCallError, buginfo( params ) \ - unless page > 0 and page <= @npages - retVal = PsPdf.new( 'ext' => 'eps' ) - if $settings.pdftops_prog and $settings.use_pdftops - cmd = "\"#{$settings.pdftops_prog}\"" + - " #{PsPdf.ps_options( sep )}" + - " -paper match -eps -f #{page} -l #{page}" + - " \"#{@path}\" \"#{retVal.path}\"" - else - cmd = "\"#{$settings.gs_prog}\" -sDEVICE=epswrite -r600" + - " #{PsPdf.gs_options}" + - " -dFirstPage=#{page}" + - " -dLastPage=#{page}" + - " -sOutputFile=\"#{retVal.path}\" \"#{@path}\"" - end - write_log cmd # if $DEBUG - fail EPSystemError, cmd unless \ - system( cmd ) and test( ?s, retVal.path ) - -# 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 -# match first chk_ze chars against `% Produced by' - chk_size = [ 1500, File.size( retVal.path ) ].min - slurp = '' - File.open( retVal.path, $R ) do |fl| - slurp = fl.read( chk_size ) - end # File - pdfpat = /([\r\n])% Produced by/m - if slurp =~ pdfpat - newpath = mktemp( 'eps' ) - write_log "pdftops header fix #{retVal.path} => #{newpath}" - File.open( newpath, $W ) do |fl2| - fl2.write( slurp.sub( pdfpat, '\1%% Produced by)' ) ) - end - sliceFile( retVal.path, newpath, File.size( retVal.path ) - chk_size, - chk_size, $A ) - retVal.path = newpath - end # if =~ -# end fix for incorrect DSC header produced by some versions of pdftops - retVal.atfront = 1 - retVal.find_bb_simple - retVal.npages = 1 - return retVal - - end # pdf_to_eps - - ################## - - def ps_to_pdf( params={} ) - - gray = params[ 'gray' ] - - fail EPCallError, buginfo( params ) \ - unless @type == 'ps' - - retVal = PsPdf.new( 'ext' => 'pdf' ) - cmd = "\"#{$settings.gs_prog}\" #{PsPdf.gs_options}" + - " -sDEVICE=pdfwrite #{PsPdf.pdf_options}" + - " -sOutputFile=\"#{retVal.path}\"" + - ( gray ? (' "' + $GRAYHEAD + '"') : "" ) + " \"#{@path}\"" - write_log cmd # if $DEBUG - fail EPSystemError, cmd \ - unless system( cmd ) and test( ?s, retVal.path ) - - retVal.pdf_pages - return retVal - - end # def ps_to_pdf - - ################## - - def pdf_to_ps( params={} ) - - sep = params[ 'sep' ] - page = params[ 'page' ] ? params[ 'page' ].to_s : nil - - fail EPCallError, buginfo( params ) unless @type == 'pdf' - retVal = PsPdf.new( 'ext' => 'ps' ) - if $settings.pdftops_prog and $settings.use_pdftops - cmd = "\"#{$settings.pdftops_prog}\" #{PsPdf.ps_options( sep )}" + - ( page ? " -f #{page} -l #{page}" : '' ) + - " -paper match \"#{@path}\" \"#{retVal.path}\"" - else - cmd = "\"#{$settings.gs_prog}\" #{PsPdf.gs_options}" + - " -sDEVICE=pswrite -r600" + - ( page ? " -dFirstPage=#{page} -dLastPage#{page}" : '' ) + - " -sOutputFile=\"#{retVal.path}\"" + " \"#{@path}\"" - end - write_log cmd # if $DEBUG - fail EPSystemError, cmd unless \ - system( cmd ) and test( ?s, retVal.path ) - retVal.npages = @npages - return retVal - - end # pdf_to_ps - - ################## - - # all possible conversions, as concatenations of direct conversions. - - def any_to_any( params={} ) - - type = params[ 'type' ] - page = params[ 'page' ] - bbox = params[ 'bbox' ] - gray = params[ 'gray' ] - gRAY = params[ 'gRAY' ] # try harder to grayscale - gray = 1 if gRAY - bbox = nil if type == 'ps' - - fail EPCallError, buginfo( params ) \ - unless ( type=='eps' or type=='pdf' or type=='ps' ) - fail EPCallError, buginfo( params ) \ - if @type=='other' - fail EPCallError, buginfo( params ) \ - if type=='ps' and bbox - fail EPCallError, buginfo( params ) \ - if @type=='eps' and type=='ps' - - # gRAY tries harder to grayscale, by converting color first to cmyk - # even if it requires an additional eps - pdf - eps roundtrip. - # Normally, conversion to cmyk is done only if it doesn't take - # an extra roundtrip. - # The separable color conversion is an option of pdftops. - - pp = self - - pp = pp.eps_clean if pp.type == 'epsPreview' - pp = pp.eps_clean if pp.type == 'eps' and \ - not ( pp.bb and pp.atfront ) # => not yet `cleaned' - #pp.pdf_pages if pp.type == 'pdf' and not pp.npages - # now also done in initialize - - case pp.type - when 'eps' - - # in some cases extra eps => pdf => eps roundtrip: - # roundtrip guarantees bb.non_negative, necessary for fix_bb. - # pdf_to_ps( sep ) improves chances of grayscaling. - - case type - when 'eps' - pp = pp.eps_to_pdf.pdf_to_eps( 'sep' => gray ) \ - if gRAY or ( bbox and not pp.bb.non_negative ) - pp = pp.eps_to_pdf( 'gray' => gray ).pdf_to_eps if gray - pp = pp.fix_bb if bbox - return pp - - when 'pdf' - pp = pp.eps_to_pdf.pdf_to_eps( 'sep' => gray ) \ - if gRAY or ( bbox and not pp.bb.non_negative ) - pp = pp.fix_bb if bbox - return pp.eps_to_pdf( 'gray' => gray ) - - when 'ps' - if gRAY - pp = pp.eps_to_pdf.pdf_to_ps( 'sep' => gray ) - pp = pp.ps_to_pdf( 'gray' => gray ).pdf_to_ps - else - pp = pp.eps_to_pdf( 'gray' => gray ).pdf_to_ps - end - return pp - - end # case type - - when 'pdf' - - case type - when 'eps' - if not gray - pp = pp.pdf_to_eps( 'page' => page ) - pp = pp.fix_bb if bbox - return pp - else - pp = pp.pdf_to_eps( 'page' => page, 'sep' => 1 ) - pp = pp.fix_bb if bbox - pp = pp.eps_to_pdf( 'gray' => 1 ) - return pp.pdf_to_eps - end - - when 'pdf' - return pp unless ( gray or bbox or page ) - if bbox or not $settings.pdftops_prog - pp = pp.pdf_to_eps( 'page' => page, 'sep' => gray ) - pp = pp.fix_bb if bbox - return pp.eps_to_pdf( 'gray' => gray ) - else - pp = pp.pdf_to_ps( 'page' => page, 'sep' => gray ) - return pp.ps_to_pdf( 'gray' => gray ) - end - - when 'ps' - if gray - pp = pp.pdf_to_ps( 'sep' => 1 ) - pp = pp.ps_to_pdf( 'gray' => 1 ) - end - return pp.pdf_to_ps - - end # case type - - when 'ps' - - case type - when 'eps' - if gRAY - pp = pp.ps_to_pdf.pdf_to_eps( 'page' => page, 'sep' => 1 ) - pp = pp.eps_to_pdf( 'gray' => 1 ) - pp = pp.pdf_to_eps - else - pp = pp.ps_to_pdf( 'gray' => gray ) - pp = pp.pdf_to_eps( 'page' => page ) - end - return pp.fix_bb - - when 'pdf' - if bbox - pp = pp.ps_to_pdf.pdf_to_eps( 'sep' => gray, 'page' => page ) - pp = pp.fix_bb - return pp = pp.eps_to_pdf( 'gray' => gray ) - elsif page - pp = pp.ps_to_pdf.pdf_to_ps( 'sep' => gray, 'page' => page ) - return pp = pp.ps_to_pdf( 'gray' => gray ) - else - pp = pp.ps_to_pdf.pdf_to_ps( 'sep' => 1 ) if gRAY - return pp.ps_to_pdf( 'gray' => gray ) - end - - when 'ps' - return pp unless page or gray - if gRAY - pp = pp.ps_to_pdf.pdf_to_ps( 'page' => page, 'sep' => 1 ) - return pp.ps_to_pdf( 'gray' => 1 ).pdf_to_ps - else - pp = pp.ps_to_pdf( 'gray' => gray ) - return pp = pp.pdf_to_ps( 'page' => page ) - end - - end # case type - - end # case pp.type - - #raise EPCallError - fail "Unsupported conversion" - # this shouldn't happen anymore - - end # any_to_any - -end # class PsPdf - -################################# -# main program - -require 'optparse' - -def gui( action ) - case action - when 'config_w' then - puts "pdftops_prog=#{$settings.pdftops_prog}" if ARCH=='w32' - puts "pdf_viewer=#{$settings.pdf_viewer}" if ARCH=='unix' - puts "ps_viewer=#{$settings.ps_viewer}" if ARCH=='unix' - puts "defaultDir=#{$settings.defaultDir}" - puts "ignore_pdftops=#{$settings.ignore_pdftops}" - puts "pdf_target=#{$settings.pdf_target}" - puts "pdf_version=#{$settings.pdf_version}" - puts "pdf_custom=#{$settings.pdf_custom}" - puts "ps_options=#{$settings.ps_options}" - puts "ignore_hires_bb=#{$settings.ignore_hires_bb}" - puts "bb_spread=#{$settings.bb_spread}" - exit - when 'config_r' then - while line = gets - (varname, val) = line.split('=', 2) - varname.strip! if varname - val.strip! if val - if $settings.has_key?( varname ) - val = nil if val == '' - $settings[varname].val = val - # puts( "\"#{val}\" assigned to #{varname}" ) - end - end # while - $settings.write_settings - exit - when nil then - $from_gui = true - else - abort( "Action should be omitted or 'config_w' or 'config_r'" ) - end # case -end - -# create a pause to examine temp files -def abortt( msg ) - if $DEBUG - $stderr.puts msg + "\nPress <enter> to finish" - $stdin.gets - end - fail -end - -save_settings = false - -opts = OptionParser.new do |opts| - # for help output - opts.banner = "Convert between [e]ps and pdf formats" - opts.separator "Usage: epspdf.rb [options] infile [outfile]" - opts.separator "" - opts.separator "Default for outfile is file.pdf" + - " if infile is file.eps or file.ps" - opts.separator "Default for outfile is file.eps" + - " if infile is file.pdf" - opts.separator "" - - opts.on( "-g", "--gray", "--grey", - "Convert to grayscale;", - "success not guaranteed" ) do |opt| - $options.gray = true - end - - opts.on( "-G", "--GRAY", "--GREY", - "Try harder to convert to grayscale" ) do |opt| - $options.gRAY = true - end - - opts.on( "-p PAGENUMBER", "--pagenumber=PAGENUMBER", - "Page to be converted or selected", Integer ) do |opt| - $options.page = opt - end - - opts.on( "-b", "--bbox", "--BoundingBox", - "Compute tight boundingbox" ) do |opt| - $options.bbox = true - end - - opts.on( "-n", "--no-hires", - "Don't use hires boundingbox" ) do |opt| - $settings.ignore_hires_bb = '1' - end - - opts.on( "-r", "--hires", - "Use hires boundingbox" ) do |opt| - $settings.ignore_hires_bb = '0' - end - - opts.on( "-T TARGET", "--target=TARGET", - PDF_TARGETS, - "Target use of pdf; one of", - "#{PDF_TARGETS.join( ', ' )}" ) do |opt| - $settings.pdf_target = opt - end - - opts.on( "-N PDFVERSION", "--pdfversion=PDFVERSION", - PDF_VERSIONS, - "Pdf version to be generated" ) do |opt| - $settings.pdf_version = opt - end - - opts.on( "-V PDFVERSION", "--version=PDFVERSION", - PDF_VERSIONS, - "Deprecated; use `-N' or `--pdfversion'." ) do |opt| - if opt == "" - puts EPVERSION - exit - end - $settings.pdf_version = opt - end - - opts.on( "-I", - "Ignore pdftops even if available", - "(default: use if available)" ) do |opt| - $settings.ignore_pdftops = '1' - end - - opts.on( "-U", - "Use pdftops if available", - "(overrides previous -I setting)" ) do |opt| - $settings.ignore_pdftops = '0' - end - - opts.on( "-C CUSTOMOPTIONS", "--custom=CUSTOMOPTIONS", - "Custom options for conversion to pdf,", - "view Use.htm and ps2pdf.htm from", - "the Ghostscript documentation set" ) do |opt| - $settings.pdf_custom = opt - end - - opts.on( "-P PSOPTIONS", "--psoptions=PSOPTIONS", - "Options for pdftops; default -level3,", - "don't include -eps or page number options;", - "these will be generated by the program" ) do |opt| - $settings.ps_options = opt - end - - opts.on( "-i", "--info", - "Info: display detected filetype" ) do |opt| - $options.info = true - end - - opts.on( "-s", - "Save (some) settings" ) do |opt| - save_settings = true - end - - opts.on( "-d", "Debug: don't remove temp files" ) do |opt| - $DEBUG = 1 - end - - opts.on( "--gui[=ACTION]", "Do not use; reserved for GUI" ) do |opt| - gui( opt ) - end - - opts.separator "" - - opts.on( "-v", "Prints version info" ) do |opt| - puts EPVERSION - exit - end - - opts.on_tail("-h", "--help", "Show this message") do - puts "Epspdf version " + EPVERSION - puts "Copyright (C) " + COPYRIGHT + " Siep Kroonenberg" - puts opts - exit - end -end # opts - -# hack alert! we support `--version' for version info although -# --version is still interpreted as desired pdf output version - -if ARGV.length == 1 and ARGV[0] == '--version' - puts EPVERSION - exit -end - -# save original command-line for later reporting -cmdline = "#{$0} #{ARGV.join( sep=' ' )}" - -# parse options destructively -begin - opts.parse!( ARGV ) -rescue OptionParser::ParseError => e - STDERR.puts e.message, "\n", opts - exit( -1 ) -end - -# log cmdline AFTER we found out whether we run from gui -write_log( cmdline ) - -$options.page = 1 if $options.bbox and not $options.page - -$settings.write_settings if save_settings - -if ARGV.length < 1 - if not save_settings # help output - puts opts - abort - else - exit - end -elsif $options.info - p = PsPdf.new( 'file' => ARGV[0] ) - puts( p.file_info ) - exit -elsif ARGV.length > 1 and - File.expand_path( ARGV[0] ) == File.expand_path( ARGV[1] ) - abort " Input and output files should be different." -else - infile = ARGV[0] - abort( infile + " not found or empty" ) unless test( ?s, infile ) -end - -# done with options - -######################################## - -source = PsPdf.new( 'file' => infile ) - -# We aren't finicky about the extension of the input file, -# but still want to check whether pdf is pdf. - -case source.type -when 'eps', 'epsPreview', 'ps' - abort "Wrong extension; input is not in pdf format" \ - if infile =~ /\.pdf$/i -when 'pdf' - abort "Wrong extension; input is in pdf format" \ - if infile !~ /\.pdf$/i -else - abort "Invalid input file type; not [e]ps or pdf" \ - if source.type == 'other' -end # case source.type - -# find or construct output file name -if ARGV.length > 1 - outfile = ARGV[1] -else - case source.type - when 'eps', 'epsPreview', 'ps' - outfile = infile.sub( /\.[^.]+$/, '.pdf' ) - when 'pdf' - outfile = infile.sub( /\.[^.]+$/, '.eps' ) - end # case -end # ifthenelse ARGV.length -outfile = File.expand_path( outfile ) - -$options.type = case outfile -when /\.pdf$/i - "pdf" -when /\.ps$/i - "ps" -when /\.eps$/i - "eps" -else - nil -end # case outfile - -abort "Unknown or unsupported output file extension" \ - unless $options.type -#abort "Output format not supported without xpdf utilities" \ -# if outfile == 'ps' and not $settings.pstopdf_prog - -pp = PsPdf.new( 'file' => infile ) - -begin # inner rescue block - pp = pp.any_to_any( $options ) - write_log( pp.file_info ) if $from_gui - ccp( pp.path, outfile ) -rescue EPCallError => exc - mess = - "Wrong method call or conversion not supported or wrong page number" + - $/ + exc.message + $/ + exc.backtrace.join( $/ ) - write_log( mess ) - puts mess - exit 1 -rescue EPBBError => exc - mess = "Boundingbox problem" + $/ + - exc.message + $/ + exc.backtrace.join( $/ ) - write_log( mess ) - puts mess - exit 1 -rescue EPCopyError => exc - mess = "Copying problem" + $/ + - exc.message + $/ + exc.backtrace.join( $/ ) - write_log( mess ) - puts mess - exit 1 -rescue EPSystemError => exc - mess = "Problem with system call" + $/ + - exc.message + $/ + exc.backtrace.join( $/ ) - write_log( mess ) - puts mess - exit 1 -rescue StandardError => exc - mess = exc.message + $/ + exc.backtrace.join( $/ ) - write_log( mess ) - puts mess - exit 1 -end # rescue block - -cleantemp unless $DEBUG == 1 -__END__ diff --git a/Build/source/texk/texlive/linked_scripts/epspdf/epspdf.tlu b/Build/source/texk/texlive/linked_scripts/epspdf/epspdf.tlu new file mode 100755 index 00000000000..ae793d7f344 --- /dev/null +++ b/Build/source/texk/texlive/linked_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 diff --git a/Build/source/texk/texlive/linked_scripts/epspdf/epspdftk.tcl b/Build/source/texk/texlive/linked_scripts/epspdf/epspdftk.tcl index 361ac83d6b1..32bb93011e9 100755 --- a/Build/source/texk/texlive/linked_scripts/epspdf/epspdftk.tcl +++ b/Build/source/texk/texlive/linked_scripts/epspdf/epspdftk.tcl @@ -3,7 +3,7 @@ # epspdf conversion utility, GUI frontend ##### -# Copyright (C) 2006, 2008, 2009, 2010, 2011 Siep Kroonenberg +# Copyright (C) 2006, 2008, 2009, 2010, 2011, 2013 Siep Kroonenberg # n dot s dot kroonenberg at rug dot nl # # This program is free software, licensed under the GNU GPL, >=2.0. @@ -16,54 +16,66 @@ package require Tk set classic_unix [expr {$::tcl_platform(platform) eq "unix" && \ $::tcl_platform(os) ne "Darwin"}] -# normally, epspdf.rb should be in the same directory -# and ruby should be on the searchpath. -# However, the Windows installer version includes a Ruby subset -# and wraps this script in a starpack. +# combo boxes and -ignorestderr introduced in Tk8.5 +set ge_85 [expr {[string index $::tcl_patchLevel 2] > 4}] -### calling epspdf.rb ######################### +# normally, epspdf.tlu should be in the same directory +# and texlua should be on the searchpath. +# However, the Windows installer version wraps this script in a starpack. -# Get full path of epspdf.rb. It should be in the same directory as -# either this script or of the starpack containing this script. +# logging: to a log window, not to a file +proc write_log {s} { + if {[winfo exists .log_t.text]} { + .log_t.text configure -state normal + .log_t.text insert end "$s\n" + .log_t.text yview moveto 1 + .log_t.text configure -state disabled + # } else { + # puts $s + } +} + +### calling epspdf.tlu ######################### + +# Get full path of epspdf.tlu. It should be in the same directory as +# either this script or of the starpack containing this script, # For non-windows versions, epspdftk might be called via a symlink. -# For the windows-only starpack, a ruby subset is included. -# Otherwise, ruby should be on the searchpath. +# Pdftops done elsewhere. proc set_progs {} { set scriptfile [file normalize [info script]] - # starpack edition? - set starred 0 - if {$::tcl_platform(platform) eq "windows"} { + set syml 0 + if {$::tcl_platform(platform) eq "unix" && \ + ! [catch {file readlink [$scriptfile]}]} { + set syml 1 + } + set eproot [file dirname $scriptfile] + if {$::tcl_platform(platform) eq "unix" && \ + ! [catch {file readlink $scriptfile}]} { + # evaluate readlink from symlink directory + set savedir [pwd] + cd $eproot + set eproot [file dirname [file normalize [file readlink $scriptfile]]] + cd $savedir + } + if {! [file exists [file join $eproot "epspdf.tlu"]]} { + # starpack edition? + set starred 0 foreach l [info loaded] { if {[lindex $l 1] eq "tclkitpath"} { set starred 1 break } } - } - set syml 0 - if {$::tcl_platform(platform) eq "unix" && \ - ! [catch {file readlink [$scriptfile]}]} { - set syml 1 - } - if {$starred} { - set eproot [file dirname [file normalize [info nameofexecutable]]] - set ::ruby [file normalize "$eproot/../rubysub/bin/ruby.exe"] - } else { - set eproot [file dirname $scriptfile] - if {$::tcl_platform(platform) eq "unix" && \ - ! [catch {file readlink $scriptfile}]} { - # evaluate readlink from symlink directory - set savedir [pwd] - cd $eproot - set eproot [file dirname [file normalize [file readlink $scriptfile]]] - cd $savedir + if {$starred} { + set eproot [file dirname [file normalize [info nameofexecutable]]] + # here no testing for symlink } - set ::ruby "ruby" } - set ::epspdf_rb [file join $eproot "epspdf.rb"] - if {! [file exists $::epspdf_rb]} { - tk_messageBox -type ok -icon error -message "Epspdf.rb not found" + set ::texlua "texlua" + set ::epspdf_tlu [file join $eproot "epspdf.tlu"] + if {! [file exists $::epspdf_tlu]} { + tk_messageBox -type ok -icon error -message "Epspdf.tlu not found" exit 1 } @@ -75,54 +87,90 @@ proc set_progs {} { set_progs -# call epspdf.rb with parameter list l (should be a list): - -set result "" -proc run_epspdf {res args} { - upvar $res result - set failed [catch [linsert $args 0 exec $::ruby $::epspdf_rb --gui] result] - if {$failed} { - wm deiconify .log_t - tk_messageBox -icon error -type ok -message "Error; see log window" - } - # update log window with $result - .log_t.text configure -state normal - .log_t.text insert end "$result\n" - .log_t.text yview moveto 1 - .log_t.text configure -state disabled - - # it is up to the caller to do anything else about failure or not. - # the user, at least, has been warned. - return [expr ! $failed] -} - -### read configured settings ######################################## - -# for checking configured viewers under non-osx unix -proc is_valid {x} { - if {[catch {exec which $x}]} {return 0} else {return 1} +# call epspdf.tlu with parameter list $args (should be a list) +# Return codes success/failure +# We also need stdout output. +# Tcl idiom: res is a variable _name_. +# The upvar construct makes it a reference parameter. + +#proc run_epspdf {res args} { +# upvar $res result +# if {$::ge_85} { +# set failed [catch [linsert $args 0 \ +# exec -ignorestderr $::texlua $::epspdf_tlu --gui=gui] result] +# } else { +# set failed [catch [linsert $args 0 \ +# exec $::texlua $::epspdf_tlu --gui=gui] result] +# } +# if {$failed} { +# # wm deiconify .log_t +# tk_messageBox -icon error -type ok -message "Error; see log window" +# } +# +# # write to log textbox +# write_log $result +# +# # it is up to the caller to do anything else about failure or not. +# # the user, at least, has been warned. +# return [expr ! $failed] +#} + +### configured and automatic settings ################################## + +# is_prog used for checking configured viewers under non-osx unix +proc is_prog {x} { + if {[expr {$::tcl_platform(platform) ne "unix"}]} {return 0} + # avoid current directory except with explicit directory + if {[expr {[string first "/" $x] >= 0 && \ + [file executable [file normalize $x]]}]} { + return 1 + } + # also check search path + set p [split $::env(PATH) ":"] ; # no need to accomodate msw + foreach d $p { + if {[expr {$d ne "" && [file executable [file join $d $x]]}]} { + return 1 + } + } + return 0 } # create a global empty settings array -array set settings [list] +array set ::settings [list] + +set ::is_tl 1 -# ask epspdf.rb for currently configured settings. +# ask epspdf.tlu for currently configured settings. # this does not include automatically configured or transient settings. # the availability of viewers is handled here. proc getsettings {} { - if [catch {exec $::ruby $::epspdf_rb --gui=config_w} set_str] { + if [catch {exec $::texlua $::epspdf_tlu --gui=config_w} set_str] { error "Epspdf configuration error: $set_str" } - #puts "settings from epspdf.rb:\n$set_str" + # write_log "settings from epspdf.tlu:\n$set_str\n" set l [split $set_str "\r\n"] + if {$::tcl_platform(platform) eq "windows"} { + set ::is_tl 1 + set settings(pdftops_prog) "" + } foreach e $l { - # $e is either a string "var=value" + # puts "settings: $e" + # $e is either a string "var = value" # or the empty string between <cr> and <lf> set i [string first "=" $e] if {$i>0} { - #puts $e - set ::settings([string range $e 0 [expr $i-1]]) \ - [string range $e [expr $i+1] end] + # write_log $e + set para [string trim [string range $e 0 [expr $i-1]]] + set val [string trim [string range $e [expr $i+1] end]] + if {$val eq "true"} {set val 1} + if {$val eq "false"} {set val 0} + if {$para eq "tl_w"} { + set ::is_tl 0 + write_log "TL for Windows not detected by epspdf" + } else { + set ::settings($para) $val + # write_log "setting $para is $val" + } } } @@ -131,25 +179,27 @@ proc getsettings {} { if {$::classic_unix} { set ::ps_viewers {} - if {$::settings(ps_viewer) ne "" && [is_valid $::settings(ps_viewer)]} { + if {$::settings(ps_viewer) ne "" && [is_prog $::settings(ps_viewer)]} { lappend ::ps_viewers $::settings(ps_viewer) } foreach v {evince okular gv kghostview ghostview} { - if {$v ne $::settings(ps_viewer) && [is_valid $v]} { + if {$v ne $::settings(ps_viewer) && [is_prog $v]} { lappend ::ps_viewers $v } } + # puts [join $::ps_viewers " "] set ::pdf_viewers {} - if {$::settings(pdf_viewer) ne "" && [is_valid $::settings(pdf_viewer)]} { + if {$::settings(pdf_viewer) ne "" && [is_prog $::settings(pdf_viewer)]} { lappend ::pdf_viewers $::settings(pdf_viewer) } foreach v {evince okular kpdf xpdf epdfview acroread \ gv kghostview ghostview} { - if {$v ne $::settings(pdf_viewer) && [is_valid $v]} { + if {$v ne $::settings(pdf_viewer) && [is_prog $v]} { lappend ::pdf_viewers $v } } + # puts [join $::pdf_viewers " "] if {[llength ::pdf_viewers] == 0 && [llength ::ps_viewers] != 0} { lappend ::pdf_viewers [lindex $::ps_viewers 0] @@ -170,22 +220,33 @@ proc getsettings {} { getsettings +proc write_settings {} { + set s "" + foreach el [array names ::settings] { + set s "$s$el = $::settings($el)\n" + } + # write_log "\nsettings for epspdf.tlu\n$s\nend writing settings\n" + if [catch {exec $::texlua $::epspdf_tlu --gui=config_r << $s} result] { + error "Epspdf configuration error: $result" + } +} + # directory and other file data if {$::argc > 0 && [file isdirectory [lindex $::argv 0]]} { - set gfile(dir) [lindex $::argv 0] -} elseif {[file isdirectory $::settings(defaultDir)]} { - set gfile(dir) $::settings(defaultDir) + set ::gfile(dir) [lindex $::argv 0] +} elseif {[file isdirectory $::settings(default_dir)]} { + set ::gfile(dir) $::settings(default_dir) } else { - set gfile(dir) $::env(HOME) + set ::gfile(dir) $::env(HOME) } -set gfile(path) "" -set gfile(type) "" -set gfile(name) "" -set gfile(npages) "" +set ::gfile(path) "" +set ::gfile(type) "" +set ::gfile(name) "" +set ::gfile(npages) "" # transient options -array set options [list gray "color" format "pdf" bbox 0 clean 1 \ +array set ::options [list gray "color" format "pdf" bbox 0 clean 1 \ pages "single" page 1] proc viewable {} { @@ -249,83 +310,106 @@ static unsigned char uparrow_bits[] = { } # mycombo -proc mycombo {w} { - # entry widget and dropdown button - frame $w - frame $w.ef - entry $w.ef.e -width 30 -borderwidth 1 - pack $w.ef.e -side left - button $w.ef.b -image dwnarrow -command "droplist $w" -borderwidth 1 - pack $w.ef.b -side right - pack $w.ef - # 'drop-down' listbox; width should match entry widget above - toplevel $w.lf -bd 0 - listbox $w.lf.l -yscrollcommand "$w.lf.s set" -height 4 -width 30 \ - -bd 1 -relief raised - grid $w.lf.l -column 0 -row 0 -sticky news - scrollbar $w.lf.s -command "$w.lf.l yview" -bd 1 - grid $w.lf.s -column 1 -row 0 -sticky ns - grid columnconfigure $w.lf 0 -weight 1 - wm overrideredirect $w.lf 1 - wm transient $w.lf - wm withdraw $w.lf - # next two bindings: - # final parameter: unmap/toggle listbox - bind $w.lf.l <KeyRelease-space> {update_e %W [%W get active] 1} - bind $w.lf.l <KeyRelease-Tab> {update_e %W [%W get active] 1} - bind $w.lf.l <FocusOut> {update_e %W [%W get active] 1} - bind $w.lf.l <1> {update_e %W [%W get @%x,%y] 0} - bind $w.lf.l <Double-1> {update_e %W [%W get @%x,%y] 1} - bind $w.ef.e <Return> {update_l %W} - bind $w.ef.e <Tab> {update_l %W} - bind $w.ef.e <FocusOut> {update_l %W} - return $w -} - -# toggle state of listbox. -# this involves calculating the place where it should appear -# and toggling the arrow image. -proc droplist {w} { - # $w.ef is the frame with the entry widget - # $w.lf is the toplevel with the listbox - # which needs to turn up right below $w.ef - if {[wm state $w.lf] eq "withdrawn" || [wm state $w.lf] eq "iconified"} { - set lfx [winfo rootx $w.ef] - set lfy [expr [winfo rooty $w.ef] + [winfo height $w.ef]] - wm geometry $w.lf [format "+%d+%d" $lfx $lfy] - wm deiconify $w.lf - $w.ef.b configure -image uparrow - } else { - wm withdraw $w.lf - $w.ef.b configure -image dwnarrow - } -} - -proc update_e {lbox vl unmap} { - set w [winfo parent [winfo parent $lbox]] - $w.ef.e delete 0 end - $w.ef.e insert 0 $vl - if {$unmap} {droplist $w} -} - -# entry => list -proc update_l {v} { - set t [$v get] - set w [winfo parent [winfo parent $v]] - for {set i 0} {$i<[$w.lf.l size]} {incr i} { - if {$t eq [$w.lf.l get $i]} { - $w.lf.l see $i - $w.lf.l activate $i - return +if {$::ge_85} { + proc update_combo {w vls} { + upvar $vls vs + set new [$w get] + if {$new ni $vs} { + if {[is_prog $new]} { + set vs [linsert $vs 0 $new] + $w configure -values $vs + } else { + tk_messageBox -title Error -icon error -message "$vl Not a program" + } } } - # $t not found - if {[$v validate]} { - $w.lf.l insert 0 $t - $w.lf.l see 0 - $w.lf.l activate 0 - } else { - tk_messageBox -message "Not a program" +} else { + proc mycombo {w} { + # entry widget and dropdown button + frame $w + frame $w.ef + entry $w.ef.e -width 30 -borderwidth 1 + pack $w.ef.e -side left + button $w.ef.b -image dwnarrow -command "toggle_list $w" -borderwidth 1 + pack $w.ef.b -side right + pack $w.ef + # 'drop-down' listbox; width should match entry widget above + toplevel $w.lf -bd 0 + listbox $w.lf.l -yscrollcommand "$w.lf.s set" -height 4 -width 30 \ + -bd 1 -relief raised + grid $w.lf.l -column 0 -row 0 -sticky news + scrollbar $w.lf.s -command "$w.lf.l yview" -bd 1 + grid $w.lf.s -column 1 -row 0 -sticky ns + grid columnconfigure $w.lf 0 -weight 1 + wm overrideredirect $w.lf 1 + wm transient $w.lf + wm withdraw $w.lf + # next two bindings: + # final parameter: unmap/toggle listbox + bind $w.lf.l <KeyRelease-space> {update_e %W [%W get active] 1} + bind $w.lf.l <KeyRelease-Tab> {update_e %W [%W get active] 1} + bind $w.lf.l <1> {update_e %W [%W index @%x,%y] 0} + bind $w.lf.l <Double-1> {update_e %W [%W index @%x,%y] 1} + bind $w.ef.e <Return> {update_l %W} + bind $w.ef.e <Tab> {update_l %W} + return $w + } + + # toggle state of listbox. + # this involves calculating the place where it should appear + # and toggling the arrow image. + proc toggle_list {w} { + # $w.ef is the frame with the entry widget + # $w.lf is the toplevel with the listbox + # which needs to turn up right below $w.ef + if {[wm state $w.lf] eq "withdrawn" || [wm state $w.lf] eq "iconified"} { + set lfx [winfo rootx $w.ef] + set lfy [expr [winfo rooty $w.ef] + [winfo height $w.ef]] + wm geometry $w.lf [format "+%d+%d" $lfx $lfy] + wm deiconify $w.lf + $w.ef.b configure -image uparrow + } else { + wm withdraw $w.lf + $w.ef.b configure -image dwnarrow + } + } + + # note: in tcl/tk 8.5, values of (some) widget variables can be accessed + # directly and explicit use of upvar is unnecessary. + + # list => entry; optionally toggle list display + proc update_e {v i toggle} { + set w [winfo parent [winfo parent $v]] + set lv [$w.lf.l cget -listvariable] + upvar $lv l + set tv [$w.ef.e cget -textvariable] + upvar $tv t + set t [lindex $l $i] + if {$toggle} {toggle_list $w} + } + + # entry => list + proc update_l {v} { + set w [winfo parent [winfo parent $v]] + set tv [$w.ef.e cget -textvariable] + upvar $tv t + set lv [$w.lf.l cget -listvariable] + upvar $lv l + set found [lsearch $l $t] + if { $found < 0} { + set ok [$v validate] + if {$ok} { + lappend l $t + set l [lsort $l] + } else { + tk_messageBox -message "Not a program" + } + } + set the_index [lsearch $l $t] + $w.lf.l see $the_index + $w.lf.l activate $the_index + wm withdraw $w.lf + $w.ef.b configure -image dwnarrow } } @@ -339,7 +423,7 @@ wm title . "PostScript- and pdf conversions" proc readhelp {} { .help_t.text configure -state normal - set helpfile [regsub {\.rb$} $::epspdf_rb {.help}] + set helpfile [regsub {\.tlu$} $::epspdf_tlu {.help}] if {[catch {set fid [open $helpfile r]}]} { .help_t.text insert end "No helpfile $helpfile found\n" } else { @@ -397,19 +481,29 @@ if {$::classic_unix} { -row 0 -column 0 -sticky w grid [label .config_t.viewf.lb_pdf -text "Pdf"] \ -row 1 -column 0 -sticky w - grid [mycombo .config_t.viewf.pdf] -row 1 -column 1 -sticky e - .config_t.viewf.pdf.ef.e configure -vcmd {is_valid %P} -validate none - .config_t.viewf.pdf.ef.e configure -textvariable settings(pdf_viewer) grid [label .config_t.viewf.lb_ps -text "PostScript"] \ -row 2 -column 0 -sticky w - grid [mycombo .config_t.viewf.ps] -row 2 -column 1 -sticky e -pady 4 - .config_t.viewf.ps.ef.e configure -vcmd {is_valid %P} -validate none - .config_t.viewf.ps.ef.e configure -textvariable settings(ps_viewer) + if {$::ge_85} { + grid [ttk::combobox .config_t.viewf.pdf] -row 1 -column 1 -sticky e + .config_t.viewf.pdf configure -values $::pdf_viewers + .config_t.viewf.pdf configure -textvariable ::settings(pdf_viewer) + bind .config_t.viewf.pdf <Return> {update_combo %W $::pdf_viewers} + grid [ttk::combobox .config_t.viewf.ps] -row 2 -column 1 -sticky e + .config_t.viewf.ps configure -values $::ps_viewers + .config_t.viewf.ps configure -textvariable ::settings(ps_viewer) + bind .config_t.viewf.ps <Return> {update_combo %W $::ps_viewers} + } else { + grid [mycombo .config_t.viewf.pdf] -row 1 -column 1 -sticky e + .config_t.viewf.pdf.lf.l configure -listvariable ::pdf_viewers + .config_t.viewf.pdf.ef.e configure -textvariable ::settings(pdf_viewer) + .config_t.viewf.pdf.ef.e configure -vcmd {is_prog %P} -validate none + grid [mycombo .config_t.viewf.ps] -row 2 -column 1 -sticky e -pady 4 + .config_t.viewf.ps.lf.l configure -listvariable ::ps_viewers + .config_t.viewf.ps.ef.e configure -textvariable ::settings(ps_viewer) + .config_t.viewf.ps.ef.e configure -vcmd {is_prog %P} -validate none + } grid columnconfigure .config_t.viewf 1 -weight 1 -pad 2 - .config_t.viewf.pdf.lf.l configure -listvariable ::pdf_viewers - .config_t.viewf.ps.lf.l configure -listvariable ::ps_viewers - spacing .config_t } @@ -423,7 +517,7 @@ pack [label .config_t.pdff.l_target -text "Target use"] -anchor w pack [frame .config_t.pdff.f_targets] -fill x foreach t {default printer prepress screen ebook} { pack [radiobutton .config_t.pdff.f_targets.$t \ - -variable settings(pdf_target) \ + -variable ::settings(pdf_target) \ -text $t -value $t] -side left -padx 2 -pady 4 -anchor w } @@ -432,14 +526,14 @@ pack [frame .config_t.pdff.f_version] -fill x foreach t {1.2 1.3 1.4 default} { regsub {\.} $t _ tp ; # replace dot in name: dots are path separators! pack [radiobutton .config_t.pdff.f_version.$tp \ - -variable settings(pdf_version) \ + -variable ::settings(pdf_version) \ -text $t -value $t] -side left -padx 2 -pady 4 -anchor w } -pack [label .config_t.pdff.l_gs \ - -text "Custom Ghostscript/ps2pdf parameters"] -anchor w -pack [entry .config_t.pdff.e_gs -border 1] -fill x -padx 2 -pady 2 -.config_t.pdff.e_gs configure -textvariable settings(pdf_custom) +#pack [label .config_t.pdff.l_gs \ +# -text "Custom Ghostscript/ps2pdf parameters"] -anchor w +#pack [entry .config_t.pdff.e_gs -border 1] -fill x -padx 2 -pady 2 +#.config_t.pdff.e_gs configure -textvariable settings(pdf_custom) spacing .config_t @@ -448,12 +542,13 @@ spacing .config_t packf [frame .config_t.psf] -ipadx 4 -fill x pack [label .config_t.psf.l_ps -text "Conversion to EPS and PostScript" \ -font boldfont] -anchor w -if {$tcl_platform(platform) eq "windows"} { +if {! $::is_tl} { + if {[string tolower [string range $::settings(pdftops_prog) end-3 end]] ne \ + ".exe"} {set ::settings(pdftops_prog) ""} pack [label .config_t.psf.l_pdftops -text "Find pdftops"] -anchor w pack [frame .config_t.psf.findf] -anchor w pack [entry .config_t.psf.findf.e -width 40] -side left -padx 4 - .config_t.psf.findf.e configure -textvariable settings(pdftops_prog) - # epspdfrc.rb already checked on this setting so we don't + .config_t.psf.findf.e configure -textvariable ::settings(pdftops_prog) packb [button .config_t.psf.findf.b -text "Browse..." \ -command find_pdftops] -side left } @@ -469,25 +564,25 @@ proc find_pdftops {} { pack [checkbutton .config_t.psf.c \ -text "Use pdftops if available (recommended)"] -anchor w -.config_t.psf.c configure \ - -variable settings(ignore_pdftops) -onvalue 0 -offvalue 1 +.config_t.psf.c configure -variable ::settings(use_pdftops) \ + -onvalue 1 -offvalue 0 spacing .config_t -# hires boundingbox setting - -packf [frame .config_t.hiresf] -ipadx 4 -fill x -pack [label .config_t.hiresf.title -font boldfont -text "Hires BoundingBox"] \ - -anchor w -pack [label .config_t.hiresf.l -text "Uncheck to prevent clipping"] \ - -anchor w - -pack [checkbutton .config_t.hiresf.c \ - -text "Use hires boundingbox if possible"] -anchor w -.config_t.hiresf.c configure \ - -variable settings(ignore_hires_bb) -onvalue 0 -offvalue 1 - -spacing .config_t +## hires boundingbox setting +# +#packf [frame .config_t.hiresf] -ipadx 4 -fill x +#pack [label .config_t.hiresf.title -font boldfont -text "Hires BoundingBox"] \ +# -anchor w +#pack [label .config_t.hiresf.l -text "Uncheck to prevent clipping"] \ +# -anchor w +# +#pack [checkbutton .config_t.hiresf.c \ +# -text "Use hires boundingbox if possible"] -anchor w +#.config_t.hiresf.c configure \ +# -variable ::settings(ignore_hires_bb) -onvalue 0 -offvalue 1 +# +#spacing .config_t # buttons for closing the configuration screen @@ -509,7 +604,7 @@ proc edit_settings {} { # store new settings proc putsettings {} { - if {$::classic_unix} { + if {$::classic_unix && ! $::ge_85} { wm withdraw .config_t.viewf.pdf.lf wm withdraw .config_t.viewf.ps.lf } @@ -518,19 +613,8 @@ proc putsettings {} { write_settings } -proc write_settings {} { - set s "" - foreach el [array names ::settings] { - set s "$s$el=$::settings($el)\n" - } - #puts "\nsettings for epspdf.rb\n$s" - if [catch {exec $::ruby $::epspdf_rb --gui=config_r << $s} result] { - error "Epspdf configuration error: $result" - } -} - proc cancelsettings {} { - if {$::classic_unix} { + if {$::classic_unix && ! $::ge_85} { wm withdraw .config_t.viewf.pdf.lf wm withdraw .config_t.viewf.ps.lf } @@ -562,22 +646,22 @@ packb [button .topf.logb -text "Show log" -command {show_w .log_t}] \ packf [frame .infof -relief sunken -border 1] -fill x grid [label .infof.dir_label -text "Directory" -anchor w] \ -row 1 -column 1 -sticky w -grid [label .infof.dir_value -textvariable gfile(dir) -anchor w] \ +grid [label .infof.dir_value -textvariable ::gfile(dir) -anchor w] \ -row 1 -column 2 -sticky w grid [label .infof.name_label -text "File" -anchor w] \ -row 2 -column 1 -sticky w -grid [label .infof.name_value -textvariable gfile(name) -anchor w] \ +grid [label .infof.name_value -textvariable ::gfile(name) -anchor w] \ -row 2 -column 2 -sticky w grid [label .infof.type_label -text "Type" -anchor w] \ -row 3 -column 1 -sticky w -grid [label .infof.type_value -textvariable gfile(type) -anchor w] \ +grid [label .infof.type_value -textvariable ::gfile(type) -anchor w] \ -row 3 -column 2 -sticky w grid [label .infof.npages_label -text "Pages" -anchor w] \ -row 4 -column 1 -sticky w -grid [label .infof.npages_value -textvariable gfile(npages) -anchor w] \ +grid [label .infof.npages_value -textvariable ::gfile(npages) -anchor w] \ -row 4 -column 2 -sticky w grid columnconfigure .infof 1 -weight 1 -pad 2 @@ -593,35 +677,35 @@ pack [frame .optsf] -fill x pack [frame .optsf.gray] -side left -anchor nw pack [label .optsf.gray.l -text "Grayscaling"] -anchor w pack [radiobutton .optsf.gray.off -text "No color conversion" \ - -variable options(gray) -value "color"] -anchor w -pack [radiobutton .optsf.gray.gray -text "Try to grayscale" \ - -variable options(gray) -value "gray"] -anchor w -pack [radiobutton .optsf.gray.gRAY -text "Try harder to grayscale" \ - -variable options(gray) -value "gRAY"] -anchor w + -variable ::options(gray) -value "color"] -anchor w +pack [radiobutton .optsf.gray.gray -text "Grayscale" \ + -variable ::options(gray) -value "gray"] -anchor w +#pack [radiobutton .optsf.gray.gRAY -text "Try harder to grayscale" \ +# -variable ::options(gray) -value "gRAY"] -anchor w # output format pack [label .optsf.format] -side right -anchor ne pack [label .optsf.format.l -text "Output format"] -anchor w pack [radiobutton .optsf.format.pdf -text "pdf" -command set_widget_states \ - -variable options(format) -value "pdf"] -anchor w + -variable ::options(format) -value "pdf"] -anchor w pack [radiobutton .optsf.format.eps -text "eps" -command set_widget_states \ - -variable options(format) -value "eps"] -anchor w + -variable ::options(format) -value "eps"] -anchor w pack [radiobutton .optsf.format.ps -text "ps" -command set_widget_states \ - -variable options(format) -value "ps"] -anchor w + -variable ::options(format) -value "ps"] -anchor w spacing . # boundingbox pack [checkbutton .bbox -text "Compute tight boundingbox" \ - -variable options(bbox) -command set_widget_states] -anchor w + -variable ::options(bbox) -command set_widget_states] -anchor w # page selection pack [frame .pagesf] -fill x pack [radiobutton .pagesf.all -text "Convert all pages" \ - -variable options(pages) -value "all" -command set_widget_states] \ + -variable ::options(pages) -value "all" -command set_widget_states] \ -side left pack [radiobutton .pagesf.single -text "Page:" \ - -variable options(pages) -value "single" -command set_widget_states] \ + -variable ::options(pages) -value "single" -command set_widget_states] \ -side left pack [entry .pagesf.e -width 6 -textvariable ::options(page)] -side left #.pagesf.e configure -vcmd {page_valid %W} -validate focusout \ @@ -633,7 +717,7 @@ spacing . # temp files pack [checkbutton .clean -text "Remove temp files" \ - -variable options(clean)] -anchor w + -variable ::options(clean)] -anchor w proc focusAndFlash {w fg bg {count 9}} { focus $w @@ -708,6 +792,7 @@ proc view {} { proc openDialog {} { set types { + {"PostScript and pdf" {.eps .epi .epsi .ps .prn .pdf}} {"Encapsulated PostScript" {.eps .epi .epsi}} {"General PostScript" {.ps .prn}} {"Pdf" {.pdf}} @@ -722,13 +807,20 @@ proc openDialog {} { set ::gfile(path) [file normalize $try] set ::gfile(dir) [file dirname $::gfile(path)] set ::gfile(name) [file tail $::gfile(path)] - if {[run_epspdf result -i $::gfile(path)]} { + set ::gfile(type) "" + if {! [catch {exec $::texlua $::epspdf_tlu --gui=gui -i $::gfile(path)} \ + result]} { # parse output - regexp { is (\w+)(?: with (\d+) pages)?[\r\n]*$} $result \ + regexp {has type (\w+)(?:.+ (\d+) pages)?\.} $result \ mtc ::gfile(type) ::gfile(npages) if {[regexp {^eps} $::gfile(type)]} {set ::gfile(npages) 1} } - set ::settings(defaultDir) $::gfile(dir) + if {$::gfile(type) eq ""} { + # unsupported type + tk_messageBox -message "$try: unreadable or unsupported type" \ + -title "Error" -icon error + } + set ::settings(default_dir) $::gfile(dir) putsettings } set_widget_states @@ -754,50 +846,55 @@ proc saveDialog {} { set try [string range $try 0 [string last "." $try]] append try $::options(format) } + set try [file normalize $try] # epspdf can read persistent options from configuration. # only options from the options array need to be converted to parameters. - set cmd [list run_epspdf result] + set args [list] if {$::options(gray) eq "gray"} { - lappend cmd "-g" - } elseif {$::options(gray) eq "gRAY"} { - lappend cmd "-G" + lappend args "-g" } - if {$::options(bbox)} {lappend cmd "-b"} - if {! $::options(clean)} {lappend cmd "-d"} + if {$::options(bbox)} {lappend args "-b"} + if {! $::options(clean)} {lappend args "-d"} if {$::options(pages) eq "single"} { if {$::options(page) eq ""} {set ::options(page) 1} - lappend cmd "-p" $::options(page) + lappend args "-p" $::options(page) } - lappend cmd $::gfile(path) - lappend cmd $try - # $cmd/run_epspdf never bombs, but returns 1 (success) or 0 (fail) + lappend args $::gfile(path) $try .status configure -text "Working..." -justify "left" foreach b {view open convert done} { .bottomf.$b configure -state disabled } update idletasks; # force immediate redisplay main window - if {[eval $cmd]} { - # if failure, there has already been an error message - set ::gfile(path) $try - set ::gfile(dir) [file dirname $try] - set ::gfile(type) $::options(format) - set ::gfile(name) [file tail $try] + if {$::ge_85} { + set failed [catch [linsert $args 0 \ + exec -ignorestderr $::texlua $::epspdf_tlu --gui=gui] result] + } else { + set failed [catch [linsert $args 0 \ + exec $::texlua $::epspdf_tlu --gui=gui] result] + } + write_log $result + if {$failed} { + tk_messageBox -icon error -type ok -message "Error; see log window" + } else { set ::gfile(path) [file normalize $try] set ::gfile(dir) [file dirname $::gfile(path)] + set ::gfile(type) $::options(format) set ::gfile(name) [file tail $::gfile(path)] # parse result output regexp { is (\w+)(?: with (\d+) pages)?[\r\n]*$} \ [string range $result [string last "File type of" $result] end] \ mtc ::gfile(type) ::gfile(npages) if {$::gfile(type) eq "eps"} {set ::gfile(npages) 1} - set ::settings(defaultDir) $::gfile(dir) + set ::settings(default_dir) $::gfile(dir) putsettings + set ::options(page) 1 } .status configure -text "" foreach b {view open convert done} { .bottomf.$b configure -state normal } + focus .bottomf.view set_widget_states } } @@ -817,10 +914,15 @@ proc set_widget_states {} { } # convert - if {$::gfile(path) ne "" && [file exists $::gfile(path)] && \ - $::gfile(type) ne "other"} { - .bottomf.convert configure -state normal - } else { + .bottomf.convert configure -state normal + if {$::gfile(path) eq "" || ! [file exists $::gfile(path)]} { \ + .bottomf.convert configure -state disabled + } + if {$::gfile(type) eq "other"} { + .bottomf.convert configure -state disabled + } + if {$::gfile(npages) ne "" && $::options(pages) eq "single" && \ + $::options(page) > $::gfile(npages)} { .bottomf.convert configure -state disabled } @@ -856,6 +958,7 @@ proc set_widget_states {} { } else { .bbox configure -state normal } + update idletasks } set_widget_states diff --git a/Build/source/texk/texlive/linked_scripts/scripts.lst b/Build/source/texk/texlive/linked_scripts/scripts.lst index 17bdd1e736f..65ffe97a923 100644 --- a/Build/source/texk/texlive/linked_scripts/scripts.lst +++ b/Build/source/texk/texlive/linked_scripts/scripts.lst @@ -12,7 +12,7 @@ latexfileversion/latexfileversion listbib/listbib listings-ext/listings-ext.sh ltxfileinfo/ltxfileinfo -lua2dox/lua2dox_lua +lua2dox/lua2dox_filter pdfjam/pdf180 pdfjam/pdf270 pdfjam/pdf90 @@ -44,7 +44,7 @@ de-macro/de-macro dosepsbin/dosepsbin.pl dviasm/dviasm.py ebong/ebong.py -epspdf/epspdf.rb +epspdf/epspdf.tlu epspdf/epspdftk.tcl epstopdf/epstopdf.pl exceltex/exceltex |