diff options
author | Siep Kroonenberg <siepo@cybercomm.nl> | 2013-02-07 21:36:03 +0000 |
---|---|---|
committer | Siep Kroonenberg <siepo@cybercomm.nl> | 2013-02-07 21:36:03 +0000 |
commit | cb3b7cca1b0b066646b30f90ba02f0e61c30dd13 (patch) | |
tree | 5c05c238a1d88a233b24f16b7938f47371e65ca5 /Master/texmf-dist/scripts/epspdf | |
parent | d4e04448079e0962ac0d665e41a7a50168c662a0 (diff) |
Epspdf updated to 0.6
git-svn-id: svn://tug.org/texlive/trunk@29050 c570f23f-e606-0410-a88d-b1316a301751
Diffstat (limited to 'Master/texmf-dist/scripts/epspdf')
-rw-r--r-- | Master/texmf-dist/scripts/epspdf/epspdf.help | 283 | ||||
-rw-r--r-- | Master/texmf-dist/scripts/epspdf/epspdf.icns | bin | 80465 -> 0 bytes | |||
-rw-r--r-- | Master/texmf-dist/scripts/epspdf/epspdf.ico | bin | 57022 -> 9662 bytes | |||
-rwxr-xr-x | Master/texmf-dist/scripts/epspdf/epspdf.rb | 1479 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/epspdf/epspdf.tlu | 2703 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/epspdf/epspdfrc.rb | 486 | ||||
-rwxr-xr-x | Master/texmf-dist/scripts/epspdf/epspdftk.tcl | 593 | ||||
-rw-r--r-- | Master/texmf-dist/scripts/epspdf/makegray.pro | 33 |
8 files changed, 3186 insertions, 2391 deletions
diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.help b/Master/texmf-dist/scripts/epspdf/epspdf.help index a1f71180989..9b43bc63c1b 100644 --- a/Master/texmf-dist/scripts/epspdf/epspdf.help +++ b/Master/texmf-dist/scripts/epspdf/epspdf.help @@ -1,15 +1,32 @@ Epspdf and epsdftk ****************** -This manual is for epspdf, version 0.5.3. +This manual is for epspdf, version 0.6.0. -Copyright (C) 2006, 2008, 2009, 2010, 2011 Siep Kroonenberg. +Copyright (C) 2006, 2008, 2009, 2010, 2011, 2013 Siep Kroonenberg. Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and this notice are preserved. This file is offered as-is, without any warranty. +What has changed? +***************** + +Improvements: + * Grayscaling should now also work on bitmapped images. + + * Revised multistep conversions, with less risk of losing high-level + structure + + * Rewritten in TeXLua, which is a component of both TeX Live and of + MikTeX. Command-line use no longer requires an external scripting + language. + +I have been testing with Ghostscript version 9.05 and 9.06. + +For more details, see *note Version 0.6::. + 1 Usage ******* @@ -29,8 +46,7 @@ in the box in the upper part of the screen. 1.2 Viewing =========== -The View button at the bottom of the screen calls an appropriate -external viewer. +The View button at the bottom of the screen calls an external viewer. Windows and OS X: Epspdftk simply tries to use the default Open command. @@ -43,26 +59,22 @@ for the current file. 1.3 Conversion options ====================== -Grayscaling: Simple color figures can be converted to grayscale. -Bitmapped data won't be converted. If grayscaling doesn't work even on -non-bitmapped data, select the "Try harder to grayscale" option. This -may cause an extra pdf-PostScript roundtrip, but grayscaling may still -fail. +Grayscaling: Grayscaling is now done by Ghostscript itself and should +"just work". Compute tight boundingbox: This option is only available if a single page is converted. Page selection: The only possibilities are selecting a single page or -selecting all pages. When converting to eps you are restricted to a -single page. +selecting all pages. Converting to eps implies selecting a single page. For general PostScript files, there is no quick way to determine the number of pages, so the program may not check beforehand whether you picked an existing page. If you don't like that, convert the entire document to pdf first - which will be done behind the scenes anyhow. -Specifying options such as grayscaling or page selection may require -several roundtrips between PostScript or eps and pdf. +Specifying options such as grayscaling or page selection may require a +multistep conversion. 1.4 Converting ============== @@ -92,16 +104,26 @@ configured outside epspdf. 1.5.2 Options for converting to pdf ----------------------------------- -Double-check the setting "Target use" under "Conversion to pdf". Choose -"prepress" for pdfs which are target for commercial printing. This -option will try to embed all fonts, among other things. Often, -printshops insist on this. With luck, Ghostscript fonts and system -fonts will be used for fonts which were not originally embedded. +Double-check the setting "Target use" under "Conversion to pdf". +"prepress" is for pdfs which are going to be printed commercially. The +options prepress, printer and default will try to embed all fonts. +Often, printshops insist on this. With luck, Ghostscript fonts and +system fonts will be used for fonts which were not originally embedded. On the other hand, you may prefer "screen" if file size is a concern. See also the Ghostscript documentation, in particular Use.htm and Ps2pdf.htm. +Specifying anything other than "default" may cause an additional +conversion step and possibly also loss of high-level structure. + +As to pdf versions: this is a trade-off between more features for higher +versions and better compatibility for lower versions - although even +version 1.4 is far from bleeding-edge. Converting to a lower version +may lead to rasterization of vector data and (much) larger file sizes. +For prepress use, the printshop or publisher may require a low version +number but otherwise leave this at "default". + 1.5.3 Options for converting to PostScript ------------------------------------------ @@ -110,41 +132,22 @@ is available. In this case, the conversion will be done by Ghostscript. Characters may be converted to drawn shapes or little bitmaps so this is not recommended if the file uses fonts. -Under Windows, this is also the place to point the program to the -location of pdftops.exe. - -1.5.4 Hires BoundingBox ------------------------ - -Uncheck the option "Use hires boundingbox if possible" if your -conversions come out slightly clipped. It has two effects: - * When converting to pdf, it uses the standard rather than the hires - boundingbox for page dimensions - - * When computing a boundingbox, it adds a small safety margin of 1pt - to the standard boundingbox, except where it would make any - boundingbox coordinate negative. +Under Windows and MikTeX, this is also the place to point the program to +the location of pdftops.exe. 2 Command-line usage ******************** -epspdf.rb is the backend of epspdftk, but it can also be used +epspdf.tlu is the backend of epspdftk, but it can also be used standalone. It shares configuration settings with epspdftk.tcl. -Under Windows, is strongly recommended to invoke epspdf.rb via a -wrapper script epspdf.bat on the search path, e.g. - "PATH_TO_RUBY.EXE" "PATH_TO_EPSPDF.RB" %* - -The TeX Live package for epspdf already includes wrappers for Windows, -both for epspdf and for epspdftk. +Below, we assume that there is a suitable wrapper or symlink for epspdf +on your searchpath. This is the case if you installed epspdf as a TeX +Live package. The first parameter of epspdftk is interpreted as startup directory for the file browser. -From version 0.5.2 on, there is no longer need for a wrapper script -under Unix/Linux/Mac OS X; a symlink on the searchpath to the actual -script suffices. - Basic usage: epspdf [OPTIONS] INFILE OUTFILE @@ -154,41 +157,31 @@ Basic usage: Typing `epspdf --help' gives you the following summary: $ epspdf --help - Epspdf version 0.5.3 - Copyright (C) 2006, 2008, 2009, 2010, 2011 Siep Kroonenberg - Convert between [e]ps and pdf formats - Usage: epspdf.rb [options] infile [outfile] + Epspdf version 0.6.0 + Copyright (c) 2006, 2008, 2009, 2010, 2011, 2013 Siep Kroonenberg + 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 - -g, --gray, --grey Convert to grayscale; - success not guaranteed - -G, --GRAY, --GREY Try harder to convert to grayscale - -p, --pagenumber=PAGENUMBER Page to be converted or selected - -b, --bbox, --BoundingBox Compute tight boundingbox - -n, --no-hires Don't use hires boundingbox - -r, --hires Use hires boundingbox - -T, --target=TARGET Target use of pdf; one of - default, printer, prepress, screen, ebook - -N, --pdfversion=PDFVERSION Pdf version to be generated - -I Ignore pdftops even if available - (default: use if available) - -U Use pdftops if available - (overrides previous -I setting) - -C, --custom=CUSTOMOPTIONS Custom options for conversion to pdf, - view Use.htm and ps2pdf.htm from - the Ghostscript documentation set - -P, --psoptions=PSOPTIONS Options for pdftops; default -level3, - don't include -eps or page number options; - these will be generated by the program - -i, --info Info: display detected filetype - -s Save (some) settings - -d Debug: don't remove temp files - --gui[=ACTION] Do not use; reserved for GUI - - -v, --version Prints version info - -h, --help Show this message + -p, --page, --pagenumber PNUM + Page number; must be a positive integer + -g, --grey, --gray, -G, --GREY, --GRAY + Convert to grayscale + -b, --bbox, --BoundingBox + Compute tight boundingbox + -T, --target TARGET + One of screen, ebook, printer, prepress or default + -N, --pdfversion VERSION + One of 1.2, 1.3, 1.4 or default + -U Use pdftops if available + -I Reverses the above + -s, --save Save some settings to configuration file + -d Debug: do not remove temp files + -v, --version + Display version info and exit + -h, --help Display this help message and exit 3 Notes on PostScript and pdf ***************************** @@ -224,17 +217,10 @@ resolution. ================== When converting to pdf, Ghostscript handles font embedding differently -depending on the "Target use" option. For prepress, it embeds even the -standard fonts, including Times etc. Ghostscript has copies of these -fonts in case the PostScript file doesn't have them. For screen, it -doesn't embed standard fonts. - -You can embed fonts after the fact by converting pdf to eps or ps, and -back to pdf, with target use set to "prepress". However, don't try this -unless you have pdftops. - -If target use is set to "screen", then standard fonts will be removed -instead of added. This option may also cause downsampling of bitmaps. +depending on the "Target use" option. According to the Ghostscript +documentation, it embeds all fonts without exception for prepress- and +printer settings. Epspdf enforces this also for default settings. For +screen- and ebook settings, standard fonts such as Times may be omitted. 3.3 Eps preview headers ======================= @@ -252,14 +238,13 @@ file may have a mediabox, a trimbox and various other boxes. Ghostscript by itself normally converts the PostScript page - which is anchored at (0,0) - to the pdf mediabox. -When epspdf or a similar program converts eps to pdf, it adds code to -move the bottom left corner of the graphic to (0,0) and to set the page -to the size of the graphic. Anything outside the boundingbox should be -cut off. +Conversion from eps to pdf usually translates the bottom-left corner to +the (0,0) origin and sets the page to the size of the graphic. Anything +outside the boundingbox should be cut off. -Both Ghostscript and pdftops have some parameters for setting or using -these boxes; consult Ps2pdf.htm from the Ghostscript documentation and -"pdftops -help" or "man pdftops". +I have seen Illustrator-generated pdf files with negative coordinates +for the lower-left corner. However, this does not seem to cause +problems. 3.5 Orientation =============== @@ -267,26 +252,21 @@ these boxes; consult Ps2pdf.htm from the Ghostscript documentation and There appears to be no reliable way to set orientation for PostScript- and pdf files. This may result part of a graphic being cut off after conversion or even everything falling outside the page / mediabox / -boundingbox. It may help to change the extension from ".eps" to ".ps" -before converting to pdf. In this case, let epspdf generate a new boundingbox. -3.6 Exporting PostScript from Windows programs -============================================== +3.6 Exporting PostScript or pdf from Windows programs +===================================================== -Often, the only way to get EPS or PostScript from a Windows program is -by "printing" to a PostScript file. From some programs, you can print a -selection. A suitable driver which comes with Windows is Generic / MS -Publisher Imagesetter. Pay attention to the printer properties: choose -"Outline" for font downloading and avoid the "Optimize for speed" -setting for PostScript Output Option. Try e.g. "Archive" instead. These -options can be found under the Advanced button. +Microsoft Office 2010 can now export to pdf, either the entire document +or a selection. This removes a major headache for Windows users. - Other possibilities are the TpX and wmf2eps programs, which both have -options to write clipboard contents to an emf file. Wmf2eps uses a -printer driver in the background. For faithful conversion, pick -wmf2eps; for subsequent editing, choose TpX. Both programs are -available from `http://mirror.ctan.org/'. +As a last resort, you can "print" to a PostScript file. From some +programs, you can print a selection. A suitable driver which comes with +Windows is Generic / MS Publisher Imagesetter. Pay attention to the +printer properties: choose "Outline" for font downloading and avoid the +"Optimize for speed" setting for PostScript Output Option. In my tests, +"Encapsulated PostScript" did not look very promising either. Try e.g. +"Archive" instead. These options can be found under the Advanced button. 4 Bitmapped graphics for LaTeX and pdflatex ******************************************* @@ -311,13 +291,14 @@ _ImageMagick/convert_ _The GIMP_ This is the premier open source image editing program. It is often pre-installed on Linux, and is also available for other platforms. - The GIMP can save in eps format. + The GIMP can save in eps- and pdf format. 4.2 Mac OS X ============ Mac OS X's built-in Preview application can read most bitmapped formats -and save them as .pdf, which epspdf can convert to .eps. +and save them as pdf or PostScript. In fact, in many cases it is an +excellent alternative to epspdf. 4.3 Windows =========== @@ -331,49 +312,36 @@ sam2p is distributed with TeX Live (Windows only), bmeps both with TeX Live and with MikTeX. Without the `-c' option, bmeps produces a grayscale image. It produces larger files than sam2p. -4.3.1 GUI use of sam2p and bmeps --------------------------------- - -Create a batchfile `bitmap2eps.bat' with contents -sam2p "%~1" "%~dpn1.eps" - (TeX Live) or -bmeps -c "%~1" "%~dpn1.eps" - (MikTeX) and place it _e.g._ on your desktop. Now if you drop a -bitmapped graphic on this batchfile then you will get an eps file with -the same name as the original, but with extension `.eps'. +With TeX Live, you can convert to eps by right-clicking an image in +Windows Explorer and "open" with _bitmap2eps_, which uses sam2p or +bmeps in the background. 5 Troubleshooting ***************** -5.1 Grayscaling fails -===================== - -See above, under *note Grayscaling: gray. Epspdf is limited by what can -be done by the backend tools, i.e. Ghostscript and pdftops. In -particular, epspdf cannot grayscale bitmapped data. - -5.2 Part of the graphic gets cut off +5.1 Part of the graphic gets cut off ==================================== -Things to try: - * If the PostScript file was generated with the Windows PostScript - driver, experiment with the PostScript Output option. Don't choose - Optimize for Speed. +If the PostScript file was generated with the Windows PostScript driver, +experiment with the PostScript Output option. Don't choose Optimize for +Speed. + +5.2 Fonts look ugly +=================== - * Replace the extension with ".ps" and let epspdf generate a - boundingbox. +If Ghostscript has to do the conversion from pdf to ps then text may not +remain text, but may be replaced by bitmaps. Try to get hold of a more +recent Ghostscript and of either the xpdf suite or the Poppler +utilities, both of which include pdftops. - * Boundingbox calculations may be slightly inaccurate where text is - involved. If just a sliver is cut off, let epspdf ignore the - high-resolutuion boundingbox. Command-line option: "-n" or - "-no-hires". +5.3 The page has been converted to a bitmap +=========================================== -5.3 Fonts look ugly -=================== +This may happen if the page contains features such as transparency which +are not supported by intermediate formats. -If Ghostscript has to do the conversion from pdf to ps then text will -not remain text, but will be replaced by drawn shapes or bitmaps. Try -to get hold of the xpdf suite, which includes pdftops. +Set pdf target use and target version both to "default" to avoid +unnecessary conversions: `-T default -N default' 5.4 Resources for troubleshooting ================================= @@ -387,9 +355,7 @@ a subdirectory epspdf of `%APPDATA%'. This APPDATA directory may be prompt and type `echo %APPDATA%', which will produce this information. The logfile lists all epspdf calls and all Ghostscript- and pdftops -calls plus error information. "EPCallError" is probably a program -error. 'EPSystemError" is more likely caused by a faulty file, but may -be a program error as well. +calls plus error information. Temporary files. The temporary files may give clues as well. Uncheck the button "Remove temp files", or for the command-line version, give a @@ -404,7 +370,28 @@ file pdftops.txt in the distribution zip. 6 Change history **************** -6.1 Version 0.5 +6.1 Version 0.6 +=============== + +The command-line backend component has been rewritten in texlua and +therefore no longer needs an external scripting language. + +Grayscaling is now done by Ghostscript's color options for pdf output. +This also works for bitmaps. + +Croppping of pdfs is now accomplished by running luatex on a suitable +wrapper file (same method as Heiko Oberdiek's pdfcrop). Such a +conversion preserves advanced features which might otherwise get lost +during a PostScript round-trip. + +I no longer own a Mac, and therefore no longer try to provide an +AppleScript wrapper. + +The current version has no provisions for custom Ghostscript- or pdftops +parameters. I may of may not re-add those in a future update. The +corresponding command-line options are accepted but have no effect. + +6.2 Version 0.5 =============== The GUI has been rewritten in Tcl/Tk, removing the dependence on the @@ -438,7 +425,7 @@ the desired pdf version. The "-info" option now also prints the number of pages for pdf files. -6.2 Version 0.4 +6.3 Version 0.4 =============== Hi-res boundingboxes are now supported. By default, conversion from eps diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.icns b/Master/texmf-dist/scripts/epspdf/epspdf.icns Binary files differdeleted file mode 100644 index 183cf660ab4..00000000000 --- a/Master/texmf-dist/scripts/epspdf/epspdf.icns +++ /dev/null diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.ico b/Master/texmf-dist/scripts/epspdf/epspdf.ico Binary files differindex adab1a8d565..be136986560 100644 --- a/Master/texmf-dist/scripts/epspdf/epspdf.ico +++ b/Master/texmf-dist/scripts/epspdf/epspdf.ico diff --git a/Master/texmf-dist/scripts/epspdf/epspdf.rb b/Master/texmf-dist/scripts/epspdf/epspdf.rb deleted file mode 100755 index 47a0c1f65b9..00000000000 --- a/Master/texmf-dist/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/Master/texmf-dist/scripts/epspdf/epspdf.tlu b/Master/texmf-dist/scripts/epspdf/epspdf.tlu new file mode 100755 index 00000000000..ae793d7f344 --- /dev/null +++ b/Master/texmf-dist/scripts/epspdf/epspdf.tlu @@ -0,0 +1,2703 @@ +#!/usr/bin/env texlua + +kpse.set_program_name('texlua') + +-- epspdf conversion utility + +-- First texlua version + +ep_version = '0.6.0' +ep_copyright = '2006, 2008, 2009, 2010, 2011, 2013' + +--[[ + +TeX code for cropping pdfs adapted from Heiko Oberdiek's pdfcrop utility + +Program structure +SETUP +- some globals +- utilities +- system info +- some infrastructure - logging, temp files +- initializing (persistent) settings and associated utilities +- initializing (transient) options +MAIN FUNCTIONS/METHODS +- boundingboxes and their methods +- PsPdf objects: + - globals + - identify function + - one-step conversion methods + - any_to_any method +INITIALIZATION +- parsing and interpreting rc file +- parsing and interpreting command-line +- non-conversion runs +- start of logging and creation of temp directory +CONVERSION +- call any_to_any + +TODO + +- duplicating epstopdf options +- use epdf library only optionally +- custom options for gs and pdftops + +Use absolute, normalized names for gs_prog and pdftops_prog but use +input- and output files as-is. + +MAYBE NOT NEEDED + +We can probably dispense with [hr]bb:wrapper() +--]] + +-- some general utilities and globals --------------------------- + +--[[ + +I think we get by just fine with simple-minded error handling. At +most, we just call a function which tries to first write the error +message to log before re-raising the error. + +The gui can capture error messages if necessary. + +--]] + +eol = nil +path_sep = nil +if os.type=='unix' then + eol='\n' + path_sep = ':' +else + eol='\r\n' + path_sep = ';' +end + +-- whether epspdf is run from the epspsdtk gui + +from_gui = false + +-- error- and debug + +function errror(mess) + if logfile then pcall(write_log, mess) end + -- ignore result of pcall: we can do nothing about failure + error(mess, 2) +end + +function dbg(mess) + if options.debug then + if logfile then write_log(mess) end + print(mess) + end +end + +--[[ + +function dbg_opt() + if options.debug then + local mess = '' + for _, k in ipairs({'bbox', 'gray', 'page'}) do + mess = mess.. ' ' .. k .. ': ' .. tostring(options.k) + end + dbg(mess) + end +end + +--]] + +function ep_shortname(path) + if os.type=='unix' then + return path + else + -- shortname appears not to work under miktex + -- so return original path as a fallback + local sp = lfs.shortname(path) + return sp or path + end +end + +function fw(path) + if os.type=='windows' then + return string.gsub(path, '\\', '/') + else + return path + end +end + +cwd = fw(lfs.currentdir()) +source_dir = false -- directory of input file; to be determined +dest_dir = false -- directory of output file; to be determined + +function absolute_path(path, reldir) + + --[[ Return absolute normalized version of path, interpreted + from the directory from where the program was called. + If reldir, then interpret path from reldir instead. + + We use the fact that lfs.currentdir() always returns an absolute and + normalized path. So we go to the parent directory of path, ask for + the current directory and then combine the current directory with + the base filename. + + The function returns nil if there is no valid parent path. + This might be an issue if path is a directory, + but we shall apply this function only on files. + It is ok if path itself does not exist. --]] + + path = fw(path) + + local present_dir = lfs.currentdir() + + lfs.chdir(cwd) + + if reldir then + if not lfs.chdir(reldir) then return nil end + end + + local parentdir + local filename + + if string.match(path, '/') then + parentdir, filename = string.match(path,'^(.*)/([^/]*)$') + if parentdir=='' then + parentdir = '/' + -- on unix, this is an absolute path. on windows, it is not + if os.type=='windows' then + lfs.chdir('/') + parentdir = fw(lfs.currentdir()) + end + elseif os.type=='windows' and string.match(parentdir,'^[a-zA-Z]:$') then + parentdir = string.sub(parentdir,1,2)..'/' + else + if not lfs.chdir(parentdir) then + parentdir = nil + else + parentdir = fw(lfs.currentdir()) + end + end + elseif os.type=='windows' and string.match(path,'^[a-zA-Z]:') then + -- windows: d:file + parentdir = string.sub(path,1,2) + if not lfs.chdir(parentdir) then + parentdir = nil + else + parentdir = fw(lfs.currentdir()) + filename = string.sub(path,3) + end + else + parentdir = fw(lfs.currentdir()) + filename = path + end + lfs.chdir(present_dir) + if not parentdir then + return nil + elseif string.sub(parentdir,-1)=='/' then + return parentdir..filename, parentdir + else + return parentdir..'/'..filename, parentdir + end +end -- absolute_path + +-- check whether prog is on the searchpath. +-- we need it only under unix, +-- so we save ourselves the trouble of accommodating windows. +-- we return the full path, although we only need a yes or no answer + +function find_on_path (prog) + if os.type ~= 'unix' then + errror('find_on_path: this is a unix-only function') + end + for d in string.gmatch(os.getenv('PATH'), '[^:]+') do + if lfs.isfile(d..'/'..prog) then + return absolute_path(d..'/'..prog) + end + end + return false +end -- find_on_path + +-- OTOH, on windows we do not rely so much on the searchpath +-- so we just test whether the file exists and is an exe file. +-- only used for pdftops. + +function is_prog (path) + -- 1. test for and if necessary add extension + -- 2. test for existence + -- 3. returns either false or absolute path + if os.type ~= 'windows' then + errror('is_prog: this is a Windows-only function') + end + if not path then + return false + end + if not string.lower(string.sub(path,-4,-1))=='.exe' then + path = path..'.exe' + end + path = absolute_path(path) + if not (path and lfs.isfile(path)) then + return false + else + return path + end +end -- is_prog + +-- check whether el occurs in array lst +function in_list (el, lst) + if not lst then return false end + for _,p in ipairs(lst) do + if el == p then + return true + end + end + return false +end -- in_list + +-- remove leading and trailing, but not embedded spaces +function strip_outer_spaces(s) + s = string.gsub(s, '%s*$', '') + s = string.gsub(s, '^%s*', '') + return s +end -- strip_outer_spaces + +function join(t, sep, lastsep) + -- there is a table function concat which does this, + -- but without optional different lastsep + if t==nil or #t<1 then return '' end -- or should we return nil? + local s = t[1] + for i=2,#t do -- ok if #t<2 + if i==#t and lastsep then + s = s .. lastsep .. t[i] + else + s = s .. sep .. t[i] + end + end + return s +end -- join + +-- combine several tables into one. +-- the parameter is a table of tables. +function tab_combine (t) + local res = {} + for _,tt in ipairs(t) do + for __, ttt in ipairs(tt) do + table.insert(res, ttt) + end + end + return res +end -- tab_combine + +-- Copy a file in chunks, with optional length and offset. +-- Since files may be very large, we copy them piecemeal. +-- An initial chunk of size bufsize should be plenty to include +-- any interesting header information. + +bufsize=16000 + +function slice_file(source, dest, len, offset, mode) + -- The final three parameters can be independently left out by + -- specifying false as value + -- Assume caller ensured parameters of correct type. + -- We do not allow negative offsets. + local sz = lfs.attributes(source).size + if not offset then + offset = 0 + elseif offset>sz then + offset = sz + end + if not len or len>sz-offset then + len = sz - offset + end + if not mode then mode = 'wb' end + -- dbg('copying '..len..' bytes of '..source..' to '..dest..' from '..offset + -- ..' in '..mode..' mode') + local buffer='' + local s=io.open(source, 'rb') + s:seek('set', offset) + local copied = 0 + local d=io.open(dest, mode) + if not d then errror('slice_file: failed to copy to '..dest) end + local slen = len + while slen>0 do + if slen>=bufsize then + buffer = s:read(bufsize) + slen = slen - bufsize + else + buffer = s:read(slen) + slen = 0 + end + if not d:write(buffer) then + errror('slice_file: failed to copy to '..dest) + end + end + s:close() + d:close() +end -- slice_file + +-- system info -------------------------------------------- + +-- safe mode? TODO +options = {safer = string.match(arg[0], 'repspdf')} + +-- Windows: miktex, TL or neither +-- no support yet for separate ghostscript +is_miktex = false +is_tl_w = false +if os.type == 'windows' then + if string.find (string.lower(kpse.version()), 'miktex') then + is_miktex = true + else + local rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '') + if not rt then + errror('Unrecognized TeX directory structure', 0) + elseif lfs.isfile(rt..'/release-texlive.txt') then + --[[ + -- TL version is easy to determine but is not needed + local fin = io:open(rt..'release-texlive.txt', 'r') + if fin then + local l = fin:read('*line') + tl_ver = string.match(l, 'version%s+(%d+)$') + if tl_ver then tl_ver = tonumber(tl_ver) end + end -- if fin + --]] + is_tl_w = true + else + errror('Not MikTeX and no file ' .. rt .. + '/release-texlive.txt; TeX installation not supported.', 0) + end -- if isfile + end -- if not miktex +end -- if windows + +-- without Ghostscript we are dead in the water +gs_prog = false +do + local rt='' + if os.type == 'unix' then + if find_on_path('gs') then + gs_prog = 'gs' + else + error('No ghostscript on searchpath!', 0) + end + elseif is_miktex then + -- gs_prog = fw(os.selfdir)..'/mgs.exe' + gs_prog = 'mgs.exe' + rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '') + if not lfs.isdir(rt..'/miktex') then + -- 64-bits: binaries one level deeper + rt = string.gsub(rt, '[\\/][^\\/]+$', '') + end + if rt=='' then errror('Unexpected MiKTeX directory layout') end + if not lfs.isdir(rt..'/miktex') then + errror('Unexpected MiKTeX directory layout') + end + os.setenv('MIKTEX_GS_LIB', rt..'/ghostscript/base;'..rt..'/fonts') + elseif is_tl_w then + -- windows/TeX Live + -- grandparent of texlua.exe directory .. ... + rt = string.gsub(os.selfdir,'[\\/][^\\/]+[\\/][^\\/]+$', '') + ..'/tlpkg/tlgs' + os.setenv('GS_LIB', rt..'/lib;'..rt..'/fonts') + os.setenv('Path', rt..'/bin'..';'..os.getenv('Path')) + gs_prog = 'gswin32c.exe' + else + errror('Only TeX Live and MikTeX supported!', 0) + end +end -- do + +-- directory for configuration and log +epsdir = '' +if os.type == 'windows' then + epsdir = fw(ep_shortname(os.getenv('APPDATA'))) .. '/epspdf' +else + epsdir = os.getenv('HOME')..'/.epspdf' +end +-- dbg('epsdir: '..epsdir) +rcfile = epsdir .. '/config' + +-- create epsdir if necessary +if lfs.isfile(epsdir) then + error('Cannot continue; epspdf directory ' .. epsdir .. ' is a file') +elseif not lfs.isdir(epsdir) then + if not lfs.mkdir(epsdir) then + error('Failed to create epspdf directory ' .. epsdir) + end +end + +-- log and log rotation + +logfile = epsdir .. '/epspdf.log' +log_bsl = string.gsub(logfile, '/', '\\') +oldlog = epsdir .. '/epspdf.log.old' + +-- tag log entries with one random integer per epspdf run, +-- in the absence of a lua process id built-in function + +logtag = math.random(0,999999) -- range is inclusive +logtag = string.format('%06d', logtag) + +-- we open and close the logfile anew for each write. +-- failure to open constitutes no error. +function write_log(s) + local f = io.open(logfile, 'a') + if f then + f:write(string.format('%s %s%s', + os.date('%Y/%m/%d %H:%M:%S', os.time()), s, eol)) + f:close() + end + if from_gui then + print(s) -- intercepted by the gui + end +end + +function log_cmd(cmd) + write_log('[' .. table.concat(cmd, '] [') .. ']') +end + +-- temporary files ---------------------------------------- + +tempdir = false -- will be created later +tempfiles = {} + +-- We just name our temporary files nn.<ext> with successive nn. +-- We cannot exclude that another process uses our tempdir +-- so we have to first check for each new file whether it already exists. +-- Note: epspdf does all the real work from this temp directory. + +function mktemp(ext) + local froot, fname, f, g + for i=0,99 do + froot = string.format('%02d.', i) + fname = froot..ext + -- dbg('New temp file '..fname..'?') + if ext~='tex' then + if not lfs.isfile(fname) then + -- dbg(fname..' available') + f = io.open(fname, 'wb') + if not f then + errror('Cannot create temporary file '..fname) + end + f:close() + table.insert(tempfiles, fname) + return froot..ext -- no need to record pdf name + end + else + -- tex; we also need a pdf + if not lfs.isfile(fname) and not lfs.isfile(froot..'pdf') then + local f = io.open(fname, 'wb') + if not f then + errror('Cannot create temporary file '..fname) + end + f:close() + table.insert(tempfiles, fname) + fname = froot..'pdf' + g = io.open(fname, 'wb') + if not g then + errror('Cannot create temporary file '..fname) + end + g:close() + table.insert(tempfiles, fname) + table.insert(tempfiles, froot..'log') + return froot..ext -- no need to record pdf name + end + end -- if + end -- for + errror('Cannot create temporary file in '..tempdir) +end + +function cleantemp() + lfs.chdir(tempdir) + for _,f in ipairs(tempfiles) do + if lfs.isfile(f) then + local success, mess = os.remove(f) + if not success then write_log(mess) end + end + end + local empty = true + for f in lfs.dir('.') do + if f ~= '.' and f ~= '..' then + empty = false + write_log('Temp dir '..tempdir..' contains '..f..' therefore not removed') + break + end + end + lfs.chdir('..') + if empty then + local res, mess + res, mess = lfs.rmdir(tempdir) + if not res then + write_log('Failed to remove empty '..tempdir..'\n'..mess) + end + end +end + +--[[ + +settings + +Now: +1. initial values +Later: +2. try to read config file +3. command-line option parsing, including settings that are not stored + +The values in the settings array have lowest priority - lower than +autodetect and command-line options. We go for false rather than +undefined, because this results in an actual settings entry. +We ignore illegal settings in the config file. + +--]] + +pdf_targets = {'screen', 'ebook', 'printer', 'prepress', 'default'} +pdf_versions = {'1.2', '1.3', '1.4', 'default'} + +settings = {} +descriptions = {} + +settings.pdf_target = 'default' +descriptions.pdf_target = 'One of ' .. join(pdf_targets, ', ', ' or ') + +settings.pdf_version = 'default' +descriptions.pdf_version = 'One of ' .. join(pdf_versions, ', ', ' or ') + +--[[ +-- is bb_spread still a useful setting? +-- look at gs options wrt boundingbox +-- settings.bb_spread = 1 +-- descriptions.bb_spread = 'Safety margin in points for (low-res) boundingbox' + +settings.use_hires_bb = false +-- descriptions.use_hires_bb = 'Use high-resolution boundingbox if available' +-- Ignored; hires bb always used +--]] + +-- because pdftops_prog is sometimes configurable, it is stored in settings. +-- it will not be used for TeX Live and only be read and written on Windows. +settings.pdftops_prog = false +--[[ +if os.type == 'unix' then + settings.pdftops_prog = find_on_path('pdftops') +elseif os.type == 'windows' and not is_miktex then + settings.pdftops_prog = os.selfdir..'/pdftops.exe' +end +--]] +descriptions.pdftops_prog = 'Full path to pdftops.exe (not used with TeX Live)' + +settings.use_pdftops = true +descriptions.use_pdftops = 'Use pdftops if available' + +-- epspdf stores ps- and pdf viewer settings on behalf of the gui interface +-- but does not use them itself. +-- They won't be used at all under osx or windows. + +settings.ps_viewer = false +descriptions.ps_viewer = + 'Epspdftk: viewer for PostScript files; not used on Windows or OS X' + +settings.pdf_viewer = false +descriptions.pdf_viewer = + 'Epspdftk: viewer for pdf files; not used on Windows or OS X' + +-- default_dir, which is used on all platforms, is only for the gui. + +if os.type == 'windows' then + settings.default_dir = + string.gsub(ep_shortname(os.getenv('USERPROFILE')), '\\', '/') +else + settings.default_dir = os.getenv('HOME') +end +descriptions.default_dir = + 'Epspdftk: initial directory; ignored by epspdf itself' + +function write_settings (file) + local f + if file then + f = io.open(rcfile, 'wb') + if not f then + return + end + else -- stdout to be captured by epspdftk + f = io.output() + if os.type=='windows' and not is_tl_w then + f:write('tl_w = no', eol) + end + end + for k, v in pairs(settings) do + if k ~= 'pdftops_prog' or os.type=='windows' then + if descriptions[k] and file then + f:write(eol, '# ', descriptions[k], eol) + end + f:write(k, ' = ', tostring(v), eol) + end + end + if file then + f:close() + end +end + +function read_settings(file) + -- read and interpret rcfile + -- we shall ignore illegal entries. + local contents + local f + if file then + f = io.open(rcfile, 'rb') + if not f then + return + end + else + f = io.input() + end + contents = f:read(10000) + if file then + f:close() + end + if not contents or contents=='' then + dbg('No settings read') + return + -- else + -- dbg(contents) + end + -- remove initial \r and \n characters + contents = string.gsub(contents, '^[\r\n]*', ''); + -- gmatch chops contents into series of non-line-ending characters + -- possibly followed by line-ending characters. + local k, v, vl, vnum + for l in string.gmatch(contents, '[^\r\n]+[\r\n]*') do + l = string.match(l,'[^\r\n]*') + if not string.match(l, '^#') then + k, v = string.match(l, '^%s*([^%s]+)%s*=%s*(.*)$') + if v then v = string.gsub(v,'%s*$', '') end + -- now handle k and v + if k == 'pdf_target' then + -- ignore unless valid option + if in_list(v, pdf_targets) then + settings[k] = v + end + elseif k == 'pdf_version' then + -- ignore unless valid option + if in_list(v, pdf_versions) then + settings[k] = v + end + --[[ + elseif k == 'ignore_hires_bb' then + vl = string.lower(string.sub(v,1,1)) + if v == 0 or vl == 'n' or vl == 'f' then + settings.use_hires_bb = true + elseif v == 1 or vl == 'y' or vl == 't' then + settings.use_hires_bb = false + end + elseif k == 'use_hires_bb' then + vl = string.lower(string.sub(v,1,1)) + if v == 0 or vl == 'n' or vl == 'f' then + settings.use_hires_bb = false + elseif v == 1 or vl == 'y' or vl == 't' then + settings.use_hires_bb = true + end + elseif k == 'bb_spread' then + vnum = tonumber(v) + if vnum and vnum >= 0 then + settings[k] = math.modf(v) -- truncate to integer + end + --]] + elseif k == 'pdftops_prog' then + if is_miktex then + settings.pdftops_prog = is_prog(v) + -- elseif os.type=='windows' then + -- settings.pdftops_prog = v + end -- else ignore + elseif k == 'ignore_pdftops' then + vl = string.lower(string.sub(v,1,1)) + if v == 0 or vl == 'n' or vl == 'f' then + settings.use_pdftops = true + elseif v == 1 or vl == 'y' or vl == 't' then + settings.use_pdftops = false + end + elseif k == 'use_pdftops' then + vl = string.lower(string.sub(v,1,1)) + if v == '0' or vl == 'n' or vl == 'f' then + settings.use_pdftops = false + elseif v == '1' or vl == 'y' or vl == 't' then + settings.use_pdftops = true + end + -- final three settings not used by epspdf itself but + -- passed along to epspdftk + elseif k == 'ps_viewer' then + settings.ps_viewer = v + elseif k == 'pdf_viewer' then + settings.pdf_viewer = v + elseif k == 'default_dir' then + settings.default_dir = v + elseif k == 'default_dir' then + settings.default_dir = v + end -- test for k + end -- not matching ^# + end -- for +end -- read settings + +-- command-line parameters: variables and functions ------------- + +function help (mess) + -- need to enforce an ordering, otherwise we could have used pairs(opts) + if mess then print(mess..eol) end + show_version() + print([[ + +Convert between [e]ps and pdf formats +Usage: epspdf[.tlu] [options] infile [outfile] +Default for outfile is file.pdf if infile is file.eps or file.ps +Default for outfile is file.eps if infile is file.pdf +]]) + -- omitted below: no-op options + for _, k in ipairs({'page', 'gray', 'bbox', 'pdf_target', 'pdf_version', + 'pdftops_prog', 'use_pdftops', 'save', 'debug', 'version', 'help' }) do + help_opt(k) + end + if mess then os.exit(1) else os.exit() end +end + +function help_opt (o) + -- one line where possible + local indent_n = 12 + local intent_sp = string.rep(' ', indent_n) + local indent_fmt = '%-' .. tostring(indent_n) .. 's' + v = opts[o] + if v=='pdftops_prog' and (os.type=='unix' or is_tl_w) then + return + end + if v and v.help then + local synt = join(v.forms, ', ') + if v.type ~= 'boolean' then synt = synt .. ' ' .. v.placeholder end + if string.len(synt)<indent_n then + print(string.format(indent_fmt, synt) .. v.help) + else + print(synt) + print(intent_sp .. v.help) + end + if v.negforms then + local neghelp = 'Reverses the above' + synt = join(v.negforms, ', ') + if string.len(synt)<indent_n then + print(string.format(indent_fmt, synt) .. neghelp) + else + print(synt) + print(intent_sp .. neghelp) + end + end + end +end + +function show_version () + print('Epspdf version '..ep_version..'\nCopyright (c) ' + ..ep_copyright..' Siep Kroonenberg') +end + +-- gui: reading and writing settings ----------- + +function gui(action) + -- use stdin for reading settings from gui, and stdout for writing + if action=='config_w' then + -- called at start of epspdftk + write_settings() -- to pipe epspdf => epspdftk + os.exit() + elseif action=='config_r' then + read_settings() -- from 'pipe' epspdftk => epspdf + write_settings(rcfile) + os.exit() + else + from_gui = true + end +end + +-- besides settings, which can be saved, we also use options which are not. +-- we already have an options table with sole entry 'safer' +-- the pdf output settings are converted to options array elements + +options.page = false +options.gray = false +options.bbox = false +options.info = false +options.debug = false +options.type = false -- implied via output filename on command line + +-- boundingboxes --------------------------------------------------- + +-- Bb.coords names now same as those of epdf PDFRectangle + +Bb = {} +Bb.coords = {'x1', 'y1', 'x2', 'y2'} + +function Bb:from_rect(r) + for _,k in ipairs(self.coords) do + if not r[k] or type(r[k])~='number' then + errror('from_rect called with illegal parameters') + end + -- sanity check on size + -- FIXME: this limit is far too high + if r[k]+.5==r[k] or r[k]-.5==r[k] then + errror('Bb:from_rect: ' .. r[k] ..' greater than maxint') + end + local b = {} + local eps = 0.000001 + b.x1, b.x2 = math.floor(math.min(r.x1, r.x2) + eps), + math.ceil(math.max(r.x1, r.x2) - eps) + b.y1, b.y2 = math.floor(math.min(r.y1, r.y2) + eps), + math.ceil(math.max(r.y1, r.y2) - eps) + if b.x1==b.x2 or b.y1==b.y2 then + errror('from_rect: width or height is zero') + end + setmetatable(b, {__index=self}) + return b + end +end + +Bb.bb_pat = '^%s*%%%%BoundingBox:' +Bb.bb_end = '^%s*%%%%BoundingBox:%s*%(%s*atend%s*%)' + +function Bb:from_comment(s) + local p = self.bb_pat..'%s*([-+%d]+)'..string.rep('%s+([-+%d]+)',3) + local b = {} + b.x1, b.y1, b.x2, b.y2 = string.match(s, p) + if not b.y2 then + errror('Bb.from_comment: illegal boundingbox string ' .. s) + end + for _,k in ipairs(self.coords) do + b[k] = tonumber(b[k]) + end + return Bb:from_rect(b) +end + +--[[ + +function Bb:copy () + local b = {} + for _,k in ipairs(self.coords) do b[k] = self[k] end + setmetatable(b, {__index=self}) +end + +function Bb:width() + return self.x2 - self.x1 +end + +function Bb:height() + return self.y2 - self.y1 +end + +function Bb:expand () + -- in-place expansion; does not return an object. + -- any point in preserving non-negativity? + local i = settings.bb_spread + if i and i>0 then + -- if x1~=0 then x1 = x1-1 end + -- if y1~=0 then y1 = y1-1 end + self.x1 = self.x1 - 1 + self.y1 = self.y1 - 1 + self.x2 = self.x2 + 1 + self.y2 = self.y2 + 1 + end +end + +-- no longer used: gs handles this +-- call this via pcall +function Bb:wrapper() + local fn = mktemp('ps') + local f = io.open(fn, 'wb') + f:write(string.format('%%%%BoundingBox: 0 0 %d %d\n', + self:width(), self:height()) + .. string.format('<< /PageSize [%d %d] >> setpagedevice\n', + self:width(), self:height()) + .. 'gsave\n' + .. string.format('%d %d translate\n', -self.x1, -self.y1)) + f:close() + return fn +end + +--]] + +function Bb:nonnegative () + return self.x1>=0 and self.y1>=0 +end + +function Bb:comment() + -- if options.debug then print(debug.traceback()) end + return string.format('%%%%BoundingBox: %d %d %d %d', + self.x1, self.y1, self.x2, self.y2) +end + +-- hires boundingboxes --------------------------------------------- + +HRBb = {} + +setmetatable(HRBb, {__index=Bb}) + +function HRBb:from_rect(r) + for _,k in ipairs(self.coords) do + if not r[k] or type(r[k])~='number' then + errror('from_rect called with illegal parameters') + end + -- sanity check on size + if r[k]+.5==r[k] or r[k]-.5==r[k] then + errror('HRBb:from_rect: ' .. b[k] ..' greater than maxint') + end + local b = {} + b.x1, b.x2 = math.min(r.x1, r.x2), math.max(r.x1, r.x2) + b.y1, b.y2 = math.min(r.y1, r.y2), math.max(r.y1, r.y2) + if b.x1==b.x2 or b.y1==b.y2 then + errror('from_rect: width or height is zero') + end + setmetatable(b, {__index=self}) + return b + end +end + +HRBb.bb_pat = '^%s*%%%%HiResBoundingBox:' +HRBb.bb_end = '^%s*%%%%HiResBoundingBox:%s*%(%s*atend%s*%)%s*$' + +function HRBb:from_comment(s) + -- dbg('hrbb from '..s) + local p = self.bb_pat..'%s*([-+.%deE]+)'..string.rep('%s+([-+.%deE]+)',3) + local b = {} + b.x1, b.y1, b.x2, b.y2 = string.match(s, p) + if not b.y2 then + errror('HRBb.from_comment: illegal boundingbox string ' .. s) + end + for _,k in ipairs(self.coords) do + b[k] = tonumber(b[k]) + end + return HRBb:from_rect(b) +end + +function HRBb:comment() + return string.format('%%%%HiResBoundingBox: %f %f %f %f', + self.x1, self.y1, self.x2, self.y2) +end + +--[[ + +function HRBb:expand () + errror('HRBb:expand not available') +end + +-- no longer used: gs handles this +-- call this one also via pcall +function HRBb:wrapper() + + -- local fn = mktemp('ps') + -- local f = io.open(fn, 'wb') + -- f.write(string.format('<< /PageSize [%f %f] >> setpagedevice\n', + -- self.x2 - self.x1, self.y2 - self.y1)) + -- f.write(string.format('gsave\n%f %f translate\n', -self.x1, -self.y1)) + -- f:close() + -- return fn + + return string.format( + '<< /PageSize [%f %f] >> setpagedevice gsave %f %f translate', + self.x2 - self.x1, self.y2 - self.y1, -self.x1, -self.y1) +end + +--]] + +-- manipulating eps/ps/pdf files ----------------------------------- + +-- command-line fragments for conversions +-- We could make these `class attributes' for PsPdf but to what purpose? +-- For Windows shell commands, we need to substitute `#' for `=' +-- when invoking Ghostscript. For simplicity, we do this across the board. + +gs_options = {gs_prog, '-q', '-dNOPAUSE', '-dBATCH', '-P-', '-dSAFER'} + +-- windows: use env vars rather than additional options +-- may add custom options later + + + +pdf_options = {'-sDEVICE#pdfwrite'} -- '-dUseCIEColor' causes serious slowdown +-- for final conversion to pdf; +-- will be completed after reading settings and options +gray_options = {'-dProcessColorModel#/DeviceGray', + '-sColorConversionStrategy#Gray'} +-- below, '-f' guarantees that next string is interpreted as input file +pdf_tailoptions = false -- to be set after option parsing + +pdftops = false +-- gets a value if we are going to use pdftops + +ps_options = {'-level3'} +-- may add custom options later + + +function identify(path) + local f = io.open(path, 'rb') + if not f then + errror('Failure to open '..path..' for identification') + end + local filestart= f:read(23) + f:close() + if not filestart or filestart=='' then + return false + elseif string.match(filestart,'^\197\208\211\198') then -- c5 d0 d3 c6 + return 'epsPreview' + elseif string.match(filestart,'^%%!PS%-Adobe%-%d%.%d EPSF%-%d%.%d') then + return 'eps' + elseif string.match(filestart,'^%%!PS%-Adobe%-%d%.%d') then + for _, p in ipairs({'.eps', '.epi', '.epsi', '.epsf'}) do + if string.sub(string.lower(path), -1-string.len(p),-1) == p then + return 'eps' + else + return 'ps' + end + end + return 'ps' + elseif string.match(filestart, '^%%PDF') then + return 'pdf' + else + return false + end +end -- identify + +function pdf_props(path) + local pdfdoc = epdf.open(path) + if not pdfdoc then + errror('epdf.open failed on '..path) + end + local cat = pdfdoc:getCatalog() + if not cat then + errror('Cannot open pdf catalog of '..path) + end + local pg = cat:getNumPages() + if not pg then + errror('Cannot read n. of pages of '..path) + end + local maver = pdfdoc:getPDFMajorVersion() + if not maver then + errror('Cannot read pdf major version of '..path) + end + local miver = pdfdoc:getPDFMinorVersion() + if not miver then + errror('Cannot read pdf minor version of '..path) + end + if maver > 1 then + print(path..' has pdf major version \n'..tostring(maver).. + ' which is unsupported;\n'.. + 'Continuing with fingers crossed...') + end + return pg, miver, maver +end + +function info (infile) + local intype = identify(infile) + if not intype then + print(infile..' has an unsupported filetype.') + elseif intype~='pdf' then + print(infile..' has type '..intype..'.') + else + local pg, miver, maver = pdf_props(infile) + print(infile..' has type pdf, version '..tostring(maver).. + '.'..tostring(miver)..' and has '..tostring(pg)..' pages.') + end + os.exit() +end + +-- PsPdf object ------------------------------------------------- + +PsPdf = {} + +-- creators + +function PsPdf:new(ext) + -- dbg('PsPdf:new') + local psp = {} + setmetatable(psp, {__index = self}) + -- assign temp file + psp.path = mktemp(string.lower(ext)) + if string.lower(ext)=='pdf' then + psp.type = 'pdf' + elseif string.lower(ext)=='eps' then + psp.type = 'eps' + elseif string.lower(ext)=='ps' then + psp.type = 'ps' + else + psp.type = false + end + if psp.type=='eps' then + psp.pages = 1 + end + psp.bb = false + psp.hrbb = false + return psp +end -- PsPdf:new + +function PsPdf:from_path(path) + -- dbg('PsPdf:from_path') + local psp = {} + setmetatable(psp, {__index = self}) + psp.path = path + if lfs.isfile(path) then + -- turn existing file into PsPdf object. + psp.type = identify(psp.path) + if psp.type=='pdf' then + psp.pages, psp.miver, psp.maver = pdf_props(psp.path) + end + else + errror('PsPdf:from_path called with non-existant file '..path) + end + if psp.type=='eps' then + psp.pages = 1 + end + psp.bb = false + psp.hrbb = false + -- calculate when needed + return psp +end -- PsPdf:from_path + +--[===[ getting boundingbox property from file itself -------------- + +find_bb_simple: use only for eps PsPdf objects we generated +ourselves, so we can assume that the bbox comments are in the header +and the hires bb lies within the lores bb. +Of course the file itself is not rewritten. + +--]===] + +function PsPdf:find_bb_simple() + -- dbg('PsPdf:find_bb_simple') + if self.type~='eps' then + errror('find_bb_simple called with non-eps file '..self.path) + end + self.bb = false + self.hrbb = false + local slurp = false + local f = io.open(self.path, 'rb') + if f then + slurp = f:read(bufsize) + f:close() + end + lines = {} + for l in string.gmatch(slurp, '[^\n\r]+') do + if string.match(l, Bb.bb_pat) then + self.bb = Bb:from_comment(l) + elseif string.match(l, HRBb.bb_pat) then + self.hrbb = HRBb:from_comment(l) + elseif self.bb then + break -- stop looking; we expect hrbb next to bb + end + if self.bb and self.hrbb then break end + end + if not self.bb then + errror('No valid boundingbox for generated file' .. self.path) + end + return self -- no real need for a return value +end + +function PsPdf:bb_from_gs(pg) + + -- dbg('bb_from_gs '..pg) + + if self.type=='ps' then + errror('bb_from_gs called with ps file '..self.path) + -- not needed for generic PostScript, + -- page selection only works with pdf files, so we save ourselves + -- the trouble of picking the right bbox from a list + end + if self.type=='eps' and not self.bb:nonnegative() then + errror('bb_from_gs called on ' .. self.path .. + ' which has some negative boundingbox coordinates') + end + -- A pdf can also have negative ...Box coordinates, but apparently + -- for pdf the bbox returned by gs is relative to the lower-left corner. + -- Anyhow, with pdf it all works out even with negative coordinates. + + -- Since Ghostscript writes the boundingbox comments to stderr, + -- we need a shell to intercept this output: + + local bb_file = mktemp('dsc') + local cmdline = table.concat(gs_options,' ') + if self.type=='pdf' then + if not pg then pg=1 end + cmdline = cmdline .. ' -dFirstPage#' .. tostring(pg) .. + ' -dLastPage#' .. tostring(pg) + end + cmdline = cmdline .. ' -sDEVICE#bbox ' .. self.path .. ' 2>'..bb_file + + -- execute shell command + + local r, cmd + if os.type=='windows' then + -- redirection does not work right for os.execute on TL/w32 <= 2011 + -- but it does when calling the cmd shell explicitly + cmd = {'cmd', '/c', cmdline} + log_cmd(cmd) + r = os.spawn(cmd) + else + write_log('os.execute: '..cmdline) + r = os.execute(cmdline) + end + if not r then + errror('Cannot get fixed boundingbox for '..self.path) + end + + -- read new bbox from ghostscript output + -- can we really count on the plain bb coming first? + -- OTOH, I would rather not introduce unnecessary complexity + -- still, it may be better to match each line with [HR]Bb_pat + + local bb = false + local hrbb = false + local fin = io.open(bb_file, 'r') + if fin then + for i=1,10 do -- actually, 2 should suffice + local l = fin:read("*line") + if not l then break end + if string.match(l, Bb.bb_pat) then + bb = Bb:from_comment(l) + end + if string.match(l, HRBb.bb_pat) then + hrbb = HRBb:from_comment(l) + end + end + fin:close() + end + if not bb or not hrbb then + errror('Cannot get fixed boundingbox for '..self.path) + end + return bb, hrbb +end + +-- eps_clean: remove some problem features from eps (new file & object) + +function PsPdf:eps_clean() + + -- return a PsPdf object referring to a new file + -- without a preview header and with boundingbox(es) in the header + + local function bytes2num (s, i) + -- convert substring s[i..i+3] to a number. + -- by working byte for byte we avoid endian issues + local n = string.byte(s, i+3) + for j=2,0,-1 do n = 256*n + string.byte(s, i+j) end + return n + -- somehow the explicit expression below didn't work + -- return ((256 * (256 * (256 * string.byte(s,i+3)) + string.byte(s,i+2)) + -- + string.byte(s,i+1)) + string.byte(s,i)) + end + + dbg('PsPdf:eps_clean '..self.path) + if self.type~='eps' and self.type~='epsPreview' then + errror('epsclean called with non-eps file ' .. self.path) + end + local offset, ps_length = false, false + local fin, fout + if self.type=='eps' then + offset = 0 + ps_length = lfs.attributes(self.path, 'size') + else + -- read TOC; see Adobe EPS specification + -- interpret byte for byte, in case the platform is not little-endian + fin = io.open(self.path, 'rb') + if fin then + local toc = fin:read(12) + fin:close() + if toc and string.len(toc)==12 then + offset = bytes2num(toc, 5) + ps_length = bytes2num(toc, 9) + end + end + if not offset then + errror('Could not read preview header of ' .. self.path) + end + -- dbg(tostring(offset)..' '..tostring(ps_length)) + end + + -- create the PsPdf object which is to be returned + + local psp + psp = PsPdf:new('eps') + -- dbg(psp.path) + + -- read an initial and if necessary a final chunk of the file + -- to find boundingbox comments. + + local atend = false + local hr_atend = false + local slurp -- the read buffer + local l -- contains current scanned line; split off from slurp + -- pre_lines: scanned header lines; alternately lines and eols + local pre_lines = {} + -- new_offset: offset plus combined length of scanned header lines + local new_offset = offset + -- post_lines: scanned trailer lines + local post_lines = {} + -- middle_length: ps_length minus scanned header- and and maybe trailer parts + -- this is the length of file that will be copied wholesale. + local middle_length + local i, i_bb, i_hrbb + local j, j_bb, j_hrbb, j_end + -- j_end: index of final scanned trailer line + -- no i_end necessary: for header lines we can use #pre_lines. + + fin = io.open(self.path, 'rb') + if not fin then errror('Cannot read '..self.path) end + fin:seek('set', offset) + + -- remaining, unscanned length of input buffer slurp + local unscanned = math.min(ps_length,bufsize) + -- dbg('bytes to be read: '..tostring(unscanned)) + slurp = fin:read(unscanned) + -- dbg('Read from '..self.path..': '..string.len(slurp)..' bytes') + + -- unnecessary: + psp.bb = nil + psp.hrbb = nil + + i, i_bb, i_hrbb = 0, false, false + while unscanned>0 do + i = i+1 + if string.find(slurp,'[\n\r]')==1 then + l,slurp = string.match(slurp, '^([\n\r]+)(.*)$') + else + l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$') + if string.match(l, Bb.bb_end) then + atend = true + i_bb = i + elseif string.match(l, Bb.bb_pat) then + -- dbg(l) + psp.bb = Bb:from_comment(l) + -- dbg(psp.bb:comment()) + -- from_comment errors out on failure; no need to check return value + i_bb = i + elseif string.match(l, HRBb.bb_end) then + hr_atend = true + i_hrbb = i + elseif string.match(l, HRBb.bb_pat) then + -- dbg(l) + psp.hrbb = HRBb:from_comment(l) + -- dbg(psp.hrbb:comment()) + i_hrbb = i + end -- bbox line + end -- eol/non-eol + pre_lines[i] = l + unscanned = unscanned - string.len(l) + if (i_bb and (i_hrbb or (i_bb<(i-1)))) or unscanned<=0 then + -- condition i_bb<i-1: + -- We do not want to find the hrbb of an included eps. + -- Therefore we stop looking for hrbb if it is not next to bb + -- (with an intervening eol) + -- Note that the header comments are not necessarily terminated + -- with a %%EndComments line; see Adobe DSC spec 5001 + break + end -- deciding whether to stop + end -- while + new_offset = offset + string.len(table.concat(pre_lines)) + middle_length = ps_length - string.len(table.concat(pre_lines)) + + if atend or hr_atend then + -- find boundingbox comments, starting from end of postscript + if ps_length>bufsize then + fin:seek('set',offset+ps_length-bufsize) + unscanned = bufsize + slurp = fin:read(unscanned) + else + -- use what is left from old slurp + unscanned = string.len(slurp) + end + j = 1 -- count down from 0 + j_bb, j_hrbb, j_end = false, false, false + while unscanned>0 do + j = j - 1 + -- dbg(j) + if string.find(slurp,'[\n\r]', string.len(slurp)) then + -- dbg('eol(s)') + slurp,l = string.match(slurp, '^(.-)([\n\r]+)$') + -- '-': non-greedy matching + else + slurp,l = string.match(slurp, '^(.-)([^\n\r]+)$') + -- dbg(l) + if string.match(l, Bb.bb_pat) then + psp.bb = Bb:from_comment(l) + j_bb = j + elseif string.match(l, HRBb.bb_pat) then + psp.hrbb = HRBb:from_comment(l) + j_hrbb = j + end -- bbox line + end -- eol/non-eol + post_lines[j] = l + unscanned = unscanned - string.len(l) + if (psp.bb and + (psp.hrbb or not hr_atend or j_bb>(j+1))) or unscanned<=0 then + -- stop looking + j_end = j + break + end -- deciding whether to stop + end -- while + middle_length = middle_length - + string.len(table.concat(post_lines, '', j_end, 0)) + end --if atend + fin:close() + -- fix boundingbox lines + if atend and j_bb then + -- pre_lines[i_bb] = post_lines[j_bb] + pre_lines[i_bb] = psp.bb:comment() -- WHY DOESNT THIS WORK ???? + post_lines[j_bb] = '' + post_lines[j_bb+1] = '' + end + if hr_atend and j_hrbb then + -- dbg(psp.hrbb:comment()) + -- pre_lines[i_hrbb] = post_lines[j_hrbb] + pre_lines[i_hrbb] = psp.hrbb:comment() + post_lines[j_hrbb] = '' + post_lines[j_hrbb+1] = '' + end + -- create cleaned eps file + fout = io.open(psp.path, 'wb') + if not fout then errror('Cannot create new file '..psp.path) end + fout:write(table.concat(pre_lines)) + fout:close() + slice_file(self.path, psp.path, middle_length, new_offset, 'ab') + fout = io.open(psp.path, 'ab') + fout:write(table.concat(post_lines, '', j_end, 0)) + fout:close() + return psp +end -- eps_clean + +-- tight boundingbox (new file & object) + +function PsPdf:eps_crop() + + -- not a proper conversion, although + -- we use the Ghostscript bbox device for a tight boundingbox. + -- We use both the regular and the hires boundingbox from gs. + -- The eps should already have been cleaned up by eps_clean, + -- and the current boundingbox should not contain negative coordinates, + -- otherwise the bbox output device may give incorrect results. + -- Only the boundingbox in the eps is rewritten. + + dbg('PsPdf:eps_crop '..self.path) + if self.type~='eps' then + errror('eps_crop called with non-eps file ' .. self.path) + end + + -- create the PsPdf object which is to be returned + + local psp = PsPdf:new('eps') + + -- read new bbox from ghostscript output + + psp.bb, psp.hrbb = self:bb_from_gs() + + -- rewrite header with new boundingboxes + + local slurp -- the read buffer + local l -- contains current scanned line; split off from slurp + -- pre_lines: scanned header lines; alternately lines and eols + local pre_lines = {} + -- offset: combined length of scanned header lines + local offset = 0 + local ps_length = lfs.attributes(self.path, 'size') + local i, i_bb, i_hrbb + + fin = io.open(self.path, 'rb') + if not fin then errror('Cannot read '..self.path) end + + -- remaining, unscanned length of input buffer slurp + local unscanned = math.min(ps_length,bufsize) + -- dbg('bytes to be read: '..tostring(unscanned)) + slurp = fin:read(unscanned) + -- dbg('Read from '..self.path..': '..string.len(slurp)..' bytes') + i, i_bb, i_hrbb = 0, false, false + while unscanned>0 do + i = i+1 + if string.find(slurp,'[\n\r]')==1 then + l,slurp = string.match(slurp, '^([\n\r]+)(.*)$') + else + l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$') + if string.match(l, Bb.bb_pat) then + i_bb = i + elseif string.match(l, HRBb.bb_pat) then + i_hrbb = i + end -- bbox line + end -- eol/non-eol + pre_lines[i] = l + unscanned = unscanned - string.len(l) + if (i_bb and (i_hrbb or (i_bb<(i-1)))) or unscanned<=0 then + break + end + end -- while + fin:close() + offset = string.len(table.concat(pre_lines)) + + if i_hrbb then + pre_lines[i_bb] = psp.bb:comment() + pre_lines[i_hrbb] = psp.hrbb:comment() + else + -- jam both bbox comments into one slot, with an intervening eol. + -- for the sake of conformity, we copy an existing eol. + pre_lines[i_bb] = psp.bb:comment() .. pre_lines[i_bb-1] .. + psp.hrbb:comment() + end + + -- write a new eps file + + fout = io.open(psp.path, 'wb') + if not fout then errror('Cannot write new file '.. psp.path) end + fout:write(table.concat(pre_lines)) + fout:close() + slice_file(self.path, psp.path, + lfs.attributes(self.path,'size') - offset, offset, 'ab') + options.bbox = false + -- dbg('eps_crop from '..self.path..' to '..psp.path) + return psp +end -- eps_crop + +--[===[ real conversions involving a single call of gs or pdftops -------- + +Each conversion fullfills all options that it can: gray, bbox and +page. gray when converting to pdf, bbox when converting from eps or +from pdf to pdf and page when converting from pdf. It then sets the +fullfilled option(s) to false. + +We like to preserve fonts as fonts. gs does this when generating +pdf, but may fail for fonts such as cid and large truetype when +generating PostScript. In such cases, pdftops may succeed. However, +it seems that if the page contains an element that does not cleanly +convert, pdftops simply rasterizes the entire page, and that this +choice is made per page. + +TODO: pdf => pdf with bbox via pdftex, as in pdfcrop utility + +--]===] + +-- TODO: multiple pages +-- (means additional parameter checking) + +-- Converting from pdf to pdf using luatex; no grayscaling + +function PsPdf:pdf_crop() + + -- options to be fulfilled: page, boundingbox + -- only called directly. + -- embeds the pdf with crop parameters into a new (lua)tex document + if not (options.bbox or options.page) then + return self + end + if options.page and options.page > self.pages then + errror('PsPdf:pdf_crop called with non-existent page '.. options.page) + end + local pg = options.page or 1 + local bb, hrbb + if options.bbox then + bb, hrbb = self:bb_from_gs(pg) + else + -- use [Trim|Crop|Media]Box instead + local dummy = epdf.open(self.path) + if not dummy then + errror('Epdf: cannot open '..self.path) + end + dummy = dummy:getCatalog() + if not dummy then + errror('Cannot open catalog of '..self.path) + end + dummy = dummy:getPage(pg) + if not dummy then + errror('Epdf: cannot open page object '..tostring(pg)..' of '..self.path) + end + hrbb = dummy:getTrimBox() + if not hrbb then + hrbb = dummy:getCropBox() + end + if not hrbb then + hrbb = dummy:getMediaBox() + end + -- further checks, including for non-nil, by Bb:from_rect, + -- which errors out on failures + hrbb = HRBb:from_rect(hrbb) + end + + -- location of luatex + local luatex_prog = fw(os.selfdir) .. '/luatex' -- absolute path + if os.type == 'windows' then + luatex_prog = luatex_prog .. '.exe' + end + + -- write TeX file which includes cropped pdf page + -- adapted from Heiko Oberdiek's pdfcrop utility. + -- first, create a table with the component strings for the tex source + dummy = {} + dummy[1] = [[ +\catcode37 14 % percent +\catcode33 12 % exclam +\catcode34 12 % quote +\catcode35 6 % hash +\catcode39 12 % apostrophe +\catcode40 12 % left parenthesis +\catcode41 12 % right parenthesis +\catcode45 12 % minus +\catcode46 12 % period +\catcode60 12 % less +\catcode61 12 % equals +\catcode62 12 % greater +\catcode64 12 % at +\catcode91 12 % left square +\catcode93 12 % right square +\catcode96 12 % back tick +\catcode123 1 % left curly brace +\catcode125 2 % right curly brace +\catcode126 12 % tilde +\catcode`\#=6 % +\escapechar=92 % +\def\IfUndefined#1#2#3{% + \begingroup\expandafter\expandafter\expandafter\endgroup + \expandafter\ifx\csname#1\endcsname\relax + #2% + \else + #3% + \fi +} +\begingroup + \newlinechar=10 % + \endlinechar=\newlinechar % + \ifnum0% + \directlua{% + if tex.enableprimitives then + tex.enableprimitives('TEST', { + 'luatexversion', + 'pdfoutput', + 'pdfcompresslevel', + 'pdfhorigin', + 'pdfvorigin', + 'pdfpagewidth', + 'pdfpageheight', + 'pdfmapfile', + 'pdfximage', + 'pdflastximage', + 'pdfrefximage', + 'pdfminorversion', + 'pdfobjcompresslevel', + }) + tex.print('1') + end + }% + \ifx\TESTluatexversion\UnDeFiNeD\else 1\fi % + =11 % + \global\let\luatexversion\luatexversion % + \global\let\pdfoutput\TESTpdfoutput % + \global\let\pdfcompresslevel\TESTpdfcompresslevel % + \global\let\pdfhorigin\TESTpdfhorigin % + \global\let\pdfvorigin\TESTpdfvorigin % + \global\let\pdfpagewidth\TESTpdfpagewidth % + \global\let\pdfpageheight\TESTpdfpageheight % + \global\let\pdfmapfile\TESTpdfmapfile % + \global\let\pdfximage\TESTpdfximage % + \global\let\pdflastximage\TESTpdflastximage % + \global\let\pdfrefximage\TESTpdfrefximage % + \global\let\pdfminorversion\TESTpdfminorversion % + \global\let\pdfobjcompresslevel\TESTpdfobjcompresslevel % + \else % + \errmessage{% + Missing \string\luatexversion % + }% + \fi % +\endgroup % + +\pdfoutput=1 % +\pdfcompresslevel=9 % +\csname pdfmapfile\endcsname{} +\def\setpdfversion#1#2{% + \ifnum#2>1 % + \pdfobjcompresslevel=2 % + % including unsupported pdf version! + \pdfinclusionerrorlevel=0 + \pdfminorversion=9\relax + \else + \ifnum#1>4 % + \pdfobjcompresslevel=2 % + \else + \pdfobjcompresslevel=0 % + \fi + \pdfminorversion=#1\relax + \fi +} +\def\page #1 [#2 #3 #4 #5]{% + \count0=#1\relax + \setbox0=\hbox{% + \pdfximage page #1 mediabox{]] + dummy[2] = self.path + dummy[3] = [[}% + \pdfrefximage\pdflastximage + }% + \pdfhorigin=-#2bp\relax + \pdfvorigin=#3bp\relax + \pdfpagewidth=#4bp\relax + \advance\pdfpagewidth by -#2bp\relax + \pdfpageheight=#5bp\relax + \advance\pdfpageheight by -#3bp\relax + \ht0=\pdfpageheight + \shipout\box0\relax +} +]] + -- pdf minor version to write to tex header + local tex_miver = false + if settings.pdf_version=='default' then + tex_miver = self.miver + else + -- in this case, gs should already have converted to + -- a sufficiently low version + tex_miver = tonumber(settings.pdf_version) + if tex_miver>self.miver then + errror('Pdf_crop: forgot to reduce pdf version') + end + end + dummy[4] = string.format([[ +\setpdfversion{%d}{%d} +\page %d [%f %f %f %f] +\csname @@end\endcsname +\end +]], + tex_miver, self.maver, options.page or 1, + hrbb.x1, hrbb.y1, hrbb.x2, hrbb.y2) + + local textemp = mktemp('tex') -- this also takes care of pdf: + local pdftemp = string.gsub(textemp, 'tex$', 'pdf') + local f = io.open(textemp, 'w') + f:write(table.concat(dummy, '')) + f:close() + local cmd, res, psp + cmd = {luatex_prog, '--safer', '--no-shell-escape', textemp} + log_cmd(cmd) + res = os.spawn(cmd) + if res and res==0 and lfs.attributes(pdftemp, 'size')>0 then + psp = PsPdf:from_path(pdftemp) + return psp + else + errror('pdf_crop failed on '..self.path) + end +end + +function PsPdf:eps_to_pdf() + + -- option to be fulfilled: gray + -- set target and pdf version if applicable + -- dbg('PsPdf:eps_to_pdf') + if self.type~='eps' then + errror('PsPdf:eps_to_pdf called for non-eps file '.. self.path) + end + local cmd + if options.bbox and self.bb:nonnegative() then + self = self:eps_crop() -- this sets options.bbox to false + end + cmd = tab_combine({gs_options, pdf_options}) + -- dbg(table.concat(cmd,' ')) + if options.gray then + cmd = tab_combine({cmd, gray_options}) + -- dbg(table.concat(cmd,' ')) + options.gray = false + end + table.insert(cmd, '-dEPSCrop') -- always hires bb + -- dbg(table.concat(cmd,' ')) + local psp = PsPdf:new('pdf') + table.insert(cmd, '-sOutputFile#'..psp.path) + -- dbg(table.concat(cmd,' ')) + cmd = tab_combine({cmd, pdf_tailoptions, {self.path}}) + -- dbg(table.concat(cmd,' ')) + log_cmd(cmd) + local res = os.spawn(cmd) + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + psp.pages, psp.miver, psp.maver = pdf_props(psp.path) + return psp + else + errror('eps_to_pdf failed on '..self.path) + end +end -- eps_to_pdf + +-- Converting from pdf to pdf with grayscaling and/or page selection + +function PsPdf:pdf_to_pdf() + + -- option to be fulfilled: gray and optionally page. + -- do not call this just for page selection because + -- pdf_crop can do this in a less invasive manner + -- dbg('PsPdf:pdf_to_pdf') + if self.type~='pdf' then + errror('PsPdf:pdf_to_pdf called for non-pdf file '.. self.path) + end + local cmd + if options.page and options.page > self.pages then + errror('PsPdf:pdf_to_pdf called with non-existent page '.. options.page) + end + cmd = tab_combine({gs_options, pdf_options}) + -- dbg(table.concat(cmd,' ')) + if options.gray then + cmd = tab_combine({cmd, gray_options}) + -- dbg(table.concat(cmd,' ')) + options.gray = false + end + if options.page then + table.insert(cmd, '-dFirstPage#'..tostring(options.page)) + table.insert(cmd, '-dLastPage#'..tostring(options.page)) + -- dbg(table.concat(cmd,' ')) + options.page = false + end + local psp = PsPdf:new('pdf') + table.insert(cmd, '-sOutputFile#'..psp.path) + cmd = tab_combine({cmd, pdf_tailoptions}) + -- dbg(table.concat(cmd,' ')) + table.insert(cmd, self.path) + -- dbg(table.concat(cmd,' ')) + log_cmd(cmd) + local res = os.spawn(cmd) + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + psp.pages, psp.miver, psp.maver = pdf_props(psp.path) + return psp + else + errror('pdf_to_pdf failed on '..self.path) + end +end -- pdf_to_pdf + +function PsPdf:pdf_to_eps() + + -- options to be fulfilled: bbox and page + -- dbg(tostring(settings.pdftops_prog)) + local psp = PsPdf:new('eps') + local cmd, res + local page = false + if self.pages>1 then + page = 1 + if options.page then page = options.page end + if options.page and options.page > self.pages then + errror('PsPdf:pdf_to_eps called with non-existant page '.. options.page) + end + page = tostring(page) + end + if pdftops then + if page then + cmd = tab_combine({{pdftops}, ps_options, + {'-f', page, '-l', page, + '-eps', self.path, psp.path}}) + else + cmd = tab_combine({{pdftops}, ps_options, + {'-eps', self.path, psp.path}}) + end + options.page = false + log_cmd(cmd) + if os.type=='windows' then + -- suppress console output of 'No display font for...' messages, + -- which are usually harmless and for which I know no easy fix + res = os.spawn({'cmd', '/c', table.concat(cmd, ' ')..' 2>>'..log_bsl}) + else + res = os.spawn(cmd) + end + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + psp.pages = 1 + else + errror('pdf_to_eps failed on '..self.path) + end + -- fix for incorrect DSC header produced by some versions of pdftops: + -- if necessary, change line `% Produced by ...' into `%%Produced by ...' + -- this is usually the second line. + -- otherwise the DSC header would be terminated before the bbox comment. + -- this problem exists with pdftops from TL2011/w32. + local slurp -- input buffer + local fin = io.open(psp.path, 'rb') + if not fin then errror('Cannot read '..psp.path) end + + -- remaining, unscanned length of input buffer slurp + local unscanned = math.min(lfs.attributes(psp.path, 'size'),bufsize) + slurp = fin:read(unscanned) + local i, i_bb = 0, false + local needs_fixing = false + local pre_lines = {} + local offset = 0 + while unscanned>0 do + i = i+1 + if string.find(slurp,'[\n\r]')==1 then + l,slurp = string.match(slurp, '^([\n\r]+)(.*)$') + else + l,slurp = string.match(slurp, '^([^\n\r]+)(.*)$') + if string.match(l, Bb.bb_pat) then + -- bbox line + i_bb = i + elseif string.match(l, '^%%%s') then -- `%' is escape char: doubled + -- %X with X printable would be ok + needs_fixing = true + -- fix rightaway + l = string.gsub(l, '^%%%s', '%%%%') -- same length + end + end -- eol/non-eol + pre_lines[i] = l + unscanned = unscanned - string.len(l) + offset = offset + string.len(l) + if i_bb then break end + end -- while + fin:close() + if needs_fixing then + -- write a new eps file + local newfile = mktemp('eps') + fout = io.open(newfile, 'wb') + if not fout then errror('Cannot write new file '.. newfile) end + fout:write(table.concat(pre_lines)) + fout:close() + slice_file(psp.path, newfile, + lfs.attributes(psp.path,'size') - offset, offset, 'ab') + psp.path = newfile + end -- needs_fixing + else -- use ghostscript + cmd = tab_combine({gs_options, + {'-sDEVICE#epswrite', '-dLanguageLevel#3'}}) + -- the restrictions on eps files are apparently + -- incompatible with grayscaling + if options.page then + table.insert(cmd, '-dFirstPage='..page) + table.insert(cmd, '-dLastPage='..page) + end + table.insert(cmd, '-sOutputFile='..psp.path) + table.insert(cmd, self.path) + options.page = false + log_cmd(cmd) + res = os.spawn(cmd) + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + psp.pages = 1 + else + errror('pdf_to_eps failed on '..self.path) + end + end -- use ghostscript + psp:find_bb_simple() + if options.bbox then psp = psp:eps_crop() end + return psp + +end -- pdf_to_eps + +function PsPdf:ps_to_pdf() + + -- options to be fulfilled: gray + -- dbg('PsPdf:ps_to_pdf') + if self.type~='ps' then + errror('PsPdf:ps_to_pdf called for non-ps file '.. self.path) + end + local cmd + cmd = tab_combine({gs_options, pdf_options}) + if options.gray then + cmd = tab_combine({cmd, gray_options}) + options.gray = false + end + local psp = PsPdf:new('pdf') + table.insert(cmd, '-sOutputFile#'..psp.path) + cmd = tab_combine({cmd, pdf_tailoptions}) + table.insert(cmd, self.path) + log_cmd(cmd) + local res = os.spawn(cmd) + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + psp.pages, psp.miver, psp.maver = pdf_props(psp.path) + return psp + else + errror('ps_to_pdf failed on '..self.path) + end + +end -- PsPdf:ps_to_pdf + +function PsPdf:pdf_to_ps() + + -- options to be fulfilled: page and, if not using pdftops, also gray + -- dbg('PsPdf:pdf_to_ps') + local psp = PsPdf:new('ps') + local page = false + if self.pages>1 then + if options.page and options.page > self.pages then + errror('PsPdf:pdf_to_ps called with non-existant page '.. options.page) + elseif options.page then + page = tostring(options.page) + psp.pages = 1 + end + else + psp.pages = self.pages + end + local cmd, res + if pdftops then + cmd = tab_combine({{pdftops}, ps_options}) + if page then + cmd = tab_combine({cmd, {'-f', page, '-l', page}}) + end + cmd = tab_combine({cmd, {'-paper', 'match', self.path, psp.path}}) + -- cmd[0] = pdftops + else -- use ghostscript + cmd = tab_combine({gs_options, + {'-sDEVICE#ps2write', '-dLanguageLevel#3'}}) + if options.gray then + cmd = tab_combine({cmd, gray_options}) + -- dbg(table.concat(cmd,' ')) + options.gray = false + end + if page then + cmd = tab_combine({cmd, {'-dFirstPage#'..page, '-dLastPage#'..page}}) + end + table.insert(cmd, '-sOutputFile#'..psp.path) + -- table.insert(cmd, '-f') + table.insert(cmd, self.path) + end + options.page = false + log_cmd(cmd) + -- if os.type=='windows' and pdftops and not is_miktex then + -- if os.type=='windows' and pdftops then + -- -- suppress console output of 'No display font for...' messages, + -- -- which are usually harmless and for which I know no easy fix + -- res = os.spawn({'cmd', '/c', table.concat(cmd, ' ')..' 2>>'..log_bsl}) + -- else + res = os.spawn(cmd) + -- end + if res and res==0 and lfs.attributes(psp.path, 'size')>0 then + return psp + else + errror('pdf_to_ps failed on '..self.path) + end +end -- PsPdf:pdf_to_ps + +function PsPdf:any_to_any() + + -- weed out nonsense options + + -- dbg('PsPdf:any_to_any') + if options.type=='ps' then + options.bbox = false + -- dbg('Ignoring bbox option for ps output') + end + if options.bbox and not options.page then + options.page = 1 + -- dbg('Selecting page 1 for bbox') + end + if self.pages==1 then + options.page = false + -- dbg('dropping page selection; source is already a 1-page document') + end + -- for _,o in ipairs({'page', 'gray', 'bbox'}) do + -- -- if options[o] then dbg('Do option '..o) end + -- end + + -- check source and destination filetypes + + if not self.type then + errror('any_to_any: cannot convert; unsupported source filetype') + end + if not options.type or options.type=='epsPreview' then + errror('any_to_any: cannot convert; unsupported destination filetype') + end + + -- `distiller' settings depend on whether final output is pdf + if options.type=='pdf' then + table.insert(pdf_options, '-dPDFSETTINGS#/'..settings.pdf_target) + if settings.pdf_version~='default' then + table.insert(pdf_options, '-dCompatibilityLevel#'..settings.pdf_version) + end + -- below, try <</NeverEmbed [/Times-Roman /TimesBold ...]>> + if settings.pdf_target=='screen' or settings.pdf_target=='ebook' then + pdf_tailoptions = {'-c', + '.setpdfwrite', '-f'} + -- -f ensures that the input filename is not added to the -c string + else + pdf_tailoptions = {'-c', + '.setpdfwrite <</NeverEmbed [ ]>> setdistillerparams', '-f'} + end + else + table.insert(pdf_options, '-dPDFSETTINGS#/default') + pdf_tailoptions = {'-c', + '.setpdfwrite <</NeverEmbed [ ]>> setdistillerparams', '-f'} + end + + -- each single-step conversion takes care of options it can handle + -- and sets those options to false. + -- for boundingboxes, eps_crop is either called explicitly + -- or called implicitly by another converter. + -- pdf_crop is always called explicitly and always as the last step + + local psp = self + + if psp.type=='eps' or psp.type=='epsPreview' then + -- As a side effect of eps_clean, the modified source file is copied + -- to the temp subdirectory. + psp = psp:eps_clean() + if options.bbox and psp.bb:nonnegative() then + psp = psp:eps_crop() + end + if options.type=='eps' then + if options.gray or options.bbox then + -- bbox: eps_crop was apparently not applicable: pdf roundtrip + psp = psp:eps_to_pdf():pdf_to_eps() + end + elseif options.type=='pdf' then + psp = psp:eps_to_pdf() + if options.bbox then + psp = psp:pdf_crop() + end + elseif options.type=='ps' then + psp = psp:eps_to_pdf():pdf_to_ps() + end + return psp + + elseif psp.type=='ps' then + -- preliminary: + -- copy infile to a file in the temp directory, for gs -dSAFER + psp.path = mktemp(psp.type) + slice_file(infile, psp.path) + + -- actual conversion + if options.type=='eps' then + return psp:ps_to_pdf():pdf_to_eps() + elseif options.type=='pdf' then + if options.bbox or options.page then + return psp:ps_to_pdf():pdf_crop() + else + return psp:ps_to_pdf() + end + elseif options.type=='ps' then + if options.gray or options.page then + return psp:ps_to_pdf():pdf_to_ps() + else + return psp -- no conversion necessary + end + end -- pdf => ps + + elseif psp.type=='pdf' then + -- preliminary: + -- copy infile to a file in the temp directory, for gs -dSAFER + psp.path = mktemp(psp.type) + slice_file(infile, psp.path) + + -- actual conversion + if options.type=='eps' then + if options.gray then + -- one-step grayscaling available for gs/ps but not for gs/eps + return psp:pdf_to_pdf():pdf_to_eps() + else + return psp:pdf_to_eps() + end + elseif options.type=='pdf' then + -- pdf_crop can take care of bbox and page, + -- but not of gray and not of target use or pdf version + do + local need_gs = false + -- compare actual and required versions, + -- allowing for rounding differences + if settings.pdf_version~='default' and + (psp.maver+0.1*psp.miver)>tonumber(settings.pdf_version)-0.01 then + need_gs = true + end + if settings.pdf_target~='default' then + need_gs = true + end + if options.gray then + need_gs = true + end + local need_crop = false + if options.bbox then + need_crop = true + end + if (not need_gs) and options.page then + need_crop = true + end + if need_gs then + psp = psp:pdf_to_pdf() + end + if need_crop or (psp.pages>1 and options.page) then + psp = psp:pdf_crop() + end + return psp + end + elseif options.type=='ps' then + if options.gray and pdftops then + return psp:pdf_to_pdf():pdf_to_ps() + else + return psp:pdf_to_ps() + end + end -- pdf => ps + end -- psp.type=='ps'|'pdf' +end -- any_to_any + +-- start logging --------------------------------- + +-- log rotate if logfile too big +if lfs.attributes(logfile) and lfs.attributes(logfile).size > 100000 then + if lfs.attributes(oldlog) then + if os.remove(oldlog) then os.rename(logfile,oldlog) end + elseif lfs.attributes(logfile) then do + -- separate epsdir runs with empty lines + local f = io.open(logfile, 'ab') + f:write(eol) + f:close() + end end -- do elseif +end -- if lfs...logfile + +write_log('epspdf '..table.concat(arg, ' ')) + +infile = false +outfile = false + +-- some debug output + +-- dbg ('os is ' .. os.type .. ' and ' .. os.name) +-- dbg ('texlua in ' .. os.selfdir) +-- dbg('Ghostscript: ' .. gs_prog) + +-- dbg('\nSettings are:\n') +-- for k,v in pairs(settings) do dbg(k .. ' = ' .. tostring(v)) end + +-- Handle command-line + +do + + read_settings(rcfile) + + -- dbg('Defining cmdline options') + opts = {} + + opts.page = { + type = 'string', val = nil, + forms = {'-p', '--page', '--pagenumber'}, + placeholder = 'PNUM', + negforms = nil, + help = 'Page number; must be a positive integer' + } + + opts.gray = { + type = 'boolean', val = nil, + forms = {'-g', '--grey', '--gray', '-G', '--GREY', '--GRAY'}, + negforms = nil, + help = 'Convert to grayscale' + } + + opts.bbox = { + type = 'boolean', val = nil, + forms = {'-b', '--bbox', '--BoundingBox'}, + negforms = nil, + help = 'Compute tight boundingbox' + } + +---[[ ignored; included for backward compatibility + opts.use_hires_bb = { + type = 'boolean', val = nil, + forms = {'-r', '--hires'}, + negforms = {'-n', '--no-hires'}, + } + + opts.custom = { + type = 'string', val = nil, + forms = {'-C', '--custom', '-P', '--psoptions'}, + negforms = nil + } + --]] + + opts.pdf_target = { + type = 'string', val = nil, + forms = {'-T', '--target'}, + placeholder = 'TARGET', + negforms = nil, + help = descriptions.pdf_target + } + opts.pdf_version = { + type = 'string', val = nil, + forms = {'-N', '--pdfversion'}, + placeholder = 'VERSION', + negforms = nil, + help = descriptions.pdf_version + } + + if os.type=='windows' and not is_tl_w then + opts.pdftops_prog = { + type = 'string', val = nil, + forms = {'--pdftops'}, + placeholder = 'PATH', + negforms = nil, + help = descriptions.pdftops_prog + } + end + + opts.use_pdftops = { + type = 'boolean', val = nil, + forms = {'-U'}, + negforms = {'-I'}, + help = descriptions.use_pdftops + } + + opts.info = { + type = 'boolean', val = nil, + forms = {'-i', '--info'}, + negforms = nil, + help = 'Info: display detected filetype and exit' + } + + opts.help = { + type = 'boolean', val = nil, + forms = {'-h', '--help'}, + negforms = nil, + help = 'Display this help message and exit' + } + + opts.version = { + type = 'boolean', val = nil, + forms = {'-v', '--version'}, + negforms = nil, + help = 'Display version info and exit' + } + + opts.save = { + type = 'boolean', val = nil, + forms = {'-s', '--save'}, + negforms = nil, + help = 'Save some settings to configuration file' + } + + opts.debug = { + type = 'boolean', val = nil, + forms = {'-d'}, + negforms = nil, + help = 'Debug: do not remove temp files' + } + + opts.gui = { + type = 'string', val = nil, + forms = {'--gui'}, + negforms = nil, + help = nil -- reserved for use by epspdftk + } + + if #arg < 1 then help('No parameters') end + + -- command-line parsing + + -- -r="tata tata" is parsed by [tex]lua as a single argument + -- lua/linux retains the quotes, + -- lua/windows strips them. + -- texlua strips them, both on unix and on windows. + + local i=1 + while i<=#arg and string.sub(arg[i],1,1)=='-' do + -- dbg('parse argument '..tostring(i)..': '..arg[i]) + local parsed = false + local kk, vv = string.match(arg[i],'([^=]+)=(.*)$') + if kk==nil then + kk = arg[i] -- also vv==nil + else + vv = strip_outer_spaces(vv) + end + for p, o in pairs(opts) do + -- dbg(' try '..p) + if in_list(kk, o.forms) or in_list(kk, o.negforms) then + parsed = true + if o.type == 'boolean' then + if vv then help(kk..' should not have a parameter.') end + if in_list(kk, o.forms) then + o.val = true + else + o.val = false + end + elseif vv then + o.val = vv + else + i = i + 1 + if i>#arg then + help('Missing parameter to '..kk) + end + o.val = strip_outer_spaces(arg[i]) + end -- testing for o.type or vv + break -- for + end -- if in_list + end -- for + if not parsed then help('illegal parameter '..kk) end + i = i + 1 + end -- while + + -- some debug output + + --[[ + if i<=#arg then + dbg('non-option arguments:') + for j=i,#arg do dbg(arg[j]) end + dbg(eol) + else + dbg('no non-option arguments') + end + + for i=1,#arg do dbg(arg[i]) end + + dbg(eol..'Options from command-line:') + for p, o in pairs(opts) do + if o.val==nil then + dbg(p..': undefined') + else + dbg(p..': '..tostring(o.val)) + end + end + --]] + + -- check and interpret opts. + -- Copy to either settings or to options table. + -- abort (via help function) at syntax error. + + -- page + + if opts.page.val then + local pnum = tonumber(opts.page.val) + if pnum<=0 or math.floor(pnum) ~= pnum then + help(opts.page.val..' not a positive integer') + else + options.page = pnum + end + end + + -- grayscaling + + if opts.gray.val then + options.gray = true + else + options.gray = false + end + + -- boundingbox + + if opts.bbox.val then + options.bbox = true + else + options.bbox = false + end + + --[[ + -- using hires boundingbox + + if opts.use_hires_bb.val~=nil then + settings.use_hires_bb = opts.use_hires_bb.val + end + --]] + + -- using pdftops + + if opts.use_pdftops.val~=nil then + settings.use_pdftops = opts.use_pdftops.val + end + + -- pdf target use + + if opts.pdf_target.val~=nil then + if in_list(opts.pdf_target.val, pdf_targets) then + settings.pdf_target = opts.pdf_target.val + else + help('Illegal value '..opts.pdf_target.val..' for pdf_target') + end + end + + -- pdf version + + if opts.pdf_version.val~=nil then + if in_list(opts.pdf_version.val, pdf_versions) then + settings.pdf_version = opts.pdf_version.val + else + help('Illegal value '..opts.pdf_version.val..' for pdf_version') + end + end + + -- pdftops program + + -- pdftops has already been been initialized to false + if os.type=='windows' and not is_tl_w and opts.pdftops_prog.val then + settings.pdftops_prog = is_prog(opts.pdftops_prog.val) + if settings.use_pdftops then + pdftops = settings.pdftops_prog + end + elseif os.type=='windows' and not is_tl_w then + if settings.use_pdftops then + pdftops = is_prog(settings.pdftops_prog) + end + elseif os.type=='windows' then + if settings.use_pdftops then + pdftops = os.selfdir..'/pdftops.exe' + end + else + if settings.use_pdftops then + pdftops = find_on_path('pdftops') + end + end + -- dbg('Option handling; pdftops is '..tostring(pdftops)) + + -- other options + + if opts.save.val then + write_settings(rcfile) + end + + if opts.debug.val then + options.debug = true + end + + if opts.info.val then + options.info = true + end + + if opts.help.val then + help() + end + + if opts.version.val then + show_version() + os.exit() + end + + if opts.gui.val then + gui(opts.gui.val) + end + + -- now we need 1 or 2 filenames, unless the user really only + -- wanted to save options without further action. + + if i>#arg then + if opts.save.val then os.exit() else help('No filenames') end + end + + infile = arg[i] + if i<#arg then + outfile = arg[i+1] + else + outfile = false + end + if (#arg>i and options.info) or (#arg>i+1) then + help('Surplus non-option parameters') + end + + -- one final quick option + if opts.info.val then + info(infile) + end + + -- add pdf_version and pdf_target to the options array, + -- from where it will be set to false when realized + if settings.pdf_target == 'default' then + options.pdf_target = false + else + options.pdf_target = settings.pdf_target + end + if settings.pdf_version == 'default' then + options.pdf_version = false + else + options.pdf_version = tonumber(settings.pdf_version) + end + +end -- decoding command-line + +-- dbg('After command-line processing\n Settings') +-- -- print settings- and options array with dbg +-- for k, v in pairs(settings) do +-- dbg(k..': '..tostring(v)) +-- end +-- dbg(' Options') +-- for k, v in pairs(options) do +-- dbg(k..': '..tostring(v)) +-- end + +--[[ + +Once it becomes clear that real work needs to be done, +we shall create a temp directory in the parent directory of the output file +and use that as working directory. + +1. consistent with the ghostscript -dSAFER option +2. we can move/rename rather than copy the final temp file + to the output file + + because of gs -dSAFER restrictions, infile must be in (a + subdirectory of) the directory of the output file, e.g. in the + temp directory. + + Also because of -dSAFER, we copy infile to the temp directory of + it is not in the same directory as outfile. + +--]] + +do + local source = io.open(infile) + if not source then + error(infile .. ' not readable') + end + source:close() + local in_dir + infile, in_dir = absolute_path(infile) + + -- we need a writable dest_dir as parent for a temp directory, + -- in some cases even for option info + if not outfile then + dest_dir = in_dir + else + outfile, dest_dir = absolute_path(outfile) + end + lfs.chdir(dest_dir) + tempdir = os.tmpdir() -- relative path! + local c, e + c, e = lfs.chdir(tempdir) + if not c then + write_log(e) + tempdir = false + -- errror('Failure to create temporary directory') + else + tempdir = lfs.currentdir() -- better for logging: absolute path + write_log('Working directory: '..tempdir) + end + + infile, source_dir = absolute_path(infile) + intype = identify(infile) + + -- remaining cases: want a real conversion + if not intype then + error(infile..' has an unsupported filetype') + end + + if not outfile then + -- derive outfile from infile: [e]ps => pdf, pdf => eps + if intype=='pdf' then + outfile = string.gsub(infile,'%.[^%.]*$','eps') + else + outfile = string.gsub(infile,'%.[^%.]*$','.pdf') + end + end + + --sanity check on output filetype + options.type = string.match(outfile, '.*%.([^%.]+)$') + if not options.type or (options.type~='ps' + and options.type~='eps' and options.type~='pdf') then + errror('Output file '..outfile.. + ' should have extension .eps, .ps or .pdf') + end + + -- if outfile equal to infile, copy to temp directory, then remove + if outfile==infile then + infile = mktemp(intype) + slice_file(outfile, infile) + write_log('Copying '..outfile..' to temporary file '..infile..'.') + end + + -- had some trouble under msw when removing outfile later so do it now + if lfs.isfile(outfile) then + os.remove(outfile) + if lfs.attributes(outfile) then + errror('Cannot overwrite '..outfile) + end + end + + local fout = io.open(outfile, 'w') + if not fout then + errror('Output file '..outfile..' not writable; aborting') + else + fout:close() + end + + source = PsPdf:from_path(infile) + dest = source:any_to_any() + -- options will be read from the global options table + -- and turned off after they have been satisfied. + -- irrelevant options are quietly ignored. + + if os.type=='unix' then + write_log('Rename '..dest.path..' to '..outfile) + os.rename(dest.path, outfile) -- we picked our temp dir to make this possible + else + write_log('Copying '..dest.path..' to '..outfile) + slice_file(dest.path, outfile) + end + if not options.debug then + cleantemp() + end + if lfs.isfile(outfile) and lfs.attributes(outfile, 'size')>0 then + os.exit() + else + errror('Conversion failed') + end +end diff --git a/Master/texmf-dist/scripts/epspdf/epspdfrc.rb b/Master/texmf-dist/scripts/epspdf/epspdfrc.rb deleted file mode 100755 index a6ea573cf11..00000000000 --- a/Master/texmf-dist/scripts/epspdf/epspdfrc.rb +++ /dev/null @@ -1,486 +0,0 @@ -EPVERSION = '0.5.3' -COPYRIGHT = '2006, 2008, 2009, 2010, 2011' - -# epspdf conversion utility, configuration module - -##### -# 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! -##### - -# Settings are stored in a settings hash, with some methods added -# for less unwieldy notation. -# -# In phase 1, some programs (converters) are autodetected. -# In phase 2, user preferences are read from the epspdf configuration. -# -# For unix/osx, configuration info is stored in $HOME/.epspdfrc. -# For w32, configuration info is stored in HKCU\software\epspdf. -# -# The UIs for settings are not here; they are: -# 1. the epspdf.rb command-line if epspdf.rb is run as -# a stand-alone program, and -# 2. a configuration screen in epspdftk.rb. - -# for notational convenience, we keep ARCH separate - -ARCH = case RUBY_PLATFORM -when /win32|mingw/ - 'w32' -when /darwin/ - 'osx' -else - 'unix' -end - -if ARCH == 'w32' - # registry and API calls - require 'dl/win32' - require 'win32/registry' - include Win32 # this lets us abbreviate Win32::Registry to Registry - # lay groundwork for short_name function - ShortPName = Win32API.new( - 'kernel32', 'GetShortPathName', ['P','P','N'], 'N' ) - # shellopen function - ShellEx = Win32API.new( - 'shell32','ShellExecute', ['L','P','P','P','P','L'], 'L' ) - #FindEx = Win32API.new( - # 'shell32','FindExecutable', ['P','P','P'], 'L' ) - # from winuser.h: - SW_SHOWNORMAL = 1 -end - -# need short_name function to get around limitations of w2k quoting -def short_name( fn ) - #puts "Calculate short name for #{fn}\n" - return fn if ARCH != 'w32' - fn.gsub!( /\//, "\\" ) - buffer = ' ' * 260 - length = ShortPName.call( fn, buffer, buffer.size ) - fn = buffer.slice(0..length-1) if length > 0 - fn.gsub!( /\\/, '/' ) - return fn -end - -def shell_open( fn ) - return nil if ARCH != 'w32' - result = ShellEx.call( 0, 0, fn, 0, 0, SW_SHOWNORMAL ) -end - -def shell_open_with( fn ) - return nil if ARCH != 'w32' - result = ShellEx.call( 0, 0, "RUNDLL32.EXE", "shell32.dll,OpenAs_RunDLL #{fn}", - 0, SW_SHOWNORMAL ) -end - -# interpret error value for shell_open -# values <=32 indicate error -# constants defined in winerror.h, shellapi.h - -def shell_error_string ( e ) - if e > 32 - return nil - else - return case e - when 0 then "Out of memory or resources" - #define ERROR_FILE_NOT_FOUND 2L - #define SE_ERR_FNF 2 - when 2 then "File not found" - #define ERROR_PATH_NOT_FOUND 3L - #define SE_ERR_PNF 3 - when 3 then "Path not found" - #define SE_ERR_ACCESSDENIED 5 - when 5 then "Access denied" - #define SE_ERR_OOM 8 - when 8 then "Not enough memory" - #define ERROR_BAD_FORMAT 11L - when 11 then "Invalid exe" - #define SE_ERR_SHARE 26 - when 26 then "Sharing violation" - #define SE_ERR_ASSOCINCOMPLETE 27 - when 27 then "Invalid file association" - #define SE_ERR_DDETIMEOUT 28 - when 28 then "DDE timeout" - #define SE_ERR_DDEFAIL 29 - when 29 then "DDE fail" - #define SE_ERR_DDEBUSY 30 - when 30 then "DDE busy" - #define SE_ERR_NOASSOC 31 - when 31 then "No file association" - #define SE_ERR_DLLNOTFOUND 32 - when 32 then "DLL not found" - else "Unspecified error" - end # case - end # else -end # def - -# system-dependent location of logfile - -epsdir = '' -case ARCH - when 'w32' - Registry::HKEY_CURRENT_USER.open( 'Software\Microsoft\Windows' + - '\CurrentVersion\Explorer\User Shell Folders' ) do |r| - epsdir = r.read_s_expand( 'AppData' ) - end - epsdir = "#{epsdir.gsub( /\\/, '/' )}/epspdf" - else - epsdir = "#{ENV['HOME']}/.epspdf" -end -Dir.mkdir( epsdir ) unless test( ?e, epsdir ) -# epsdir may be a regular file, or creation above may have failed -fail("Cannot create directory #{epsdir}" ) if not test( ?d, epsdir ) -LOGFILE = "#{epsdir}/epspdf.log" -LOGFILE_OLD = LOGFILE + '.old' -#puts LOGFILE - -# system-dependent locations of saved settings - -RC_FILE = "#{epsdir}/config" - -# hash of saved settings from rc file or registry - -$rc_settings = {} - -# test whether a string is a valid program call -# 'which' also works with explicit paths. -# 'which' returns an error for non-executable files. -# Linux/Unix/OS X: for testing existence of gs and pdftops -# Windows: function not usable and not used - -def is_a_program( s ) - case ARCH - when 'w32' - nil - else - ( system "which #{s} >/dev/null 2>&1" ) ? 1 : nil - end -end # is_a_program - -# for Windows, use the registry or the following function instead: - -def find_on_path( s ) - # for now, Windows-only; parameter s with or without path - if ARCH != 'w32' - return nil - end - ENV['PATH'].gsub( "\\", "/" ).split( /;/ ).each do |d| - d = File.expand_path( d ) # this seems a syntactic operation - s_full = d.sub( /\/$/, '' ) + '/' + s - if test( ?d, d ) and test( ?f, s_full ) - return s_full - end - ENV['PATHEXT'].split( /;/ ).each do |e| - # components of PATHEXT should include leading dot - s_full = d.sub( /\/$/, '' ) + '/' + s + e - if test( ?f, s_full ) - return s_full - end - end # do |e| - end # do |d| - return nil -end - -class Setting - attr_reader :val, :type, :comment - attr_writer :val - - def initialize( v, t, c ) - @val = v - @type = t - @comment = c - end -end - -PDF_TARGETS = [ 'default', 'printer', 'prepress', 'screen', 'ebook' ] -PDF_VERSIONS = [ '1.2', '1.3', '1.4', 'default' ] - -$settings = { - # converters - # ghostscript: under windows it must be told about its directories, - # so we don't want users to mess with it. - # If they really want to, they can manually edit PATH and set GS_LIB - 'gs_prog' => Setting.new( nil, 'auto', nil ), - #'gs_version' => Setting.new( nil, 'auto', nil ), - 'pdftops_prog' => Setting.new( nil, ARCH=='w32' ? 'config' : 'auto', nil ), - # epspdftk: viewers; epspdftk.tcl takes care of this, - # but their configuration is stored and retrieved by the Ruby scripts - 'pdf_viewer' => Setting.new( nil, ARCH=='unix' ? 'config' : 'auto', nil ), - 'ps_viewer' => Setting.new( nil, ARCH=='unix' ? 'config' : 'auto', nil ), - # epspdftk: initial dir - 'defaultDir' => Setting.new( nil, 'config', nil ), - # conversion options - 'ignore_pdftops' => Setting.new( '0', 'config', - 'Ignore pdftops even if available; 1=yes, empty or 0=no(default)' ), - 'pdf_target' => Setting.new( 'prepress', 'config', - 'Target: screen, ebook, print, prepress (default) or default' ), - 'pdf_version' => Setting.new( 'default', 'config', - 'Pdf version: e.g 1.4 or 1.2 or default' ), - 'pdf_custom' => Setting.new( '', 'config', - 'additional options for [e]ps to pdf conversion' ), - 'ps_options' => Setting.new( '-level3', 'config', - 'options for pdftops; default -level3' ), - 'ignore_hires_bb' => Setting.new( '0', 'config', - 'Ignore hi-res boundingbox for pdf generation;' + - ' 1=yes, empty or 0=no(default)' ), - 'bb_spread' => Setting.new( '1', 'config', - 'margin in points to be added to computed lores boundingbox; default 1' ) -} - -# Using pdftops or a hires boundingbox should be the norm, not the exception. -# We emphasize this by naming the corresponding options ignore_pdftops and -# ignore_hires_bb, and letting them default to false. - -def reverse_of( x ) - case x - when /t(rue)?|y(es)?|1/i - false - when /f(alse)?|n(o)?|0/i - true - when 1 - false - when 0 - true - else - true - end -end - -class << $settings - - # create shortcut methods $settings.x and $settings.x= - # for reading and writing hash elements. - - $settings.each_key { |k| - eval "def #{k} ; self[\'#{k}\'].val ; end" - eval "def #{k}=(v) ; self[\'#{k}\'].val=v ; end" - } - - # use_pdftops and use_hires_bb are a boolean counterparts and inverses - # of the stored string attributes ignore_pdftops and ignore_hires_bb - - def use_hires_bb - reverse_of( self['ignore_hires_bb'].val ) - end - - def use_pdftops - reverse_of( self['ignore_pdftops'].val ) - end - - # previously-configured settings into hash $rc_settings - # here no validity testing. - # we accept empty settings. - - def read_settings - if test( ?s, RC_FILE ) - lines = File.read( RC_FILE ).split( /\r\n?|\n/ ) - lines.each do |l| - l = l.sub( /#.*$/, '' ) # remove trailing comments - if l =~ /^\s*(\S+)\s*=\s*(\S(.*\S)?)\s*$/ - key, val = $1, $2 - $rc_settings[ key ] = val \ - if $settings[ key ] and $settings[ key ].type == 'config' - elsif l =~ /^\s*(\S+)\s*=\s*$/ - key, val = $1, '' - $rc_settings[ key ] = '' \ - if $settings[ key ] and $settings[ key ].type == 'config' - end # if l =~ - end # do |l| - end # if ?s - end # def - - def write_settings - File.open( RC_FILE, 'w' ) do |f| - f.write( "# This file will be overwritten by epspdf.rb/epspdftk.tcl" \ - + $/ ) - $settings.each do |key, st| - if st.type == 'config' - f.write( $/ ) - f.write( '# ' + st.comment + $/ ) if st.comment - # STDERR.puts "write setting for " + key - # STDERR.flush - f.write( key + " = " + ( st.val ? st.val : '' ) + $/ ) - end - end # do |key, st| - end # do |f| - end # def - - def get_settings - - # phase 1: autodetect - - # w32: gs_prog, pdftops_prog - # gs: search path; search registry settings (user and system); - # search tex installation. The first one wins. - # pdftops: search path - # unix: gs_prog, pdftops_prog - # osx: gs_prog, pdftops_prog - # unix/osx are done together. - - # Windows: test for TeX, because it may come with a hidden - # ghostscript. TeX may also come with pdftops.exe, but pdftops - # would then be on the searchpath and not require special treatment. - - texbindir = ( ARCH == 'w32' ) ? find_on_path( 'tex' ) : nil - texbindir = short_name( File.dirname( texbindir ) ) if texbindir - - case ARCH - when 'w32' - # ghostscript: - # 1. check searchpath - # 2. check registry for latest valid version of Ghostscript - # 3. check tex installation - # TeX Live- and MikTeX versions need environment variables - # no need to remember: it will be the last thing we try - # searchpath - self.gs_prog = find_on_path( 'gswin32c' ) - - if not self.gs_prog - try_gs_version = 0 - try_gs_prog = nil - this_gs_prog = nil - # tentative values; HKLM and HKCU may provide different values - [Registry::HKEY_LOCAL_MACHINE, - Registry::HKEY_CURRENT_USER].each { |hk| - puts hk.to_s if $DEBUG - hk.open('SOFTWARE') { |sof| - puts sof.to_s if $DEBUG - sof.each_key { |key, wtime| - puts key.to_s if $DEBUG - if key =~ /ghostscript/i - gh = hk.open('SOFTWARE\\' + key) - gh.each_key { |this_gs_version, wtime| - if this_gs_version =~ /^\d+\.\d+/ # version number - if this_gs_version.to_f > try_gs_version.to_f - this_gs = hk.open('SOFTWARE\\' + key + - '\\' + this_gs_version) - this_gs_prog = this_gs['GS_DLL'].sub( - /([\\\/])([^\\\/]+)$/, '\1gswin32c.exe') - if test(?e,this_gs_prog) - try_gs_version = this_gs_version - try_gs_prog = this_gs_prog - end # if ?e (exist) - end # this_gs_version.to_f > try_gs_version.to_f - end # if this_gs_version =~ /^\d+\.\d+$/ - } # gh.each_key - end # if key =~ /ghostscript/i - } # sof.each_key - } # hk.open |sof| - } # [HKCU, HKLM].each - self.gs_prog = this_gs_prog if this_gs_prog - end # if not self.gs_prog - if texbindir and ! self.gs_prog - # TeX Live >= 2008: - # hidden gs in texbindir/../../tlpkg/tlgs - texroot = File.dirname( File.dirname( File.expand_path( texbindir ))) - gsroot = texroot + '/tlpkg/tlgs' - this_gs_prog = gsroot + '/bin/gswin32c.exe' - if test( ?f, this_gs_prog ) - self.gs_prog = this_gs_prog - ENV['GS_LIB'] = "#{gsroot}/lib;#{gsroot}/fonts" - if test( ?d, "#{gsroot}/Resource" ) - ENV['GS_LIB'] += ";#{gsroot}/Resource" - end - else # test for MikTeX hidden ghostscript - # http://blog.miktex.org/post/2005/04/ - # Starting-mgsexe-at-the-DOS-Prompt.aspx - # That's because mgs.exe doesn't use the original registry keys - # and environment variables. For example, mgs.exe queries - # MIKTEX_GS_LIB instead of GS_LIB. You can start mgs.exe - # at the DOS-prompt if you set MIKTEX_GS_LIB as follows: - # MIKTEX_GS_LIB=C:\texmf\ghostscript\base;C:\texmf\fonts - this_gs_prog = texbindir + '/mgs.exe' - if test( ?f, this_gs_prog ) and - test( ?d, "#{texroot}/ghostscript/base" ) - self.gs_prog = this_gs_prog - ENV['MIKTEX_GS_LIB'] = - "#{texroot}/ghostscript/base;#{texroot}/fonts" - end # ?f, this_gs_prog - end # try MikTeX hidden Ghostscript - end # texbindir and ! self.gs_prog - fail( "No ghostscript" ) unless self.gs_prog - self.gs_prog = short_name( self.gs_prog ) - - # pdftops: just check the searchpath - self.pdftops_prog = find_on_path( 'pdftops' ) - - when /unix|osx/ - # gs - self.gs_prog = is_a_program( 'gs' ) ? 'gs' : nil - fail( "No ghostscript" ) unless self.gs_prog - - # pdftops - self.pdftops_prog = is_a_program( 'pdftops' ) ? 'pdftops' : nil - - end # case ARCH - - # built-in defaults already set during initialization of $settings - - # phase 2: pre-existing configuration - # w32: pdftops_prog - # unix (not osx): pdf_viewers - - read_settings - - if ARCH == 'w32' - self.pdftops_prog = $rc_settings[ 'pdftops_prog' ] if - $rc_settings[ 'pdftops_prog' ] and - ! $rc_settings[ 'pdftops_prog' ].empty? and - test( ?f, $rc_settings[ 'pdftops_prog' ] ) - end # ARCH == 'w32' - - if ARCH == 'unix' - self.pdf_viewer = $rc_settings[ 'pdf_viewer' ] # no checks here - self.ps_viewer = $rc_settings[ 'ps_viewer' ] - end - - if ( $rc_settings[ 'defaultDir' ] and - ( not $rc_settings[ 'defaultDir' ].strip.empty? ) and - test( ?d, $rc_settings[ 'defaultDir' ] ) ) - self.defaultDir = File.expand_path( $rc_settings[ 'defaultDir' ] ) - else - self.defaultDir = Dir.getwd - end - #print "defaultDir set to #{self.defaultDir} by epspdfrc.getsettings" - - - # no validity checks for pdf- and ps output options. - # reminder: self.s shortcut for self['s'].val - [ 'pdf_target', 'pdf_version', 'pdf_custom', 'ps_options' ].each { |p| - self[ p ].val = $rc_settings[ p ] if $rc_settings[ p ] - } - if $rc_settings.has_key?( 'ignore_pdftops' ) - self.ignore_pdftops = case $rc_settings[ 'ignore_pdftops' ] - when /1|yes|true|y|t/i - '1' - when /0|no|false|n|f/i - '0' - end - else - self.ignore_pdftops = '0' - end - - if $rc_settings.has_key?( 'ignore_hires_bb' ) - self.ignore_hires_bb = case $rc_settings[ 'ignore_hires_bb' ] - when /1|yes|true|y|t/i - '1' - when /0|no|false|n|f/i - '0' - end - else - self.ignore_hires_bb = '0' - end - - # bb_spread has an effect only if ignore_hires_bb - self.bb_spread = $rc_settings[ 'bb_spread' ] if - $rc_settings.has_key?( 'bb_spread' ) and - $rc_settings[ 'bb_spread' ] =~ /^[+-]?[\d]+$/ - - end # get_settings - -end # class << $settings - -$settings.get_settings diff --git a/Master/texmf-dist/scripts/epspdf/epspdftk.tcl b/Master/texmf-dist/scripts/epspdf/epspdftk.tcl index 361ac83d6b1..32bb93011e9 100755 --- a/Master/texmf-dist/scripts/epspdf/epspdftk.tcl +++ b/Master/texmf-dist/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/Master/texmf-dist/scripts/epspdf/makegray.pro b/Master/texmf-dist/scripts/epspdf/makegray.pro deleted file mode 100644 index 6206e559560..00000000000 --- a/Master/texmf-dist/scripts/epspdf/makegray.pro +++ /dev/null @@ -1,33 +0,0 @@ -%! -% makegray.pro, PostScript header file for grayscale conversion. - -%%%%% -% Siep Kroonenberg, n dot s dot kroonenberg at rug dot nl -% Public domain. -%%%%% - -% This version only handles some basic color models, -% and doesn't handle image data at all. -% A more robust version would be warmly welcomed. - -%%%%<< /ProcessColorModel /DeviceGray >> setpagedevice -userdict begin -% .11*B + .59*G + .3*R -/setrgbcolor { - 0.11 mul exch 0.59 mul add exch 0.3 mul add - systemdict begin setgray end -} def -% 1.0 - min(1.0, .3*C + .59*M + .11*Y + B) -/setcmykcolor { - exch 0.11 mul add - exch 0.59 mul add - exch 0.3 mul add - dup 1 gt {pop 1} if - 1 exch sub - systemdict begin setgray end -} def -% hsb: toss hue and saturation; keep brightness for gray setting -/sethsbcolor { - pop pop systemdict begin setgray end -} def -end |