summaryrefslogtreecommitdiff
path: root/Master/texmf-dist/scripts/epspdf/epspdf.rb
diff options
context:
space:
mode:
authorSiep Kroonenberg <siepo@cybercomm.nl>2008-07-04 10:17:06 +0000
committerSiep Kroonenberg <siepo@cybercomm.nl>2008-07-04 10:17:06 +0000
commit4a697a53a3c9c2795d549c7d5eb7a6c88c7911a9 (patch)
treeb2f5967ce6865b615213754aa2aa3d41574502bc /Master/texmf-dist/scripts/epspdf/epspdf.rb
parentcdbc5d3bd4d89338a033d47d1368e8e30d273544 (diff)
Added: epspdf conversion utility
git-svn-id: svn://tug.org/texlive/trunk@9222 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/epspdf/epspdf.rb')
-rwxr-xr-xMaster/texmf-dist/scripts/epspdf/epspdf.rb1123
1 files changed, 1123 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.rb b/Master/texmf-dist/scripts/epspdf/epspdf.rb
new file mode 100755
index 00000000000..fb391b6dde6
--- /dev/null
+++ b/Master/texmf-dist/scripts/epspdf/epspdf.rb
@@ -0,0 +1,1123 @@
+#!/usr/bin/env ruby
+
+# epspdf conversion utility, main source
+
+#####
+# Copyright (C) 2006 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.
+
+# `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
+# itself).
+
+# The use of exception handling makes it unnecessary to inspect return
+# values.
+
+# Limitation
+# HiRes boundingboxes are ignored.
+# Two reasons:
+# - they get lost anyhow on conversion to pdf
+# - They are not required so when you find one you have to make sure
+# that it doesn't refer to an included eps. This would
+# require additional logic.
+
+###########################################
+
+# some initialization in case this file is used as main program
+
+if __FILE__ == $0
+
+ # epspdfboot.rb: modify loadpath in a way which works both
+ # with a full Ruby and with a custom subset ruby
+
+ require __FILE__.sub( /epspdf\.rb/, 'epspdfboot.rb' )
+
+ # turn on warnings
+ #$VERBOSE = 1
+
+end # if main program
+
+###########################################
+
+# 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
+
+# system call failed
+class EPSystemError < StandardError; end
+
+###########################################
+
+# PostScript header file for grayscaling
+
+$GRAYHEAD = File.join( File.dirname( __FILE__ ), "makegray.pro" )
+
+###########################################
+
+# handle auto-detected and saved settings
+
+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
+ 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 # |d| (automatic closing)
+ 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" )
+ }
+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
+
+BB_PAT = /^\s*%%BoundingBox:\s*(-?\d+)((\s+-?\d+){3})\s*$/
+BB_END = /^\s*%%BoundingBox:\s*\(\s*atend\s*\)\s*$/
+# for multiline matching:
+BB_PAT1 = /%%BoundingBox:\s*(-?\d+)((\s+-?\d+){3})/m
+BB_END1 = /%%BoundingBox:\s*\(\s*atend\s*\)/m
+
+class Bb
+
+ 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 { |l| l = l.to_i.to_s }
+ end
+
+ def Bb.from_bbcomment( 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 bbcomment
+ fail EPBBError, prn unless valid
+ "%%BoundingBox: #{@llx} #{@lly} #{@urx} #{@ury}"
+ end
+
+end # class Bb
+
+###########################################
+
+# 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 + " -level2sep"
+ end # case
+ else
+ $settings.ps_options
+ end # ifthenelse sep
+ end
+
+ # instance methods ###
+
+ attr_accessor :path, :bb, :type, :npages, :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 )
+ end
+ end # initialize
+
+ ##################
+
+ # debug string for EPCallError
+
+ def buginfo( params = nil )
+ b = "Source was: " + @path + $/
+ b = b + 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). The file is not rewritten.
+
+ def find_bb_simple
+
+ fail EPCallError, buginfo unless @type == 'eps'
+ @bb = nil
+ slurp = ''
+ File.open( @path, $R ) do |fl|
+ slurp = fl.read( [File.size(@path), SAFESIZE].min )
+ end
+ lines = slurp.split( /\r\n?|\n/ )
+ lines.each do |l|
+ if l =~ BB_PAT
+ @bb = Bb.from_bbcomment( l )
+ break
+ end
+ 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.
+ # the eps is always written to a new file.
+
+ def eps_clean
+
+ fail EPCallError, buginfo if @type != 'eps' and @type != 'epsPreview'
+ 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
+
+ # boundingbox unknown and possibly atend
+ @bb, @atfront = 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.
+ # It is not necessary to treat lines and separators differently.
+
+ lines = slurp.split( /(\r\n?|\n)/ )
+ pre_block = ''
+ post_block = ''
+ bb_comment = ''
+ lines.each do |l|
+ pre_block = pre_block + l
+ if l =~ BB_PAT
+ @bb = Bb.from_bbcomment( l )
+ @atfront = true
+ break
+ elsif l =~ BB_END
+ atend = true
+ break
+ end # =~ BB_PAT
+ end # do |l|
+ if 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
+ lines = slurp.split( /(\r\n?|\n)/ )
+ end
+ lines.reverse_each do |l|
+ post_block = l + post_block
+ if l =~ BB_PAT
+ bb_comment = l
+ @bb = Bb.from_bbcomment( bb_comment )
+ break
+ end # if
+ end # do
+ end # if atend
+ fail EPBBError, @path unless @bb.valid
+ retVal = PsPdf.new( 'ext' => 'eps' )
+ if @type == 'eps' and @atfront # simple copy
+ ccp( @path, retVal.path )
+ elsif @atfront # copy a slice
+ sliceFile( @path, retVal.path, ps_len, offset )
+ elsif atend # relocate boundingbox
+ File.open( retVal.path, $W ) do |fl|
+ fl.write( pre_block.sub( BB_END1, bb_comment ))
+ end
+ sliceFile( @path, retVal.path,
+ ps_len - pre_block.length - post_block.length,
+ offset + pre_block.length, $A )
+ File.open( retVal.path, $A ) do |fl|
+ fl.write( post_block.sub( BB_PAT1, '%' ))
+ end
+ end # if atend
+ retVal.bb = @bb.copy
+ retVal.atfront = true
+ return retVal
+
+ end # eps_clean
+
+ ##################
+
+ # Use the Ghostscript bbox device to give an eps a tight boundingbox.
+ # Newer versions of Ghostscript can do that by themselves, but
+ # for the time being we just assume a bbox device.
+ # 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
+ cmd = "#{short_name($settings.gs_prog)} #{PsPdf.gs_options}" +
+ " -sDEVICE=bbox #{short_name(@path)} 2>&1"
+ write_log cmd # if $DEBUG
+ bb_output = `#{cmd}`
+ 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_bbcomment( b )
+ fail EPBBError, bb_output unless bb_temp.valid
+ bb_temp.expand
+ @bb = bb_temp.copy
+ # write a new eps file with a new boundingbox comment
+ ps_len = File.size( @path )
+ slurp = ''
+ File.open( @path, $R ) do |fl|
+ slurp = fl.read( [ps_len,SAFESIZE].min )
+ end
+ lines = slurp.split( /(\r\n?|\n)/ )
+ pre_block = ''
+ pre_length = 0
+ lines.each do |l|
+ pre_length = pre_length + l.length
+ if l =~ BB_PAT
+ pre_block = pre_block + bb_temp.bbcomment
+ oldpath = @path
+ @path = mktemp( 'eps' )
+ File.open( @path, $W ) { |fl| fl.write( pre_block ) }
+ sliceFile( oldpath, @path, \
+ ps_len - pre_length, pre_length, $A )
+ return self
+ else
+ pre_block = pre_block + l
+ end
+ end # do |l|
+ fail EPBBError, bb_output
+ end # if =~ BB_PAT
+ end # do |b|
+ fail EPBBError, bb_output
+
+ end # fix_bb
+
+ ##################
+
+ # Convert eps to pdf.
+ # The eps should already have a boundingbox in the header.
+ # When fix_bbox, let Ghostscript calculate a tight boundingbox.
+ # For this, we require non-negative boundingbox coordinates.
+
+ def eps_to_pdf( params={} )
+
+ gray = params[ 'gray' ]
+
+ fail EPCallError, buginfo( params ) unless @type == 'eps' and @atfront
+
+ wrp = @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 )}" +
+ " -eps -f #{page} -l #{page}" +
+ " \"#{@path}\" \"#{retVal.path}\""
+ else
+ cmd = "\"#{$settings.gs_prog}\" -sDEVICE=epswrite" +
+ " #{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 )
+ retVal.atfront = 1
+ retVal.find_bb_simple
+ 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 \
+ $settings.pdftops_prog and $settings.use_pdftops and @type == 'pdf'
+ retVal = PsPdf.new( 'ext' => 'ps' )
+ cmd = "\"#{$settings.pdftops_prog}\" #{PsPdf.ps_options( sep )}" +
+ ( page ? " -f #{page} -l #{page}" : '' ) +
+ " -paper match \"#{@path}\" \"#{retVal.path}\""
+ write_log cmd # if $DEBUG
+ fail EPSystemError, cmd unless \
+ system( cmd ) and test( ?s, retVal.path )
+ 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.
+
+ gRAY = nil unless \
+ ( $settings.pdftops_prog and $settings.use_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 )
+ pp.pdf_pages if pp.type == 'pdf' and not pp.npages
+
+ 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.
+
+ pp = pp.eps_to_pdf.pdf_to_eps( 'sep' => gray ) \
+ if gRAY or ( bbox and not pp.bb.non_negative )
+
+ case type
+ when 'eps'
+ pp = pp.eps_to_pdf( 'gray' => gray ).pdf_to_eps if gray
+ pp = pp.fix_bb if bbox
+ return pp
+
+ when 'pdf'
+ pp = pp.fix_bb if bbox
+ return pp.eps_to_pdf( 'gray' => gray )
+
+ 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( 'sep' => gray )
+ pp = pp.pdf_to_eps( 'page' => page )
+ pp = pp.fix_bb
+ return pp = pp.eps_to_pdf( 'gray' => gray )
+ elsif page
+ pp = pp.ps_to_pdf( 'sep' => gray )
+ pp = pp.pdf_to_ps( '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
+
+ fail "Unsupported conversion"
+
+ end # any_to_any
+
+end # class PsPdf
+
+#################################
+# main program when not called from GUI
+
+if __FILE__ == $0
+
+require 'optparse'
+
+# create a pause to examine temp files
+def abortt( msg )
+ if $DEBUG
+ $stderr.puts msg + "\nPress <enter> to finish"
+ $stdin.gets
+ end
+ fail
+end
+
+begin # outer rescue block
+
+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( "-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( "-T TARGET", "--target=TARGET", PDF_TARGETS,
+ "Target use of pdf; one of",
+ "#{PDF_TARGETS.join( ', ' )}" ) do |opt|
+ $settings.pdf_target = opt
+ end
+
+ opts.on( "-V VERSION", "--version=VERSION", PDF_VERSIONS,
+ "Pdf version to be generated;",
+ "one of #{PDF_VERSIONS.join( ', ' )}" ) do |opt|
+ $settings.pdf_version = opt
+ 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 -level2,",
+ "don't include -eps or page number options;",
+ "these will be generated by the program" ) do |opt|
+ $settings.ps_options = opt
+ 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( "-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;",
+ "success still not guaranteed" ) do |opt|
+ $options.gRAY = true
+ 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.separator ""
+
+ opts.on_tail("-h", "--help", "Show this message") do
+ puts opts
+ exit
+ end
+
+ # opts.on_tail("--version", "Show version") do
+ # puts OptionParser::Version.join('.')
+ # exit
+ # end
+end # opts
+
+# parse options destructively
+
+opts.parse!( ARGV )
+
+$options.page = 1 if $options.bbox and not $options.page
+
+$settings.write_settings if save_settings
+
+if ARGV.length < 1
+ puts opts if not save_settings # help output
+ # puts "Settings:"
+ # $settings.each_key do |k|
+ # puts "key #{k} value #{$settings[k].val}"
+ # end
+ abort
+elsif $options.info
+ puts "File type of #{ARGV[0]} is #{PsPdf.new( 'file' => ARGV[0] ).type}"
+ 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 )
+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
+ fail StandardError
+rescue EPBBError => exc
+ mess = "Boundingbox problem" + $/ +
+ exc.message + $/ + exc.backtrace.join( $/ )
+ write_log( mess )
+ puts mess
+ fail StandardError
+rescue EPSystemError => exc
+ mess = "Problem with system call" + $/ +
+ exc.message + $/ + exc.backtrace.join( $/ )
+ write_log( mess )
+ puts mess
+ fail StandardError
+end # rescue block
+
+ccp( pp.path, outfile )
+
+cleantemp unless $DEBUG == 1
+
+rescue => detail
+write_log detail.backtrace.join( $/ )
+puts detail.backtrace.join( $/ )
+exit
+end # outer rescue block
+
+end # if called as main program
+__END__