#!/usr/bin/perl
#$Id: jpeg2eps,v 1.7 2003/11/21 14:30:12 danlee Exp $
#
# Copyright Lee Sau Dan (c) 2002, 2003
#
# Converts jpeg to EPS (Postscript Level 2)
#
#     This program is free software; you can redistribute it and/or modify
#     it under the terms of the GNU General Public License as published by
#     the Free Software Foundation; either version 2 of the License, or
#     (at your option) any later version.
#
#     This program is distributed in the hope that it will be useful,
#     but WITHOUT ANY WARRANTY; without even the implied warranty of
#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#     GNU General Public License for more details.
#
#     You should have received a copy of the GNU General Public License
#     along with this program; if not, write to the Free Software
#     Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#     or download it from http://www.gnu.org/licenses/gpl.txt
#

use strict;
use Fcntl 'SEEK_SET', 'SEEK_CUR';
use Getopt::Long;
use File::Basename;
use POSIX 'ceil';


our $version = version_string('$Revision: 1.7 $', '$Name:  $');
our $opt_version;
our $opt_help;
our $opt_dpi;
our $opt_interpolate;
our $dpi = 300; # default to 300dpi
our $jpeg_filename;
our ($ps_width, $ps_height);

sub show_version {
    my $out = shift;
    print $out <<EOF;
jpeg2eps $version
Copyright 2002, 2003 by LEE Sau Dan <danlee\@informatik.uni-freiburg.de>
This program is released under the GNU General Public License
<http://www.gnu.org/licenses/gpl.txt>
EOF
}
sub show_usage {
    my $out = shift;
    show_version($out);
    print $out "
Usage: jpeg2eps [options] jpeg_file [eps_file]
    Convert jpegfile into an EPS file.  If eps_file is not specified,
    the result goes to standard output.

    jpeg2eps is \"lossless\" in the sense that it passes the JPEG data
    to the  output file, which  a Postscript Level 2  interpreter will
    read,  without  changing  the  contents.   (It  just  encodes  the
    contents  so that  it is  ASCII compatible.)   So,  the Postscript
    interpreter gets the original  JPEG data.  This is quite different
    from the approach of first  decompressing the JPEG file (e.g. with
    'djpeg') into a bitmap file  (e.g. pnm) and then encapsulating the
    bitmap file into an EPS file.  Such an approach would increase the
    file size because of the decompression, as well as not keeping the
    original image data.  jpeg2eps, by contrast, directly encapsulates
    the JPEG  file into the output  file.  It is thus  faster and lets
    the Postscript interpreter see the originial JPEG data stream, which
    is decompressed at the Postscript interpreter.

Options:
--version   Show version of this program
--help      Show this help message
--dpi=<float>  Specify the image resolution, in dots-per-inch. (default: 300)
--width=<measure>
--height=<measure>
            Specify the desired size of the image in the EPS file.
            <measure> takes the form: <float><unit>, where <unit>
            is 'in' for inch, 'cm' for centimetre and 'pt' for 
            Postscript points.  An omitted <unit> is equivalent to 'pt'.
            If only width or height is given, the other is determined by
            keeping the aspect ratio or the image.  You may not
            specify --dpi together with these options.
--interpolate

            Causes the generated Postscript code to exploit the
            \"interpolation\" feature.  When your image has a lower
            resolution than the device resolution, enabling this
            option *may* make the output look smoother on the device.
            But the Postscript interpreter may need much more time to
            handle such an image.  Note that this option doesn't
            affect the output file size (up to a few bytes), because
            the interpolation is done by the Postscript interpreter,
            not this program.  This option only adds code to enable
            that feature in the Postscript interpreter.
";
}

