package Foomatic::DB; use Exporter; use Encode; @ISA = qw(Exporter); @EXPORT_OK = qw(normalizename comment_filter get_overview getexecdocs translate_printer_id ); @EXPORT = qw(ppdtoperl ppdfromvartoperl); use Foomatic::Defaults qw(:DEFAULT $DEBUG); use Data::Dumper; use POSIX; # for rounding integers use strict; my $ver = '$Revision$ '; # constructor for Foomatic::DB sub new { my $type = shift(@_); my $this = bless {@_}, $type; return $this; } # A map from the database's internal one-letter driver types to English my %driver_types = ('F' => 'Filter', 'P' => 'Postscript', 'U' => 'Ghostscript Uniprint', 'G' => 'Ghostscript'); # Translate old numerical PostGreSQL printer IDs to the new clear text ones. sub translate_printer_id { my ($oldid) = @_; # Read translation table for the printer IDs my $translation_table = "$libdir/db/oldprinterids"; open TRTAB, "< $translation_table" or return $oldid; while () { chomp; my $searcholdid = quotemeta($oldid); if (/^\s*$searcholdid\s+(\S+)\s*$/) { # ID found, return new ID my $newid = $1; close TRTAB; return $newid; } } # ID not found, return original one close TRTAB; return $oldid; } # List of driver names sub get_driverlist { my ($this) = @_; return $this->_get_xml_filelist('source/driver'); } # List of printer id's sub get_printerlist { my ($this) = @_; return $this->_get_xml_filelist('source/printer'); } sub get_overview { my ($this, $rebuild, $cupsppds) = @_; # "$this->{'overview'}" is a memory cache only for the current process if ((!defined($this->{'overview'})) or (defined($rebuild) and $rebuild)) { my $otype = ($cupsppds ? '-C' : '-O'); $otype .= ' -n' if ($cupsppds == 1); # Generate overview Perl data structure from database my $VAR1; eval (`$bindir/foomatic-combo-xml $otype -l '$libdir' | $bindir/foomatic-perl-data -O`) || die ("Could not run \"foomatic-combo-xml\"/\"foomatic-perl-data\"!"); $this->{'overview'} = $VAR1; } return $this->{'overview'}; } sub get_overview_xml { my ($this, $compile) = @_; open( FCX, "$bindir/foomatic-combo-xml -O -l '$libdir'|") or die "Can't execute $bindir/foomatic-combo-xml -O -l '$libdir'"; $_ = join('', ); close FCX; return $_; } sub get_combo_data_xml { my ($this, $drv, $poid, $withoptions) = @_; # Insert the default option settings if there are some and the user # desires it. my $options = ""; if (($withoptions) && (defined($this->{'dat'}))) { my $dat = $this->{'dat'}; for my $arg (@{$dat->{'args'}}) { my $name = $arg->{'name'}; my $default = $arg->{'default'}; if (($name) && ($default)) { $options .= " -o '$name'='$default'"; } } } open( FCX, "$bindir/foomatic-combo-xml -d '$drv' -p '$poid'$options -l '$libdir'|") or die "Can't execute $bindir/foomatic-combo-xml -d '$drv' -p '$poid'$options -l '$libdir'"; $_ = join('', ); close FCX; return $_; } sub get_printer { my ($this, $poid) = @_; # Generate printer Perl data structure from database my $VAR1; if (-r "$libdir/db/source/printer/$poid.xml") { eval (`$bindir/foomatic-perl-data -P '$libdir/db/source/printer/$poid.xml'`) || die ("Could not run \"foomatic-perl-data\"!"); } else { return undef; } return $VAR1; } sub get_printer_xml { my ($this, $poid) = @_; return $this->_get_object_xml("source/printer/$poid", 1); } sub get_driver { my ($this, $drv) = @_; # Generate driver Perl data structure from database my $VAR1; if (-r "$libdir/db/source/driver/$drv.xml") { eval (`$bindir/foomatic-perl-data -D '$libdir/db/source/driver/$drv.xml'`) || die ("Could not run \"foomatic-perl-data\"!"); } else { return undef; } return $VAR1; } sub get_driver_xml { my ($this, $drv) = @_; return $this->_get_object_xml("source/driver/$drv", 1); } # Utility query function sorts of things: sub get_printers_for_driver { my ($this, $drv) = @_; my $driver = $this->get_driver($drv); if (!defined($driver)) {return undef;} return map { $_->{'id'} } @{$driver->{'printers'}}; } # Routine lookup; just examine the overview sub get_drivers_for_printer { my ($this, $printer) = @_; my @drivers = (); my $over = $this->get_overview(); my $p; for $p (@{$over}) { if ($p->{'id'} eq $printer) { return @{$p->{'drivers'}}; } } return undef; } # This function sorts the options at first by their group membership and # then by their names appearing in the list of functional areas. This way # it will be made easier to build the PPD file with option groups and in # user interfaces options will appear sorted by their functionality. sub sortargs { # All sorting done case-insensitive and characters which are not a # letter or number are taken out!! # List of typical option names to appear at first # The terms must fit to the beginning of the line, terms which must fit # exactly must have '\$' in the end. my @standardopts = ( # The most important composite option "printoutmode", # Options which appear in the "General" group in # CUPS and similar media handling options "pagesize", "papersize", "mediasize", "inputslot", "papersource", "mediasource", "sheetfeeder", "mediafeed", "paperfeed", "manualfeed", "manual", "outputtray", "outputslot", "outtray", "faceup", "facedown", "mediatype", "papertype", "mediaweight", "paperweight", "duplex", "sides", "binding", "tumble", "notumble", "media", "paper", # Other hardware options "inktype", "ink", # Page choice/ordering options "pageset", "pagerange", "pages", "nup", "numberup", # Printout quality, colour/bw "resolution", "gsresolution", "hwresolution", "jclresolution", "fastres", "jclfastres", "quality", "printquality", "printingquality", "printoutquality", "bitsperpixel", "econo", "jclecono", "tonersav", "photomode", "photo", "colormode", "colourmode", "color", "colour", "grayscale", "gray", "monochrome", "mono", "blackonly", "colormodel", "colourmodel", "processcolormodel", "processcolourmodel", "printcolors", "printcolours", "outputtype", "outputmode", "printingmode", "printoutmode", "printmode", "mode", "imagetype", "imagemode", "image", "dithering", "dither", "halftoning", "halftone", "floydsteinberg", "ret\$", "cret\$", "photoret\$", "smooth", # Adjustments "gammacorrection", "gammacorr", "gammageneral", "mastergamma", "stpgamma", "gammablack", "blackgamma", "gammacyan", "cyangamma", "gammamagenta", "magentagamma", "gammayellow", "yellowgamma", "gammared", "redgamma", "gammagreen", "greengamma", "gammablue", "bluegamma", "gamma", "density", "stpdensity", "hpljdensity", "tonerdensity", "inkdensity", "brightness", "stpbrightness", "saturation", "stpsaturation", "hue", "stphue", "tint", "stptint", "contrast", "stpcontrast", "black", "stpblack", "cyan", "stpcyan", "magenta", "stpmagenta", "yellow", "stpyellow", "red", "stpred", "green", "stpgreen", "blue", "stpblue" ); my @standardgroups = ( "general", "media", "quality", "imag", "color", "output", "finish", "stapl", "extra", "install" ); my $compare; # Argument records my $firstarg = $a; my $secondarg = $b; # Bring the two option names into a standard form to compare them # in a better way my $first = normalizename(lc($firstarg->{'name'})); $first =~ s/[\W_]//g; my $second = normalizename(lc($secondarg->{'name'})); $second =~ s/[\W_]//g; # group names my $firstgr = $firstarg->{'group'}; my @firstgroup; @firstgroup = split("/", $firstgr) if defined($firstgr); my $secondgr = $secondarg->{'group'}; my @secondgroup; @secondgroup = split("/", $secondgr) if defined($secondgr); my $i = 0; # Compare groups while ($firstgroup[$i] && $secondgroup[$i]) { # Normalize group names my $firstgr = normalizename(lc($firstgroup[$i])); $firstgr =~ s/[\W_]//g; my $secondgr = normalizename(lc($secondgroup[$i])); $secondgr =~ s/[\W_]//g; # Are the groups in the list of standard group names? my $j; for ($j = 0; $j <= $#standardgroups; $j++) { my $firstinlist = ($firstgr =~ /^$standardgroups[$j]/); my $secondinlist = ($secondgr =~ /^$standardgroups[$j]/); if (($firstinlist) && (!$secondinlist)) {return -1}; if (($secondinlist) && (!$firstinlist)) {return 1}; if (($firstinlist) && ($secondinlist)) {last}; } # Compare normalized group names $compare = $firstgr cmp $secondgr; if ($compare != 0) {return $compare}; # Compare original group names $compare = $firstgroup[$i] cmp $secondgroup[$i]; if ($compare != 0) {return $compare}; $i++; } # The one with a deeper level in the group tree will come later if ($firstgroup[$i]) {return 1}; if ($secondgroup[$i]) {return -1}; # Sort by order parameter if the order parameters are different if (defined($firstarg->{'order'}) && defined($secondarg->{'order'}) && $firstarg->{'order'} != $secondarg->{'order'}) { return $firstarg->{'order'} cmp $secondarg->{'order'}; } # Check whether the argument names are in the @standardopts list for ($i = 0; $i <= $#standardopts; $i++) { my $firstinlist = ($first =~ /^$standardopts[$i]/); my $secondinlist = ($second =~ /^$standardopts[$i]/); if (($firstinlist) && (!$secondinlist)) {return -1}; if (($secondinlist) && (!$firstinlist)) {return 1}; if (($firstinlist) && ($secondinlist)) {last}; } # None of the search terms in the list, compare the standard-formed # strings $compare = ( $first cmp $second ); if ($compare != 0) {return $compare}; # No other criteria fullfilled, compare the original input strings return $firstarg->{'name'} cmp $secondarg->{'name'}; } sub sortvals { # All sorting done case-insensitive and characters which are not a letter # or number are taken out!! # List of typical choice names to appear at first # The terms must fit to the beginning of the line, terms which must fit # exactly must have '\$' in the end. my @standardvals = ( # Default setting "default", "printerdefault", # "Neutral" setting "None\$", # Paper sizes "letter\$", #"legal", "a4\$", # Paper types "plain", # Printout Modes "draft\$", "draft\.gray", "draft\.mono", "draft\.", "draft", "normal\$", "normal\.gray", "normal\.mono", "normal\.", "normal", "high\$", "high\.gray", "high\.mono", "high\.", "high", "veryhigh\$", "veryhigh\.gray", "veryhigh\.mono", "veryhigh\.", "veryhigh", "photo\$", "photo\.gray", "photo\.mono", "photo\.", "photo", # Trays "upper", "top", "middle", "mid", "lower", "bottom", "highcapacity", "multipurpose", "tray", ); # Do not waste time if the input strings are equal if ($a eq $b) {return 0;} # Are the two strings numbers? Compare them numerically if (($a =~ /^[\d\.]+$/) && ($b =~ /^[\d\.]+$/)) { my $compare = ( $a <=> $b ); if ($compare != 0) {return $compare}; } # Bring the two option names into a standard form to compare them # in a better way my $first = lc($a); $first =~ s/[\W_]//g; my $second = lc($b); $second =~ s/[\W_]//g; # Check whether they are in the @standardvals list for (my $i = 0; $i <= $#standardvals; $i++) { my $firstinlist = ($first =~ /^$standardvals[$i]/); my $secondinlist = ($second =~ /^$standardvals[$i]/); if (($firstinlist) && (!$secondinlist)) {return -1}; if (($secondinlist) && (!$firstinlist)) {return 1}; if (($firstinlist) && ($secondinlist)) {last}; } # None of the search terms in the list, compare the standard-formed # strings my $compare = ( normalizename($first) cmp normalizename($second) ); if ($compare != 0) {return $compare}; # No other criteria fullfilled, compare the original input strings return $a cmp $b; } # Take driver/pid arguments and generate a Perl data structure for the # Perl filter scripts. Sort the options and enumerated choices so that # they get presented more nicely on frontends which do not sort by # themselves sub getdat ($ $ $) { my ($this, $drv, $poid) = @_; my $ppdfile; # Do we have a link to a custom PPD file for this driver in the # printer XML file? Then return the custom PPD my $p = $this->get_printer($poid); if (defined($p->{'drivers'})) { for my $d (@{$p->{'drivers'}}) { next if ($d->{'id'} ne $drv); $ppdfile = $d->{'ppd'} if defined($d->{'ppd'}); last; } } # Do we have a PostScript printer and a link to a manufacturer- # supplied PPD file? Then return the manufacturer-supplied PPD if ($drv =~ /^Postscript$/i) { $ppdfile = $p->{'ppdurl'} if defined($p->{'ppdurl'}); } # There is a link to a custom PPD, if it is installed on the local # machine, use the custom PPD instead of generating one from the # Foomatic data if ($ppdfile) { $ppdfile =~ s,^http://.*/(PPD/.*)$,$1,; $ppdfile = $libdir . "/db/source/" . $ppdfile; $ppdfile = "${ppdfile}.gz" if (! -r $ppdfile); if (-r $ppdfile) { $this->getdatfromppd($ppdfile); $this->{'dat'}{'ppdfile'} = $ppdfile; return $this->{'dat'}; } } # Generate Perl data structure from database my %dat; # Our purpose in life... my $VAR1; eval (`$bindir/foomatic-combo-xml -d '$drv' -p '$poid' -l '$libdir' | $bindir/foomatic-perl-data -C`) || die ("Could not run \"foomatic-combo-xml\"/" . "\"foomatic-perl-data\"!"); %dat = %{$VAR1}; # Funky one-at-a-time cache thing $this->{'dat'} = \%dat; # We do some additional stuff which is very awkward to implement in C # now, so we do it here # Some clean-up checklongnames($this->{'dat'}); sortoptions($this->{'dat'}); generalentries($this->{'dat'}); return \%dat; } sub getdatfromppd ($ $) { my ($this, $ppdfile) = @_; my $dat = ppdtoperl($ppdfile); if (!defined($dat)) { die ("Unable to open PPD file \'$ppdfile\'\n"); } $this->{'dat'} = $dat; } sub ppdfromvartoperl ($); sub ppdtoperl($); sub perltoxml($); sub ppdtoperl($) { # Build a Perl data structure of the printer/driver options my ($ppdfile) = @_; # Load the PPD file and send it to the parser open PPD, ($ppdfile !~ /\.gz$/i ? "< $ppdfile" : "$sysdeps->{'gzip'} -cd \'$ppdfile\' |") or return undef; my @ppd = ; close PPD; return ppdfromvartoperl(\@ppd); } sub ppdfromvartoperl ($) { my ($ppd) = @_; # Build a data structure for the renderer's command line and the # options my $dat = {}; # data structure for the options my $currentargument = ""; # We are currently reading this argument my $currentgroup = ""; # We are currently in this group/subgroup my @currentgrouptrans; # Translation/long name for group/subgroup my $isfoomatic = 0; # Do we have a Foomatic PPD? # If we have an old Foomatic 2.0.x PPD file, read its built-in Perl # data structure into @datablob and the default values in %ppddefaults # Then delete the $dat structure, replace it by the one "eval"ed from # @datablob, and correct the default settings according to the ones of # the main PPD structure my @datablob; $dat->{"encoding"} = "ascii"; # search for LanguageEncoding for (my $i = 0; $i < @{$ppd}; $i ++) { $_ = $ppd->[$i]; if (m!^\*LanguageEncoding:\s*(\S+)\s*$!) { # "*LanguageEncoding: " $dat->{'encoding'} = $1; if ($dat->{'encoding'} eq 'MacStandard') { $dat->{'encoding'} = 'MacCentralEurRoman'; } elsif ($dat->{'encoding'} eq 'JIS83-RKSJ') { $dat->{'encoding'} = 'shiftjis'; } last; } } # decode PPD my $encoding = $dat->{"encoding"}; for (my $i = 0; $i < @{$ppd}; $i ++) { $ppd->[$i] = decode($encoding, $ppd->[$i]); } # Parse the PPD file for (my $i = 0; $i < @{$ppd}; $i ++) { $_ = $ppd->[$i]; # Foomatic should also work with PPD files downloaded under # Windows. $_ = undossify($_); # Parse keywords if (m!^\*NickName:\s*\"(.*)$!) { # "*ShortNickName: " my $line = $1; # Store the value # Code string can have multiple lines, read all of them my $cmd = ""; while ($line !~ m!\"!) { if ($line =~ m!&&$!) { # line continues in next line $cmd .= substr($line, 0, -2); } else { # line ends here $cmd .= "$line\n"; } # Read next line $i ++; $line = $ppd->[$i]; chomp $line; } $line =~ m!^([^\"]*)\"!; $cmd .= $1; $dat->{'makemodel'} = unhtmlify($cmd); $dat->{'makemodel'} =~ s/^([^,]+),.*$/$1/; # The following fields are only valid for Foomatic PPDs # they will be deleted when it turns out that this PPD # is not a Foomatic PPD. if ($dat->{'makemodel'} =~ /^(\S+)\s+(\S.*)$/) { $dat->{'make'} = $1; $dat->{'model'} = $2; $dat->{'model'} =~ s/\s+Foomatic.*$//i; } } elsif (m!^\*LanguageVersion:\s*(\S+)\s*$!) { # "*LanguageVersion: " $dat->{'language'} = $1; } elsif (m!^\*FoomaticIDs:\s*(\S+)\s+(\S+)\s*$!) { # "*FoomaticIDs: " my $id = $1; my $driver = $2; # Store the values $dat->{'id'} = $id; $dat->{'driver'} = $driver; $isfoomatic = 1; } elsif (m!^\*FoomaticRIPPostPipe:\s*\"(.*)$!) { # "*FoomaticRIPPostPipe: " my $line = $1; # Store the value # Code string can have multiple lines, read all of them my $cmd = ""; while ($line !~ m!\"!) { if ($line =~ m!&&$!) { # line continues in next line $cmd .= substr($line, 0, -2); } else { # line ends here $cmd .= "$line\n"; } # Read next line $i ++; $line = $ppd->[$i]; chomp $line; } $line =~ m!^([^\"]*)\"!; $cmd .= $1; $dat->{'postpipe'} = unhtmlify($cmd); } elsif (m!^\*FoomaticRIPCommandLine:\s*\"(.*)$!) { # "*FoomaticRIPCommandLine: " my $line = $1; # Store the value # Code string can have multiple lines, read all of them my $cmd = ""; while ($line !~ m!\"!) { if ($line =~ m!&&$!) { # line continues in next line $cmd .= substr($line, 0, -2); } else { # line ends here $cmd .= "$line\n"; } # Read next line $i ++; $line = $ppd->[$i]; chomp $line; } $line =~ m!^([^\"]*)\"!; $cmd .= $1; $dat->{'cmd'} = unhtmlify($cmd); } elsif (m!^\*CustomPageSize\s+True:\s*\"(.*)$!) { # "*CustomPageSize True: " my $setting = "Custom"; my $translation = "Custom Size"; my $line = $1; # Make sure that the argument is in the data structure checkarg ($dat, "PageSize"); checkarg ($dat, "PageRegion"); # "PageSize" and "PageRegion" must be both user-visible as they are # options required by the PPD spec undef $dat->{'args_byname'}{"PageSize"}{'hidden'}; undef $dat->{'args_byname'}{"PageRegion"}{'hidden'}; # Make sure that the setting is in the data structure checksetting ($dat, "PageSize", $setting); checksetting ($dat, "PageRegion", $setting); $dat->{'args_byname'}{'PageSize'}{'vals_byname'}{$setting}{'comment'} = $translation; $dat->{'args_byname'}{'PageRegion'}{'vals_byname'}{$setting}{'comment'} = $translation; # Store the value # Code string can have multiple lines, read all of them my $code = ""; while ($line !~ m!\"!) { if ($line =~ m!&&$!) { # line continues in next line $code .= substr($line, 0, -2); } else { # line ends here $code .= "$line\n"; } # Read next line $i ++; $line = $ppd->[$i]; chomp $line; } $line =~ m!^([^\"]*)\"!; $code .= $1; if ($code !~ m!^%% FoomaticRIPOptionSetting!m) { $dat->{'args_byname'}{'PageSize'}{'vals_byname'}{$setting}{'driverval'} = $code; $dat->{'args_byname'}{'PageRegion'}{'vals_byname'}{$setting}{'driverval'} = $code; } } elsif (m!^\*Open(Sub|)Group:\s*([^/]+)(/(.*)|)$!) { # "*Open[Sub]Group: [/] my $group = $2; chomp($group) if $group; my $grouptrans = $4; chomp($grouptrans) if $grouptrans; if (!$grouptrans) { $grouptrans = longname($group); } if ($currentgroup) { $currentgroup .= "/"; } $currentgroup .= $group; push(@currentgrouptrans, unhexify($grouptrans, $dat->{"encoding"})); } elsif (m!^\*Close(Sub|)Group:\s*([^/]+)$!) { # "*Close[Sub]Group: " my $group = $2; chomp($group) if $group; $currentgroup =~ s!$group$!!; $currentgroup =~ s!/$!!; pop(@currentgrouptrans); } elsif (m!^\*Close(Sub|)Group\s*$!) { # "*Close[Sub]Group" # NOTE: This expression is not Adobe-conforming $currentgroup =~ s![^/]+$!!; $currentgroup =~ s!/$!!; pop(@currentgrouptrans); } elsif (m!^\*(JCL|)OpenUI\s+\*([^:]+):\s*(\S+)\s*$!) { # "*[JCL]OpenUI *