/* postscript.c * * Copyright (C) 2008 Till Kamppeter * Copyright (C) 2008 Lars Uebernickel * * This file is part of foomatic-rip. * * Foomatic-rip 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. * * Foomatic-rip 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 Lesser General Public * License along with this library; if not, write to the * Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #include "foomaticrip.h" #include "util.h" #include "options.h" #include "fileconverter.h" #include "renderer.h" #include "process.h" #include #include #include #include void get_renderer_handle(const dstr_t *prepend, FILE **fd, pid_t *pid); int close_renderer_handle(FILE *rendererhandle, pid_t rendererpid); #define LT_BEGIN_FEATURE 1 #define LT_FOOMATIC_RIP_OPTION_SETTING 2 int line_type(const char *line) { const char *p; if (startswith(line, "%%BeginFeature:")) return LT_BEGIN_FEATURE; p = line; while (*p && isspace(*p)) p++; if (!startswith(p, "%%")) return 0; p += 2; while (*p && isspace(*p)) p++; if (startswith(p, "FoomaticRIPOptionSetting:")) return LT_FOOMATIC_RIP_OPTION_SETTING; return 0; } /* Next, examine the PostScript job for traces of command-line and JCL options. PPD-aware applications and spoolers stuff option settings directly into the file, they do not necessarily send PPD options by the command line. Also stuff in PostScript code to apply option settings given by the command line and to set the defaults given in the PPD file. Examination strategy: read lines from STDIN until the first %%Page: comment appears and save them as @psheader. This is the page-independent header part of the PostScript file. The PostScript interpreter (renderer) must execute this part once before rendering any assortment of pages. Then pages can be printed in any arbitrary selection or order. All option settings we find here will be collected in the default option set for the RIP command line. Now the pages will be read and sent to the renderer, one after the other. Every page is read into memory until the %%EndPageSetup comment appears (or a certain amount of lines was read). So we can get option settings only valid for this page. If we have such settings we set them in the modified command set for this page. If the renderer is not running yet (first page) we start it with the command line built from the current modified command set and send the first page to it, in the end we leave the renderer running and keep input and output pipes open, so that it can accept further pages. If the renderer is still running from the previous page and the current modified command set is the same as the one for the previous page, we send the page. If the command set is different, we close the renderer, re-start it with the command line built from the new modified command set, send the header again, and then the page. After the last page the trailer (%%Trailer) is sent. The output pipe of this program stays open all the time so that the spooler does not assume that the job has finished when the renderer is re-started. Non DSC-conforming documents will be read until a certain line number is reached. Command line or JCL options inserted later will be ignored. If options are implemented by PostScript code supposed to be stuffed into the job's PostScript data we stuff the code for all these options into our job data, So all default settings made in the PPD file (the user can have edited the PPD file to change them) are taken care of and command line options get also applied. To give priority to settings made by applications we insert the options's code in the beginnings of their respective sections, so that sommething, which is already inserted, gets executed after our code. Missing sections are automatically created. In non-DSC-conforming files we insert the option code in the beginning of the file. This is the same policy as used by the "pstops" filter of CUPS. If CUPS is the spooler, the option settings were already inserted by the "pstops" filter, so we don't insert them again. The only thing we do is correcting settings of numerical options when they were set to a value not available as choice in the PPD file, As "pstops" does not support "real" numerical options, it sees these settings as an invalid choice and stays with the default setting. In this case we correct the setting in the first occurence of the option's code, as this one is the one added by CUPS, later occurences come from applications and should not be touched. If the input is not PostScript (if there is no "%!" after $maxlinestopsstart lines) a file conversion filter will automatically be applied to the incoming data, so that we will process the resulting PostScript here. This way we have always PostScript data here and so we can apply the printer/driver features described in the PPD file. Supported file conversion filters are "a2ps", "enscript", "mpage", and spooler-specific filters. All filters convert plain text to PostScript, "a2ps" also other formats. The conversion filter is always used when one prints the documentation pages, as they are created as plain text, when CUPS is the spooler "pstops" is executed after the filter so that the default option settings from the PPD file and CUPS-specific options as N-up get applied. On regular printouts one gets always PostScript when CUPS or PPR is the spooler, so the filter is only used for regular printouts under LPD, LPRng, GNUlpr or without spooler. */ /* PostScript sections */ #define PS_SECTION_JCLSETUP 1 #define PS_SECTION_PROLOG 2 #define PS_SECTION_SETUP 3 #define PS_SECTION_PAGESETUP 4 #define MAX_NON_DSC_LINES_IN_HEADER 1000 #define MAX_LINES_FOR_PAGE_OPTIONS 200 typedef struct { size_t pos; FILE *file; const char *alreadyread; size_t len; } stream_t; void _print_ps(stream_t *stream); int stream_next_line(dstr_t *line, stream_t *s) { int c; size_t cnt = 0; dstrclear(line); while (s->pos < s->len) { c = s->alreadyread[s->pos++]; dstrputc(line, c); cnt++; if (c == '\n') return cnt; } while ((c = fgetc(s->file)) != EOF) { dstrputc(line, c); cnt++; if (c == '\n') return cnt; } return cnt; } int print_ps(FILE *file, const char *alreadyread, size_t len, const char *filename) { stream_t stream; if (file != stdin && (dup2(fileno(file), fileno(stdin)) < 0)) { _log("Could not dup %s to stdin.\n", filename); return 0; } stream.pos = 0; stream.file = stdin; stream.alreadyread = alreadyread; stream.len = len; _print_ps(&stream); return 1; } void _print_ps(stream_t *stream) { char *p; int maxlines = 1000; /* Maximum number of lines to be read when the documenent is not DSC-conforming. "$maxlines = 0" means that all will be read and examined. If it is discovered that the input file is DSC-conforming, this will be set to 0. */ int maxlinestopsstart = 200; /* That many lines are allowed until the "%!" indicating PS comes. These additional lines in the beginning are usually JCL commands. The lines will be ignored by our parsing but passed through. */ int printprevpage = 0; /* We set this when encountering "%%Page:" and the previous page is not printed yet. Then it will be printed and the new page will be prepared in the next run of the loop (we don't read a new line and don't increase the $linect then). */ int linect = 0; /* how many lines have we examined */ int nonpslines = 0; /* lines before "%!" found yet. */ int more_stuff = 1; /* there is more stuff in stdin */ int saved = 0; /* DSC line not precessed yet */ int isdscjob = 0; /* is the job dsc conforming */ int inheader = 1; /* Are we still in the header, before first "%%Page:" comment= */ int optionsalsointoheader = 0; /* 1: We are in a "%%BeginSetup... %%EndSetup" section after the first "%%Page:..." line (OpenOffice.org does this and intends the options here apply to the whole document and not only to the current page). We have to add all lines also to the end of the @psheader now and we have to set non-PostScript options also in the "header" optionset. 0: otherwise. */ int insertoptions = 1; /* If we find out that a file with a DSC magic string ("%!PS-Adobe-") is not really DSC- conforming, we insert the options directly after the line with the magic string. We use this variable to store the number of the line with the magic string */ int prologfound = 0; /* Did we find the "%%BeginProlog...%%EndProlog" section? */ int setupfound = 0; /* Did we find the %%BeginSetup...%%EndSetup" section? */ int pagesetupfound = 0; /* special page setup handling needed */ int inprolog = 0; /* We are between "%%BeginProlog" and "%%EndProlog" */ int insetup = 0; /* We are between "%%BeginSetup" and "%%EndSetup" */ int infeature = 0; /* We are between "%%BeginFeature" and "%%EndFeature" */ int optionreplaced = 0; /* Will be set to 1 when we are in an option ("%%BeginFeature... %%EndFeature") which we have replaced. */ int postscriptsection = PS_SECTION_JCLSETUP; /* In which section of the PostScript file are we currently ? */ int nondsclines = 0; /* Number of subsequent lines found which are at a non-DSC-conforming place, between the sections of the header.*/ int nestinglevel = 0; /* Are we in the main document (0) or in an embedded document bracketed by "%%BeginDocument" and "%%EndDocument" (>0) We do not parse the PostScript in an embedded document. */ int inpageheader = 0; /* Are we in the header of a page, between "%%BeginPageSetup" and "%%EndPageSetup" (1) or not (0). */ int passthru = 0; /* 0: write data into psfifo, 1: pass data directly to the renderer */ int lastpassthru = 0; /* State of 'passthru' in previous line (to allow debug output when $passthru switches. */ int ignorepageheader = 0; /* Will be set to 1 as soon as active code (not between "%%BeginPageSetup" and "%%EndPageSetup") appears after a "%%Page:" comment. In this case "%%BeginPageSetup" and "%%EndPageSetup" is not allowed any more on this page and will be ignored. Will be set to 0 when a new "%%Page:" comment appears. */ int optset = optionset("header"); /* Where do the option settings which we have found go? */ /* current line */ dstr_t *line = create_dstr(); dstr_t *onelinebefore = create_dstr(); dstr_t *twolinesbefore = create_dstr(); /* The header of the PostScript file, to be send after each start of the renderer */ dstr_t *psheader = create_dstr(); /* The input FIFO, data which we have pulled from stdin for examination, but not send to the renderer yet */ dstr_t *psfifo = create_dstr(); FILE *fileconverter_handle = NULL; /* File handle to converter process */ pid_t fileconverter_pid = 0; /* PID of the fileconverter process */ int ignoreline; int ooo110 = 0; /* Flag to work around an application bug */ int currentpage = 0; /* The page which we are currently printing */ option_t *o; const char *val; int linetype; dstr_t *linesafterlastbeginfeature = create_dstr(); /* All codelines after the last "%%BeginFeature" */ char optionname [128]; char value [128]; int fromcomposite = 0; dstr_t *pdest; double width, height; pid_t rendererpid = 0; FILE *rendererhandle = NULL; int retval; dstr_t *tmp = create_dstr(); jobhasjcl = 0; /* We do not parse the PostScript to find Foomatic options, we check only whether we have PostScript. */ if (dontparse) maxlines = 1; _log("Reading PostScript input ...\n"); do { ignoreline = 0; if (printprevpage || saved || stream_next_line(line, stream)) { saved = 0; if (linect == nonpslines) { /* In the beginning should be the postscript leader, sometimes after some JCL commands */ if ( !(line->data[0] == '%' && line->data[1] == '!') && !(line->data[1] == '%' && line->data[2] == '!')) /* There can be a Windows control character before "%!" */ { nonpslines++; if (maxlines == nonpslines) maxlines ++; jobhasjcl = 1; if (nonpslines > maxlinestopsstart) { /* This is not a PostScript job, we must convert it */ _log("Job does not start with \"%%!\", is it Postscript?\n" "Starting file converter\n"); /* Reset all variables but conserve the data which we have already read */ jobhasjcl = 0; linect = 0; nonpslines = 1; /* Take into account that the line of this run of the loop will be put into @psheader, so the first line read by the file converter is already the second line */ maxlines = 1001; dstrclear(onelinebefore); dstrclear(twolinesbefore); dstrcpyf(tmp, "%s%s%s", psheader, psfifo, line); dstrclear(psheader); dstrclear(psfifo); dstrclear(line); /* Start the file conversion filter */ if (!fileconverter_pid) get_fileconverter_handle(tmp->data, &fileconverter_handle, &fileconverter_pid); else rip_die(EXIT_JOBERR, "File conversion filter probably crashed\n"); /* Read the further data from the file converter and not from STDIN */ if (close(fileno(stdin)) == -1 && errno != ESPIPE) rip_die(EXIT_PRNERR_NORETRY_BAD_SETTINGS, "Couldn't close STDIN\n"); if (dup2(fileno(stdin), fileno(fileconverter_handle)) == -1) rip_die(EXIT_PRNERR_NORETRY_BAD_SETTINGS, "Couldn't dup fileconverter_handle\n"); } } else { /* Do we have a DSC-conforming document? */ if ((line->data[0] == '%' && startswith(line->data, "%!PS-Adobe-")) || (line->data[1] == '%' && startswith(line->data, "%!PS-Adobe-"))) { /* Do not stop parsing the document */ if (!dontparse) { maxlines = 0; isdscjob = 1; insertoptions = linect + 1; /* We have written into psfifo before, now we continue in psheader and move over the data which is already in psfifo */ dstrcat(psheader, psfifo->data); dstrclear(psfifo); } _log("--> This document is DSC-conforming!\n"); } else { /* Job is not DSC-conforming, stick in all PostScript option settings in the beginning */ append_prolog_section(line, optset, 1); append_setup_section(line, optset, 1); append_page_setup_section(line, optset, 1); prologfound = 1; setupfound = 1; pagesetupfound = 1; } } } else { if (startswith(line->data, "%")) { if (startswith(line->data, "%%BeginDocument")) { /* Beginning of an embedded document Note that Adobe Acrobat has a bug and so uses "%%BeginDocument " instead of "%%BeginDocument:" */ nestinglevel++; _log("Embedded document, nesting level now: %d\n", nestinglevel); } else if (nestinglevel > 0 && startswith(line->data, "%%EndDocument")) { /* End of an embedded document */ nestinglevel--; _log("End of embedded document, nesting level now: %d\n", nestinglevel); } else if (nestinglevel == 0 && startswith(line->data, "%%Creator")) { /* Here we set flags to treat particular bugs of the PostScript produced by certain applications */ p = strstr(line->data, "%%Creator") + 9; while (*p && (isspace(*p) || *p == ':')) p++; if (!strcmp(p, "OpenOffice.org")) { p += 14; while (*p && isspace(*p)) p++; if (sscanf(p, "1.1.%d", &ooo110) == 1) { _log("Document created with OpenOffice.org 1.1.x\n"); ooo110 = 1; } } else if (!strcmp(p, "StarOffice 8")) { p += 12; _log("Document created with StarOffice 8\n"); ooo110 = 1; } } else if (nestinglevel == 0 && startswith(line->data, "%%BeginProlog")) { /* Note: Below is another place where a "Prolog" section start will be considered. There we assume start of the "Prolog" if the job is DSC-Conformimg, but an arbitrary comment starting with "%%Begin", but not a comment explicitly treated here, is found. This is done because many "dvips" (TeX/LaTeX) files miss the "%%BeginProlog" comment. Beginning of Prolog */ _log("\n-----------\nFound: %%%%BeginProlog\n"); inprolog = 1; if (inheader) postscriptsection = PS_SECTION_PROLOG; nondsclines = 0; /* Insert options for "Prolog" */ if (!prologfound) { append_prolog_section(line, optset, 0); prologfound = 1; } } else if (nestinglevel == 0 && startswith(line->data, "%%EndProlog")) { /* End of Prolog */ _log("Found: %%%%EndProlog\n"); inprolog = 0; insertoptions = linect +1; } else if (nestinglevel == 0 && startswith(line->data, "%%BeginSetup")) { /* Beginning of Setup */ _log("\n-----------\nFound: %%%%BeginSetup\n"); insetup = 1; nondsclines = 0; /* We need to distinguish with the $inheader variable here whether we are in the header or on a page, as OpenOffice.org inserts a "%%BeginSetup...%%EndSetup" section after the first "%%Page:..." line and assumes this section to be valid for all pages. */ if (inheader) { postscriptsection = PS_SECTION_SETUP; /* If there was no "Prolog" but there are options for the "Prolog", push a "Prolog" with these options onto the psfifo here */ if (!prologfound) { dstrclear(tmp); append_prolog_section(tmp, optset, 1); dstrprepend(line, tmp->data); prologfound = 1; } /* Insert options for "DocumentSetup" or "AnySetup" */ if (spooler != SPOOLER_CUPS && !setupfound) { /* For non-CUPS spoolers or no spooler at all, we leave everythnig as it is */ append_setup_section(line, optset, 0); setupfound = 1; } } else { /* Found option settings must be stuffed into both the header and the currrent page now. They will be written into both the "header" and the "currentpage" optionsets and the PostScript code lines of this section will not only go into the output stream, but also added to the end of the @psheader, so that they get repeated (to preserve the embedded PostScript option settings) on a restart of the renderer due to command line option changes */ optionsalsointoheader = 1; _log("\"%%%%BeginSetup\" in page header\n"); } } else if (nestinglevel == 0 && startswith(line->data, "%%EndSetup")) { /* End of Setup */ _log("Found: %%%%EndSetup\n"); insetup = 0; if (inheader) { if (spooler == SPOOLER_CUPS) { /* In case of CUPS, we must insert the accounting stuff just before the %%EndSetup comment in order to leave any EndPage procedures that have been defined by either the pstops filter or the PostScript job itself fully functional. */ if (!setupfound) { dstrclear(tmp); append_setup_section(tmp, optset, 0); dstrprepend(line, tmp->data); setupfound = 1; } } insertoptions = linect +1; } else { /* The "%%BeginSetup...%%EndSetup" which OpenOffice.org has inserted after the first "%%Page:..." line ends here, so the following options go only onto the current page again */ optionsalsointoheader = 0; } } else if (nestinglevel == 0 && startswith(line->data, "%%Page:")) { if (!lastpassthru && !inheader) { /* In the last line we were not in passthru mode, so the last page is not printed. Prepare to do it now. */ printprevpage = 1; passthru = 1; _log("New page found but previous not printed, print it now.\n"); } else { /* the previous page is printed, so we can prepare the current one */ _log("\n-----------\nNew page: %s", line->data); printprevpage = 0; currentpage++; /* We consider the beginning of the page already as page setup section, as some apps do not use "%%PageSetup" tags. */ postscriptsection = PS_SECTION_PAGESETUP; /* TODO can this be removed? Save PostScript state before beginning the page $line .= "/foomatic-saved-state save def\n"; */ /* Here begins a new page */ if (inheader) { build_commandline(optset, NULL, 0); /* Here we add some stuff which still belongs into the header */ dstrclear(tmp); /* If there was no "Setup" but there are options for the "Setup", push a "Setup" with these options onto the @psfifo here */ if (!setupfound) { append_setup_section(tmp, optset, 1); setupfound = 1; } /* If there was no "Prolog" but there are options for the "Prolog", push a "Prolog" with these options onto the @psfifo here */ if (!prologfound) { append_prolog_section(tmp, optset, 1); prologfound = 1; } /* Now we push this into the header */ dstrcat(psheader, tmp->data); /* The first page starts, so header ends */ inheader = 0; nondsclines = 0; /* Option setting should go into the page specific option set now */ optset = optionset("currentpage"); } else { /* Restore PostScript state after completing the previous page: foomatic-saved-state restore %%Page: ... /foomatic-saved-state save def Print this directly, so that if we need to restart the renderer for this page due to a command line change this is done under the old instance of the renderer rint $rendererhandle "foomatic-saved-state restore\n"; */ /* Save the option settings of the previous page */ optionset_copy_values(optionset("currentpage"), optionset("previouspage")); optionset_delete_values(optionset("currentpage")); } /* Initialize the option set */ optionset_copy_values(optionset("header"), optionset("currentpage")); /* Set the command line options which apply only to given pages */ set_options_for_page(optionset("currentpage"), currentpage); pagesetupfound = 0; if (spooler == SPOOLER_CUPS) { /* Remove the "notfirst" flag from all options forseen for the "PageSetup" section, because when these are numerical options for CUPS. they have to be set to the correct value for every page */ for (o = optionlist; o; o = o->next) { if (option_get_section(o ) == SECTION_PAGESETUP) o->notfirst = 0; } } /* Now the page header comes, so buffer the data, because we must perhaps shut down and restart the renderer */ passthru = 0; ignorepageheader = 0; optionsalsointoheader = 0; } } else if (nestinglevel == 0 && !ignorepageheader && startswith(line->data, "%%BeginPageSetup")) { /* Start of the page header, up to %%EndPageSetup nothing of the page will be drawn, page-specific option settngs (as letter-head paper for page 1) go here*/ _log("\nFound: %%%%BeginPageSetup\n"); passthru = 0; inpageheader = 1; postscriptsection = PS_SECTION_PAGESETUP; optionsalsointoheader = (ooo110 && currentpage == 1) ? 1 : 0; /* Insert PostScript option settings (options for section "PageSetup") */ if (isdscjob) { append_page_setup_section(line, optset, 0); pagesetupfound = 1; } } else if (nestinglevel == 0 && !ignorepageheader && startswith(line->data, "%%BeginPageSetup")) { /* End of the page header, the page is ready to be printed */ _log("Found: %%%%EndPageSetup\n"); _log("End of page header\n"); /* We cannot for sure say that the page header ends here OpenOffice.org puts (due to a bug) a "%%BeginSetup... %%EndSetup" section after the first "%%Page:...". It is possible that CUPS inserts a "%%BeginPageSetup... %%EndPageSetup" before this section, which means that the options in the "%%BeginSetup...%%EndSetup" section are after the "%%EndPageSetup", so we continue for searching options up to the buffer size limit $maxlinesforpageoptions. */ passthru = 0; inpageheader = 0; optionsalsointoheader = 0; } else if (nestinglevel == 0 && !optionreplaced && (!passthru || !isdscjob) && ((linetype = line_type(line->data)) && (linetype == LT_BEGIN_FEATURE || linetype == LT_FOOMATIC_RIP_OPTION_SETTING))) { /* parse */ if (linetype == LT_BEGIN_FEATURE) { dstrcpy(tmp, line->data); p = strtok(tmp->data, " \t"); /* %%BeginFeature: */ p = strtok(NULL, " \t="); /* Option */ if (*p == '*') p++; strlcpy(optionname, p, 128); p = strtok(NULL, " \t\r\n"); /* value */ fromcomposite = 0; strlcpy(value, p, 128); } else { /* LT_FOOMATIC_RIP_OPTION_SETTING */ dstrcpy(tmp, line->data); p = strstr(tmp->data, "FoomaticRIPOptionSetting:"); p = strtok(p, " \t"); /* FoomaticRIPOptionSetting */ p = strtok(NULL, " \t="); /* Option */ strlcpy(optionname, p, 128); p = strtok(NULL, " \t\r\n"); /* value */ if (*p == '@') { /* fromcomposite */ p++; fromcomposite = 1; } else fromcomposite = 0; strlcpy(value, p, 128); } /* Mark that we are in a "Feature" section */ if (linetype == LT_BEGIN_FEATURE) { infeature = 1; dstrclear(linesafterlastbeginfeature); } /* OK, we have an option. If it's not a Postscript-style option (ie, it's command-line or JCL) then we should note that fact, since the attribute-to-filter option passing in CUPS is kind of funky, especially wrt boolean options. */ _log("Found: %s", line->data); if ((o = find_option(optionname)) && (o->type != TYPE_NONE)) { _log(" Option: %s=%s%s\n", optionname, fromcomposite ? "From" : "", value); if (spooler == SPOOLER_CUPS && linetype == LT_BEGIN_FEATURE && !option_get_value(o, optionset("notfirst")) && strcmp(option_get_value(o, optset) ?: "", value) != 0 && (inheader || option_get_section(o) == SECTION_PAGESETUP)) { /* We have the first occurence of an option setting and the spooler is CUPS, so this setting is inserted by "pstops" or "imagetops". The value from the command line was not inserted by "pstops" or "imagetops" so it seems to be not under the choices in the PPD. Possible reasons: - "pstops" and "imagetops" ignore settings of numerical or string options which are not one of the choices in the PPD file, and inserts the default value instead. - On the command line an option was applied only to selected pages: "-o :