unless (GetOptions("version"  => \$opt_version,
		   "help" => \$opt_help,
		   "dpi=f" => \$opt_dpi,
		   "width=s" => \$ps_width,
		   "height=s" => \$ps_height,
		   "interpolate" => \$opt_interpolate,
		   )) {
    show_usage(*STDERR{IO});
    exit 1;
}
if ($opt_help) { show_usage(*STDOUT{IO}); exit 0; }
if ($opt_version) { show_version(*STDOUT{IO}); exit 0; }
if ($opt_dpi) {
    $dpi = $opt_dpi;
    die "dpi must be non-zero\n" if ($dpi == 0);
}
if ($ps_width) {
    die "You cannot specify both --width and --dpi together\n"
	if $opt_dpi;
    $ps_width = parse_measure($ps_width);
    die "Bad width specification '$ps_width'\n" unless $ps_width;
}
if ($ps_height) {
    die "You cannot specify both --height and --dpi together\n"
	if $opt_dpi;
    $ps_height = parse_measure($ps_height);
    die "Bad height specification '$ps_height'\n" unless $ps_height;
}

die "no JPEG file specified"	
    unless ($jpeg_filename = shift @ARGV);
{
    my $out_filename = shift @ARGV;
    if (defined($out_filename)) {
	open(STDOUT, "<$out_filename") || die "Can't open $out_filename: $!\n";
    }
}
warn "extra arguments ignored: " . join(" ", @ARGV)
    if ($#ARGV > 0);

open(JPEG, "<$jpeg_filename") || die "Can't open $jpeg_filename: $!\n";

jpeg_info_init();
my ($width, $height, $nr_cc) = jpeg_get_info(*JPEG{IO});
die "$width\n" unless defined($nr_cc); # error message instead of info

my ($decode, $dcs);
if ($nr_cc == 1) {
    ($decode, $dcs) = ("[0 1]", "/DeviceGray");
} elsif ($nr_cc == 3) {
    ($decode, $dcs) = ("[0 1 0 1 0 1]", "/DeviceRGB");
} elsif ($nr_cc == 4) {
    ($decode, $dcs) = ("[1 0 1 0 1 0 1 0]", "/DeviceCMYK");
} else {
    die "Unsupported: $nr_cc color components";
}

my $interpolate = $opt_interpolate? "/Interpolate true" : "";

if (!defined($ps_width)) {
    if (!defined($ps_height)) {
	# neither width nor height was specified: use dpi
	($ps_width, $ps_height) = ($width * 72 / $dpi, $height * 72 / $dpi);
    } else {
	# have height, but not width: keep aspect ratio
	$ps_width = $width * $ps_height / $height;
	$dpi = $height * 72 / $ps_height;
    }
} else { # we have width specified
    if (!defined($ps_height)) {
	# height not specified: keep aspect ratio
	$ps_height = $height * $ps_width / $width;
	$dpi = $width * 72 / $ps_width;
    } else {
	# aspect ratio may have changed: we may have different DPI on
	# the two dimensions...
	my ($xdpi, $ydpi) =
	    ($width * 72 / $ps_width, $height * 72 / $ps_height);
	if ($xdpi == $ydpi) {
	    $dpi = $xdpi;
	} else {
	    $dpi = "$xdpi $ydpi";
	}
    }
}


my ($ps_width_ceil, $ps_height_ceil) =
    (POSIX::ceil($ps_width), POSIX::ceil($ps_height));
my $title = basename($jpeg_filename, (".jpg", ".JPG", ".jpeg"));
my $date = `date`; chomp $date;
print <<EOF;
%!PS-Adobe-3.0
%%Title: $title
%%Creator: jpeg2eps (C) 2002, 2003 by LEE Sau Dan
%%+        <danlee\@informatik.uni-freiburg.de>
%%+ $version
%%CreationDate: $date
%%BoundingBox: 0 0 $ps_width_ceil $ps_height_ceil
%%HiResBoundingBox: 0 0 $ps_width $ps_height
%%Pages: 1
%%LanguageLevel: 2
%%EndComments
%jpeg2eps: resolution = $dpi
%%EndProlog
%%Page: 1 1
gsave
$dcs setcolorspace
$ps_width $ps_height scale
<</ImageType 1 /Width $width /Height $height
  /BitsPerComponent 8
  /ImageMatrix[$width 0 0 -$height 0 $height]
  /Decode$decode$interpolate
  /DataSource currentfile/ASCII85Decode filter/DCTDecode filter
>>image
EOF

# now, encode and output the image data
seek(JPEG, 0, SEEK_SET) || die "can't rewind: $!\n";
encode_ascii85(*JPEG{IO});
close(JPEG) || die "can't close $jpeg_filename: $!\n";

# trailer
print <<EOF;
grestore
showpage
%%EOF
EOF

exit 0;


### jpeg info extraction routines
our @jpeg_not_supported_markers;
our @jpeg_no_params_markers;
sub jpeg_info_init {
    for my $marker (0xC3,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xCB,0xCD,0xCE,0xCF){
	$jpeg_not_supported_markers[$marker] = 1;
    }
    for my $marker (0xD0,0xD1,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0x01) {
	$jpeg_no_params_markers[$marker] = 1;
    }
}

sub jpeg_get_info { # returns (width, height, nr_cc), or error message
    my $jpeg_ref = shift;
    my $buffer;
    while (read $jpeg_ref, $buffer, 1) {
	next unless ord $buffer == 0xFF; # look for SOF? marker
	my $markertype;
	read($jpeg_ref, $markertype, 1) || return "$!";
	$markertype = ord $markertype;
	if ($markertype >= 0xC0 && $markertype <= 0xC2) {
	    # SOF0==baseline; SOF1==extended sequential; SOF2==progressive
	    read($jpeg_ref, $buffer, 3)==3 || return "$!";
	    return "JPEG file is not 8 bits per component!\n"
		if (ord substr($buffer, 2, 1) != 8);
	    read($jpeg_ref, $buffer, 5)==5 || return "$!";
	    my ($height, $width, $nr_cc) = unpack("nnC", $buffer);
	    return ($width, $height, $nr_cc);
	} elsif ($jpeg_not_supported_markers[$markertype]) {
	    return sprintf("Unsupported JPEG marker (0x$02x)\n", $markertype);
	} else {
	    # skip segment
	    next if ($jpeg_no_params_markers[$markertype]);
	    read($jpeg_ref, $buffer, 2)==2 || return "$!";
	    my $segment_length = unpack("n", $buffer);
	    seek($jpeg_ref, $segment_length-2, SEEK_CUR) || return "$!";
	}
    }
    return "Can't find JPEG markers to determine image size";
}


### ascii85 encoder
sub encode_ascii85 {
    my $in = shift;
    my $buffer;
    my $out_buf;
    my $r;
    while (($r=read($in, $buffer, 4))==4) {
	my $b = unpack("N", $buffer);
	my $c;
	if ($b == 0) {
	    $c = 'z';
	} else {
	    for (my $i = 4; $i >= 0; --$i) {
		$c = chr($b % 85 + ord '!') . $c;
		$b /= 85;
	    }
	}
	$out_buf .= $c;
	print $&, "\n" if ($out_buf =~ s/^.{70}//);
    }
    die if $r>4;
    if ($r > 0) {
	# the remaining tail
	$buffer .= chr(0) x 4;
	my $b = unpack("N", $buffer);
	my $c;
	for (my $i = 4; $i >= 0; --$i) {
	    $c = chr($b % 85 + ord '!') . $c;
	    $b /= 85;
	}
	$out_buf .= substr($c, 0, $r+1);
    }
    $out_buf .= "~>"; # end mark of ASCII85 stream
    while ($out_buf =~ s/^.{70}//) {
	print $&, "\n";
    }
    print $out_buf, "\n" if $out_buf;
}


### misc. routines
sub version_string {
    my($rev, $name) = @_;
    if ($name =~ /^[\$]Name: (.+) [\$]/) {
	$name = $1;
	if ($name =~ s/^r//) {
	    $name =~ s/_/./g;
	    return "Release $name";
	}
	return $name;
    }

    if ($rev =~ /^[\$]Revision: (.*) [\$]/) {
	return "Revision $1";
    } else {
	return "(unknown version)";
    }
}


sub parse_measure { # returns undef, or equivalent measure in points
    my $measure = shift;
    $measure =~ /^([0-9]+(?:\.[0-9]*)?|\.[0-9]+)(pt|in|cm)?$/ || return;
    my ($value, $unit) = ($1, $2);
    return $value * 72 if ($unit eq 'in');
    return $value * 72 / 2.54 if ($unit eq 'cm');
    return $value; # 'in' or no unit specified
}



#bye!