diff mupdf-source/source/fitz/output-pcl.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/source/fitz/output-pcl.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1550 @@
+// Copyright (C) 2004-2025 Artifex Software, Inc.
+//
+// This file is part of MuPDF.
+//
+// MuPDF is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// MuPDF 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 Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see <https://www.artifex.com/> or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+#include "mupdf/fitz.h"
+
+#include <limits.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* Lifted from ghostscript gdevjlm.h */
+/*
+ * The notion that there is such a thing as a "PCL printer" is a fiction: no
+ * two "PCL" printers, even at the same PCL level, have identical command
+ * sets. (The H-P documentation isn't fully accurate either; for example,
+ * it doesn't reveal that the DeskJet printers implement anything beyond PCL
+ * 3.)
+ *
+ * This file contains feature definitions for a generic monochrome PCL
+ * driver (gdevdljm.c), and the specific feature values for all such
+ * printers that Ghostscript currently supports.
+ */
+
+/* Printer spacing capabilities. Include at most one of these. */
+#define PCL_NO_SPACING	0	/* no vertical spacing capability, must be 0 */
+#define PCL3_SPACING	1	/* <ESC>*p+<n>Y (PCL 3) */
+#define PCL4_SPACING	2	/* <ESC>*b<n>Y (PCL 4) */
+#define PCL5_SPACING	4	/* <ESC>*b<n>Y and clear seed row (PCL 5) */
+/* The following is only used internally. */
+#define PCL_ANY_SPACING \
+	(PCL3_SPACING | PCL4_SPACING | PCL5_SPACING)
+
+/* Individual printer properties. Any subset of these may be included. */
+#define PCL_MODE_2_COMPRESSION		8	/* compression mode 2 supported */
+						/* (PCL 4) */
+#define PCL_MODE_3_COMPRESSION		16	/* compression modes 2 & 3 supported */
+						/* (PCL 5) */
+#define PCL_END_GRAPHICS_DOES_RESET	32	/* <esc>*rB resets all parameters */
+#define PCL_HAS_DUPLEX			64	/* <esc>&l<duplex>S supported */
+#define PCL_CAN_SET_PAPER_SIZE		128	/* <esc>&l<sizecode>A supported */
+#define PCL_CAN_PRINT_COPIES		256	/* <esc>&l<copies>X supported */
+#define HACK__IS_A_LJET4PJL		512
+#define HACK__IS_A_OCE9050		1024
+#define PCL_HAS_ORIENTATION             2048
+#define PCL_CAN_SET_CUSTOM_PAPER_SIZE   4096
+#define PCL_HAS_RICOH_PAPER_SIZES       8192
+
+/* Shorthands for the most common spacing/compression combinations. */
+#define PCL_MODE0 PCL3_SPACING
+#define PCL_MODE0NS PCL_NO_SPACING
+#define PCL_MODE2 (PCL4_SPACING | PCL_MODE_2_COMPRESSION)
+#define PCL_MODE2P (PCL_NO_SPACING | PCL_MODE_2_COMPRESSION)
+#define PCL_MODE3 (PCL5_SPACING | PCL_MODE_3_COMPRESSION)
+#define PCL_MODE3NS (PCL_NO_SPACING | PCL_MODE_3_COMPRESSION)
+
+#define MIN_SKIP_LINES 7
+static const char *const from2to3 = "\033*b3M";
+static const char *const from3to2 = "\033*b2M";
+static const int penalty_from2to3 = 5; /* strlen(from2to3); */
+static const int penalty_from3to2 = 5; /* strlen(from3to2); */
+
+/* Generic */
+static const fz_pcl_options fz_pcl_options_generic =
+{
+	(PCL_MODE2 | PCL_END_GRAPHICS_DOES_RESET | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_SET_CUSTOM_PAPER_SIZE),
+	"\033&k1W\033*b2M",
+	"\033&k1W\033*b2M"
+};
+
+/* H-P DeskJet */
+static const fz_pcl_options fz_pcl_options_ljet4 =
+{
+	(PCL_MODE2 | PCL_END_GRAPHICS_DOES_RESET | PCL_CAN_SET_PAPER_SIZE),
+	"\033&k1W\033*b2M",
+	"\033&k1W\033*b2M"
+};
+
+/* H-P DeskJet 500 */
+static const fz_pcl_options fz_pcl_options_dj500 =
+{
+	(PCL_MODE3 | PCL_END_GRAPHICS_DOES_RESET | PCL_CAN_SET_PAPER_SIZE),
+	"\033&k1W",
+	"\033&k1W"
+};
+
+/* Kyocera FS-600 */
+static const fz_pcl_options fz_pcl_options_fs600 =
+{
+	(PCL_MODE3 | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES),
+	"\033*r0F\033&u%dD",
+	"\033*r0F\033&u%dD"
+};
+
+/* H-P original LaserJet */
+/* H-P LaserJet Plus */
+static const fz_pcl_options fz_pcl_options_lj =
+{
+	(PCL_MODE0),
+	"\033*b0M",
+	"\033*b0M"
+};
+
+/* H-P LaserJet IIp, IId */
+static const fz_pcl_options fz_pcl_options_lj2 =
+{
+	(PCL_MODE2P | PCL_CAN_SET_PAPER_SIZE),
+	"\033*r0F\033*b2M",
+	"\033*r0F\033*b2M"
+};
+
+/* H-P LaserJet III* */
+static const fz_pcl_options fz_pcl_options_lj3 =
+{
+	(PCL_MODE3 | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES),
+	"\033&l-180u36Z\033*r0F",
+	"\033&l-180u36Z\033*r0F"
+};
+
+/* H-P LaserJet IIId */
+static const fz_pcl_options fz_pcl_options_lj3d =
+{
+	(PCL_MODE3 | PCL_HAS_DUPLEX | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES),
+	"\033&l-180u36Z\033*r0F",
+	"\033&l180u36Z\033*r0F"
+};
+
+/* H-P LaserJet 4 */
+static const fz_pcl_options fz_pcl_options_lj4 =
+{
+	(PCL_MODE3 | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES),
+	"\033&l-180u36Z\033*r0F\033&u%dD",
+	"\033&l-180u36Z\033*r0F\033&u%dD"
+};
+
+/* H-P LaserJet 4 PL */
+static const fz_pcl_options fz_pcl_options_lj4pl =
+{
+	(PCL_MODE3 | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES | HACK__IS_A_LJET4PJL),
+	"\033&l-180u36Z\033*r0F\033&u%dD",
+	"\033&l-180u36Z\033*r0F\033&u%dD"
+};
+
+/* H-P LaserJet 4d */
+static const fz_pcl_options fz_pcl_options_lj4d =
+{
+	(PCL_MODE3 | PCL_HAS_DUPLEX | PCL_CAN_SET_PAPER_SIZE | PCL_CAN_PRINT_COPIES),
+	"\033&l-180u36Z\033*r0F\033&u%dD",
+	"\033&l180u36Z\033*r0F\033&u%dD"
+};
+
+/* H-P 2563B line printer */
+static const fz_pcl_options fz_pcl_options_lp2563b =
+{
+	(PCL_MODE0NS | PCL_CAN_SET_PAPER_SIZE),
+	"\033*b0M",
+	"\033*b0M"
+};
+
+/* OCE 9050 line printer */
+static const fz_pcl_options fz_pcl_options_oce9050 =
+{
+	(PCL_MODE3NS | PCL_CAN_SET_PAPER_SIZE | HACK__IS_A_OCE9050),
+	"\033*b0M",
+	"\033*b0M"
+};
+
+enum {
+	eLetterPaper = 0,
+	eLegalPaper,
+	eA4Paper,
+	eExecPaper,
+	eLedgerPaper,
+	eA3Paper,
+	eCOM10Envelope,
+	eMonarchEnvelope,
+	eC5Envelope,
+	eDLEnvelope,
+	eJB4Paper,
+	eJB5Paper,
+	eB5Envelope,
+	eB5Paper,                   /* 2.1 */
+	eJPostcard,
+	eJDoublePostcard,
+	eA5Paper,
+	eA6Paper,                   /* 2.0 */
+	eJB6Paper,                  /* 2.0 */
+	eJIS8K,                     /* 2.1 */
+	eJIS16K,                    /* 2.1 */
+	eJISExec,                   /* 2.1 */
+	eDefaultPaperSize = 96,     /* 2.1 */
+	eCustomPaperSize = 101,
+	eB6JIS = 201,               /* non-standard, Ricoh printers */
+	eC6Envelope = 202,          /* non-standard, Ricoh printers */
+	e8Kai  = 203,               /* non-standard, Ricoh printers */
+	e16Kai = 204,               /* non-standard, Ricoh printers */
+	e12x18 = 205,               /* non-standard, Ricoh printers */
+	e13x19_2 = 212,             /* non-standard, Ricoh printers */
+	e13x19 = 213,               /* non-standard, Ricoh printers */
+	e12_6x19_2 = 214,           /* non-standard, Ricoh printers */
+	e12_6x18_5 = 215,           /* non-standard, Ricoh printers */
+	e13x18  = 216,              /* non-standard, Ricoh printers */
+	eSRA3 = 217,                /* non-standard, Ricoh printers */
+	eSRA4 = 218,                /* non-standard, Ricoh printers */
+	e226x310 = 219,             /* non-standard, Ricoh printers */
+	e310x432 = 220,             /* non-standard, Ricoh printers */
+	eEngQuatro = 221,           /* non-standard, Ricoh printers */
+	e11x14 = 222,               /* non-standard, Ricoh printers */
+	e11x15 = 223,               /* non-standard, Ricoh printers */
+	e10x14 = 224,               /* non-standard, Ricoh printers */
+};
+
+static void copy_opts(fz_pcl_options *dst, const fz_pcl_options *src)
+{
+	if (dst)
+		*dst = *src;
+}
+
+const char *fz_pcl_write_options_usage =
+	"PCL output options:\n"
+	"\tcolorspace=mono: render 1-bit black and white page\n"
+	"\tcolorspace=rgb: render full color page\n"
+	"\tpreset=generic|ljet4|dj500|fs600|lj|lj2|lj3|lj3d|lj4|lj4pl|lj4d|lp2563b|oce9050\n"
+	"\tspacing=0: No vertical spacing capability\n"
+	"\tspacing=1: PCL 3 spacing (<ESC>*p+<n>Y)\n"
+	"\tspacing=2: PCL 4 spacing (<ESC>*b<n>Y)\n"
+	"\tspacing=3: PCL 5 spacing (<ESC>*b<n>Y and clear seed row)\n"
+	"\tmode2: Enable mode 2 graphics compression\n"
+	"\tmode3: Enable mode 3 graphics compression\n"
+	"\teog_reset: End of graphics (<ESC>*rB) resets all parameters\n"
+	"\thas_duplex: Duplex supported (<ESC>&l<duplex>S)\n"
+	"\thas_papersize: Papersize setting supported (<ESC>&l<sizecode>A)\n"
+	"\thas_copies: Number of copies supported (<ESC>&l<copies>X)\n"
+	"\tis_ljet4pjl: Disable/Enable HP 4PJL model-specific output\n"
+	"\tis_oce9050: Disable/Enable Oce 9050 model-specific output\n"
+	"\n";
+
+void fz_pcl_preset(fz_context *ctx, fz_pcl_options *opts, const char *preset)
+{
+	if (preset == NULL || *preset == 0 || !strcmp(preset, "generic"))
+		copy_opts(opts, &fz_pcl_options_generic);
+	else if (!strcmp(preset, "ljet4"))
+		copy_opts(opts, &fz_pcl_options_ljet4);
+	else if (!strcmp(preset, "dj500"))
+		copy_opts(opts, &fz_pcl_options_dj500);
+	else if (!strcmp(preset, "fs600"))
+		copy_opts(opts, &fz_pcl_options_fs600);
+	else if (!strcmp(preset, "lj"))
+		copy_opts(opts, &fz_pcl_options_lj);
+	else if (!strcmp(preset, "lj2"))
+		copy_opts(opts, &fz_pcl_options_lj2);
+	else if (!strcmp(preset, "lj3"))
+		copy_opts(opts, &fz_pcl_options_lj3);
+	else if (!strcmp(preset, "lj3d"))
+		copy_opts(opts, &fz_pcl_options_lj3d);
+	else if (!strcmp(preset, "lj4"))
+		copy_opts(opts, &fz_pcl_options_lj4);
+	else if (!strcmp(preset, "lj4pl"))
+		copy_opts(opts, &fz_pcl_options_lj4pl);
+	else if (!strcmp(preset, "lj4d"))
+		copy_opts(opts, &fz_pcl_options_lj4d);
+	else if (!strcmp(preset, "lp2563b"))
+		copy_opts(opts, &fz_pcl_options_lp2563b);
+	else if (!strcmp(preset, "oce9050"))
+		copy_opts(opts, &fz_pcl_options_oce9050);
+	else
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "Unknown preset '%s'", preset);
+}
+
+fz_pcl_options *
+fz_parse_pcl_options(fz_context *ctx, fz_pcl_options *opts, const char *args)
+{
+	const char *val;
+
+	memset(opts, 0, sizeof *opts);
+
+	if (fz_has_option(ctx, args, "preset", &val))
+		fz_pcl_preset(ctx, opts, val);
+	else
+		fz_pcl_preset(ctx, opts, "generic");
+
+	if (fz_has_option(ctx, args, "spacing", &val))
+	{
+		switch (atoi(val))
+		{
+		case 0: opts->features &= ~PCL_ANY_SPACING; break;
+		case 1: opts->features = (opts->features & ~PCL_ANY_SPACING) | PCL3_SPACING; break;
+		case 2: opts->features = (opts->features & ~PCL_ANY_SPACING) | PCL4_SPACING; break;
+		case 3: opts->features = (opts->features & ~PCL_ANY_SPACING) | PCL5_SPACING; break;
+		default: fz_throw(ctx, FZ_ERROR_ARGUMENT, "Unsupported PCL spacing %d (0-3 only)", atoi(val));
+		}
+	}
+	if (fz_has_option(ctx, args, "mode2", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_MODE_2_COMPRESSION;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_MODE_2_COMPRESSION;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for mode2 value");
+	}
+	if (fz_has_option(ctx, args, "mode3", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_MODE_3_COMPRESSION;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_MODE_3_COMPRESSION;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for mode3 value");
+	}
+	if (fz_has_option(ctx, args, "eog_reset", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_END_GRAPHICS_DOES_RESET;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_END_GRAPHICS_DOES_RESET;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for eog_reset value");
+	}
+	if (fz_has_option(ctx, args, "has_duplex", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_HAS_DUPLEX;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_HAS_DUPLEX;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for has_duplex value");
+	}
+	if (fz_has_option(ctx, args, "has_papersize", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_CAN_SET_PAPER_SIZE;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_CAN_SET_PAPER_SIZE;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for has_papersize value");
+	}
+	if (fz_has_option(ctx, args, "has_copies", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~PCL_CAN_PRINT_COPIES;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= PCL_CAN_PRINT_COPIES;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for has_papersize value");
+	}
+	if (fz_has_option(ctx, args, "is_ljet4pjl", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~HACK__IS_A_LJET4PJL;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= HACK__IS_A_LJET4PJL;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for is_ljet4pjl value");
+	}
+	if (fz_has_option(ctx, args, "is_oce9050", &val))
+	{
+		if (fz_option_eq(val, "no"))
+			opts->features &= ~HACK__IS_A_OCE9050;
+		else if (fz_option_eq(val, "yes"))
+			opts->features |= HACK__IS_A_OCE9050;
+		else
+			fz_throw(ctx, FZ_ERROR_ARGUMENT, "Expected 'yes' or 'no' for is_oce9050 value");
+	}
+
+	return opts;
+}
+
+static void
+make_init(fz_pcl_options *pcl, char *buf, unsigned long len, const char *str, int res)
+{
+	int paper_source = -1;
+
+	fz_snprintf(buf, len, str, res);
+
+	if (pcl->manual_feed_set && pcl->manual_feed)
+		paper_source = 2;
+	else if (pcl->media_position_set && pcl->media_position >= 0)
+		paper_source = pcl->media_position;
+	if (paper_source >= 0)
+	{
+		char buf2[40];
+		fz_snprintf(buf2, sizeof(buf2), "\033&l%dH", paper_source);
+		strncat(buf, buf2, len);
+	}
+}
+
+static void
+pcl_header(fz_context *ctx, fz_output *out, fz_pcl_options *pcl, int num_copies, int xres, int yres, int w, int h)
+{
+	char odd_page_init[80];
+	char even_page_init[80];
+
+	make_init(pcl, odd_page_init, sizeof(odd_page_init), pcl->odd_page_init, xres);
+	make_init(pcl, even_page_init, sizeof(even_page_init), pcl->even_page_init, xres);
+
+	if (pcl->page_count == 0)
+	{
+		if (pcl->features & HACK__IS_A_LJET4PJL)
+			fz_write_string(ctx, out, "\033%-12345X@PJL\r\n@PJL ENTER LANGUAGE = PCL\r\n");
+		fz_write_string(ctx, out, "\033E"); /* reset printer */
+		/* Reset the margins */
+		/* ESC & l # E  =  Top Margin in decipoints */
+		fz_write_string(ctx, out, "\033&l0E");
+		/* ESC & l # U  =  Left (Long-Edge) offset registration */
+		/* I don't like it that we have to hardcode -180 decipoints in here, but it seems to work. */
+		fz_write_string(ctx, out, "\033&l-180U");
+		/* ESC & l # Z  =  Top (Short-Edge) offset registration */
+		fz_write_string(ctx, out, "\033&l0Z");
+		/* If the printer supports it, set orientation */
+		if (pcl->features & PCL_HAS_ORIENTATION)
+		{
+			fz_write_printf(ctx, out, "\033&l%dO", pcl->orientation);
+		}
+		/* If the printer supports it, set the paper size */
+		/* based on the actual requested size. */
+		if (pcl->features & PCL_CAN_SET_PAPER_SIZE)
+		{
+			/* It probably never hurts to define the page explicitly */
+			{
+				int decipointw = (w * 720 + (xres>>1)) / xres;
+				int decipointh = (h * 720 + (yres>>1)) / yres;
+
+				fz_write_printf(ctx, out, "\033&f%dI", decipointw);
+				fz_write_printf(ctx, out, "\033&f%dJ", decipointh);
+			}
+			fz_write_printf(ctx, out, "\033&l%dA", pcl->paper_size);
+		}
+		/* If printer can duplex, set duplex mode appropriately. */
+		if (pcl->features & PCL_HAS_DUPLEX)
+		{
+			if (pcl->duplex_set)
+			{
+				if (pcl->duplex)
+				{
+					if (!pcl->tumble)
+						fz_write_string(ctx, out, "\033&l1S");
+					else
+						fz_write_string(ctx, out, "\033&l2S");
+				}
+				else
+					fz_write_string(ctx, out, "\033&l0S");
+			}
+			else
+			{
+				/* default to duplex for this printer */
+				fz_write_string(ctx, out, "\033&l1S");
+			}
+		}
+	}
+
+	/* Put out per-page initialization. */
+	/* In duplex mode the sheet is already in process, so there are some
+	 * commands which must not be sent to the printer for the 2nd page,
+	 * as these commands will cause the printer to eject the sheet with
+	 * only the 1st page printed. These commands are:
+	 * \033&l%dA (setting paper size)
+	 * \033&l%dH (setting paper tray)
+	 * in simplex mode we set these parameters for each page,
+	 * in duplex mode we set these parameters for each odd page
+	 */
+
+	if ((pcl->features & PCL_HAS_DUPLEX) && pcl->duplex_set && pcl->duplex)
+	{
+		/* We are printing duplex, so change margins as needed */
+		if (((pcl->page_count/num_copies)%2) == 0)
+		{
+			if (pcl->page_count != 0 && (pcl->features & PCL_CAN_SET_PAPER_SIZE))
+			{
+				fz_write_printf(ctx, out, "\033&l%dA", pcl->paper_size);
+			}
+			fz_write_string(ctx, out, "\033&l0o0l0E");
+			fz_write_string(ctx, out, pcl->odd_page_init);
+		}
+		else
+			fz_write_string(ctx, out, pcl->even_page_init);
+	}
+	else
+	{
+		if (pcl->features & PCL_CAN_SET_PAPER_SIZE)
+		{
+			fz_write_printf(ctx, out, "\033&l%dA", pcl->paper_size);
+		}
+		fz_write_string(ctx, out, "\033&l0o0l0E");
+		fz_write_string(ctx, out, pcl->odd_page_init);
+	}
+
+	fz_write_printf(ctx, out, "\033&l%dX", num_copies); /* # of copies */
+
+	/* End raster graphics, position cursor at top. */
+	fz_write_string(ctx, out, "\033*rB\033*p0x0Y");
+
+	/* The DeskJet and DeskJet Plus reset everything upon */
+	/* receiving \033*rB, so we must reinitialize graphics mode. */
+	if (pcl->features & PCL_END_GRAPHICS_DOES_RESET)
+	{
+		fz_write_string(ctx, out, pcl->odd_page_init); /* Assume this does the right thing */
+		fz_write_printf(ctx, out, "\033&l%dX", num_copies); /* # of copies */
+	}
+
+	/* Set resolution. */
+	fz_write_printf(ctx, out, "\033*t%dR", xres);
+
+	/* Raster units */
+	/* 96,100,120,144,150,160,180,200,225,240,288,300,360,400,450,480,600,720,800,900,1200,1440,1800,2400,3600,7200 */
+	/* FIXME: xres vs yres */
+	fz_write_printf(ctx, out, "\033&u%dD", xres);
+
+	pcl->page_count++;
+}
+
+typedef struct pcl_papersize_s
+{
+	int code;
+	const char *text;
+	int width;
+	int height;
+} pcl_papersize;
+
+static const pcl_papersize papersizes[] =
+{
+	{ eLetterPaper,      "letter",       2550, 3300},
+	{ eLegalPaper,       "legal",        2550, 4200},
+	{ eA4Paper,          "a4",           2480, 3507},
+	{ eExecPaper,        "executive",    2175, 3150},
+	{ eLedgerPaper,      "ledger",       3300, 5100},
+	{ eA3Paper,          "a3",           3507, 4960},
+	{ eCOM10Envelope,    "com10",        1237, 2850},
+	{ eMonarchEnvelope,  "monarch",      1162, 2250},
+	{ eC5Envelope,       "c5",           1913, 2704},
+	{ eDLEnvelope,       "dl",           1299, 2598},
+	{ eJB4Paper,         "jisb4",        3035, 4299},
+	{ eJB4Paper,         "jis b4",       3035, 4299},
+	{ eJB5Paper,         "jisb5",        2150, 3035},
+	{ eJB5Paper,         "jis b5",       2150, 3035},
+	{ eB5Envelope,       "b5",           2078, 2952},
+	{ eB5Paper,          "b5paper",      2150, 3035},
+	{ eJPostcard,        "jpost",        1181, 1748},
+	{ eJDoublePostcard,  "jpostd",       2362, 1748},
+	{ eA5Paper,          "a5",           1748, 2480},
+	{ eA6Paper,          "a6",           1240, 1748},
+	{ eJB6Paper,         "jisb6",        1512, 2150},
+	{ eJIS8K,            "jis8K",        3154, 4606},
+	{ eJIS16K,           "jis16K",       2303, 3154},
+	{ eJISExec,          "jisexec",      2551, 3898},
+	{ eB6JIS,            "B6 (JIS)",     1512, 2150},
+	{ eC6Envelope,       "C6",           1345, 1912},
+	{ e8Kai,             "8Kai",         3154, 4608},
+	{ e16Kai,            "16Kai",        2304, 3154},
+	{ e12x18,            "12x18",        3600, 5400},
+	{ e13x19_2,          "13x19.2",      3900, 5758},
+	{ e13x19,            "13x19",        3900, 5700},
+	{ e12_6x19_2,        "12.6x19.2",    3779, 5758},
+	{ e12_6x18_5,        "12.6x18.5",    3779, 5550},
+	{ e13x18,            "13x18",        3900, 5400},
+	{ eSRA3,             "SRA3",         3779, 5316},
+	{ eSRA4,             "SRA4",         2658, 3779},
+	{ e226x310,          "226x310",      2670, 3662},
+	{ e310x432,          "310x432",      3662, 5104},
+	{ eEngQuatro,        "EngQuatro",    2400, 3000},
+	{ e11x14,            "11x14",        3300, 4200},
+	{ e11x15,            "11x15",        3300, 4500},
+	{ e10x14,            "10x14",        3000, 4200}
+};
+
+static void guess_paper_size(fz_pcl_options *pcl, int w, int h, int xres, int yres)
+{
+	int size;
+	int rotated = 0;
+
+	/* If we've been given a paper size, live with it */
+	if (pcl->paper_size != 0)
+		return;
+
+	w = w * 300 / xres;
+	h = h * 300 / xres;
+
+	/* Look for an exact match */
+	for (size = 0; size < (int)nelem(papersizes); size++)
+	{
+		if (papersizes[size].code > eCustomPaperSize && (pcl->features & PCL_HAS_RICOH_PAPER_SIZES) == 0)
+			continue;
+		if (w == papersizes[size].width && h == papersizes[size].height)
+			break;
+		if ((pcl->features & PCL_HAS_ORIENTATION) && w == papersizes[size].height && h == papersizes[size].width)
+		{
+			rotated = 1;
+			break;
+		}
+	}
+
+	/* If we didn't find an exact match, find the smallest one that's
+	 * larger. Consider orientation if our printer supports it. */
+	if (size == nelem(papersizes))
+	{
+		if ((pcl->features & PCL_CAN_SET_CUSTOM_PAPER_SIZE) != 0)
+		{
+			/* Send it as a custom size */
+			size = eCustomPaperSize;
+		}
+		else
+		{
+			/* Send the next larger one (minimise waste) */
+			int i;
+			int best_waste = INT_MAX;
+			for (i = 0; i < (int)nelem(papersizes); i++)
+			{
+				int waste;
+				if (papersizes[i].code > eCustomPaperSize && (pcl->features & PCL_HAS_RICOH_PAPER_SIZES) == 0)
+					continue;
+				waste = papersizes[i].width * papersizes[i].height - w * h;
+				if (waste > best_waste)
+					continue;
+				if (w <= papersizes[i].width && h <= papersizes[i].height)
+				{
+					best_waste = waste;
+					rotated = 0;
+					size = i;
+				}
+				if ((pcl->features & PCL_HAS_ORIENTATION) && w <= papersizes[i].height && h <= papersizes[i].width)
+				{
+					best_waste = waste;
+					rotated = 1;
+					size = i;
+				}
+			}
+		}
+	}
+
+	/* Now, size = The best size we have (or nelem(papersizes)) if it's too big */
+
+	if (size < (int)nelem(papersizes))
+		pcl->paper_size = papersizes[size].code;
+	else
+		pcl->paper_size = eCustomPaperSize; /* Custom */
+
+	pcl->orientation = rotated;
+}
+
+/* Copy a line, returning true if the line was blank. */
+static int
+line_is_blank(unsigned char *dst, const unsigned char *sp, int w)
+{
+	int zero = 0;
+
+	while (w-- > 0)
+	{
+		zero |= (*dst++ = *sp++);
+		zero |= (*dst++ = *sp++);
+		zero |= (*dst++ = *sp++);
+	}
+
+	return zero == 0;
+}
+
+static int
+delta_compression(unsigned char *curr, unsigned char *prev, unsigned char *comp, int ds, int space)
+{
+	int left = space;
+	int x = ds;
+
+	while (x > 0)
+	{
+		/* Count matching bytes */
+		int match = 0;
+		int diff = 0;
+		while (x > 0 && *curr == *prev)
+		{
+			curr++;
+			prev++;
+			match++;
+			x--;
+		}
+
+		/* Count different bytes */
+		while (x > 0 && *curr != *prev)
+		{
+			curr++;
+			prev++;
+			diff++;
+			x--;
+		}
+
+		while (diff > 0)
+		{
+			int exts;
+			int mini_diff = diff;
+			if (mini_diff > 8)
+				mini_diff = 8;
+
+			exts = (match+255-31)/255;
+			left -= 1 + mini_diff + exts;
+			if (left < 0)
+				return 0;
+			*comp++ = ((mini_diff-1)<<5) | (match < 31 ? match : 31);
+			if (exts > 0)
+			{
+				match -= 31;
+				while (--exts)
+				{
+					*comp++ = 255;
+					match -= 255;
+				}
+				*comp++ = match;
+			}
+			memcpy(comp, curr-diff, mini_diff);
+			comp += mini_diff;
+
+			match = 0;
+			diff -= mini_diff;
+		}
+	}
+	return space - left;
+}
+
+void
+fz_write_pixmap_as_pcl(fz_context *ctx, fz_output *out, const fz_pixmap *pixmap, const fz_pcl_options *pcl)
+{
+	fz_band_writer *writer;
+
+	if (!pixmap || !out)
+		return;
+
+	writer = fz_new_color_pcl_band_writer(ctx, out, pcl);
+	fz_try(ctx)
+	{
+		fz_write_header(ctx, writer, pixmap->w, pixmap->h, pixmap->n, pixmap->alpha, pixmap->xres, pixmap->yres, 0, pixmap->colorspace, pixmap->seps);
+		fz_write_band(ctx, writer, pixmap->stride, pixmap->h, pixmap->samples);
+		fz_close_band_writer(ctx, writer);
+	}
+	fz_always(ctx)
+		fz_drop_band_writer(ctx, writer);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+typedef struct color_pcl_band_writer_s
+{
+	fz_band_writer super;
+	fz_pcl_options options;
+	unsigned char *linebuf;
+	unsigned char compbuf[32768];
+	unsigned char compbuf2[32768];
+} color_pcl_band_writer;
+
+static void
+color_pcl_write_header(fz_context *ctx, fz_band_writer *writer_, fz_colorspace *cs)
+{
+	color_pcl_band_writer *writer = (color_pcl_band_writer *)writer_;
+	fz_output *out = writer->super.out;
+	int w = writer->super.w;
+	int h = writer->super.h;
+	int n = writer->super.n;
+	int s = writer->super.s;
+	int a = writer->super.alpha;
+	int xres = writer->super.xres;
+	int yres = writer->super.yres;
+
+	if (a != 0)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "color PCL cannot write alpha channel");
+	if (s != 0)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "color PCL cannot write spot colors");
+	if (n != 3)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "color PCL must be RGB");
+
+	writer->linebuf = Memento_label(fz_malloc(ctx, w * 3 * 2), "color_pcl_linebuf");
+
+	guess_paper_size(&writer->options, w, h, xres, yres);
+
+	pcl_header(ctx, out, &writer->options, 1, xres, yres, w, h);
+
+	/* Raster presentation */
+	/* Print in orientation of the logical page */
+	fz_write_string(ctx, out, "\033&r0F");
+
+	/* Set color mode */
+	fz_write_data(ctx, out, "\033*v6W"
+		"\000"	/* Colorspace 0 = Device RGB */
+		"\003"	/* Pixel encoding mode: 3 = Direct by Pixel*/
+		"\000"	/* Bits per index: 0 = no palette */
+		"\010"	/* Red bits */
+		"\010"	/* Green bits */
+		"\010",	/* Blue bits */
+		11
+		);
+
+	/* Raster resolution */
+	/* Supposed to be strictly 75, 100, 150, 200, 300, 600 */
+	/* FIXME: xres vs yres */
+	fz_write_printf(ctx, out, "\033*t%dR", xres);
+}
+
+static void flush_if_not_room(fz_context *ctx, fz_output *out, const unsigned char *comp, int *fill, int len)
+{
+	if (len + *fill >= 32767)
+	{
+		/* Can't fit any data, so flush */
+		fz_write_printf(ctx, out, "\033*b%dW", *fill);
+		fz_write_data(ctx, out, comp, *fill);
+		*fill = 0;
+	}
+}
+
+static void
+color_pcl_compress_column(fz_context *ctx, color_pcl_band_writer *writer, const unsigned char *sp, int w, int h, int stride)
+{
+	fz_output *out = writer->super.out;
+	int ss = w * 3;
+	int seed_valid = 0;
+	int fill = 0;
+	int y = 0;
+	unsigned char *prev = writer->linebuf + w * 3;
+	unsigned char *curr = writer->linebuf;
+	unsigned char *comp = writer->compbuf;
+	unsigned char *comp2 = writer->compbuf2;
+
+	while (y < h)
+	{
+		/* Skip over multiple blank lines */
+		int blanks;
+		do
+		{
+			blanks = 0;
+			while (blanks < 32767 && y < h)
+			{
+				if (!line_is_blank(curr, sp, w))
+					break;
+				blanks++;
+				y++;
+				sp += stride;
+			}
+
+			if (blanks)
+			{
+				flush_if_not_room(ctx, out, comp, &fill, 3);
+				comp[fill++] = 4; /* Empty row */
+				comp[fill++] = blanks>>8;
+				comp[fill++] = blanks & 0xFF;
+				seed_valid = 0;
+			}
+		}
+		while (blanks == 32767);
+
+		if (y == h)
+			break;
+
+		/* So, at least 1 more line to copy, and it's in curr */
+		if (seed_valid && memcmp(curr, prev, ss) == 0)
+		{
+			int count = 1;
+			sp += stride;
+			y++;
+			while (count < 32767 && y < h)
+			{
+				if (memcmp(sp-stride, sp, ss) != 0)
+					break;
+				count++;
+				sp += stride;
+				y++;
+			}
+			flush_if_not_room(ctx, out, comp, &fill, 3);
+			comp[fill++] = 5; /* Duplicate row */
+			comp[fill++] = count>>8;
+			comp[fill++] = count & 0xFF;
+		}
+		else
+		{
+			unsigned char *tmp;
+			int len = 0;
+
+			/* Compress the line into our fixed buffer. */
+			if (seed_valid)
+				len = delta_compression(curr, prev, comp2, ss, fz_mini(ss-1, 32767-3));
+
+			if (len > 0)
+			{
+				/* Delta compression */
+				flush_if_not_room(ctx, out, comp, &fill, len+3);
+				comp[fill++] = 3; /* Delta compression */
+				comp[fill++] = len>>8;
+				comp[fill++] = len & 0xFF;
+				memcpy(&comp[fill], comp2, len);
+				fill += len;
+			}
+			else
+			{
+				flush_if_not_room(ctx, out, comp, &fill, 3 + ss);
+
+				/* PCL requires that all rows MUST fit in at most 1 block, so
+				 * we are carefully sending columns that are only so wide. */
+
+				/* Unencoded */
+				/* Transfer Raster Data: ss+3 bytes, 0 = Unencoded, count high, count low */
+				comp[fill++] = 0;
+				comp[fill++] = ss>>8;
+				comp[fill++] = ss & 0xFF;
+				memcpy(&comp[fill], curr, ss);
+				fill += ss;
+				seed_valid = 1;
+			}
+
+			/* curr becomes prev */
+			tmp = prev; prev = curr; curr = tmp;
+			sp += stride;
+			y++;
+		}
+	}
+	/* And flush */
+	if (fill) {
+		fz_write_printf(ctx, out, "\033*b%dW", fill);
+		fz_write_data(ctx, out, comp, fill);
+	}
+
+	/* End Raster Graphics */
+	fz_write_string(ctx, out, "\033*rC");
+}
+
+static void
+color_pcl_write_band(fz_context *ctx, fz_band_writer *writer_, int stride, int band_start, int band_height, const unsigned char *sp)
+{
+	color_pcl_band_writer *writer = (color_pcl_band_writer *)writer_;
+	fz_output *out = writer->super.out;
+	int w = writer->super.w;
+	int h = writer->super.h;
+	int xres = writer->super.xres;
+	int cw;
+	int x;
+
+	if (!out)
+		return;
+
+	if (band_start+band_height >= h)
+		band_height = h - band_start;
+
+	/* We have to specify image output size in decipoints (720dpi).
+	 * Most usual PCL resolutions are a multiple of 75.
+	 * Pick our maximum column size to be 10800 = 15*720 = 144*75
+	 * to give us good results. 10800 * 3 = 32400 < 32760 */
+	cw = 10800; /* Limited by how much rowdata we can send at once */
+	if (cw > w)
+		cw = w;
+
+	for (x = 0; x*cw < w; x++)
+	{
+		int col_w = w - cw*x;
+		if (col_w > cw)
+			col_w = cw;
+
+		/* Top left corner */
+		fz_write_printf(ctx, out, "\033*p%dx%dY", x*cw, band_start);
+
+		/* Raster height */
+		fz_write_printf(ctx, out, "\033*r%dT", band_height);
+
+		/* Raster width */
+		fz_write_printf(ctx, out, "\033*r%dS", col_w);
+
+		/* Destination height */
+		fz_write_printf(ctx, out, "\033*t%dV", band_height*720/xres);
+
+		/* Destination width */
+		fz_write_printf(ctx, out, "\033*t%dH", col_w*720/xres);
+
+		/* start raster graphics */
+		/* 1 = start at cursor position */
+		fz_write_string(ctx, out, "\033*r3A");
+
+		/* Now output the actual bitmap */
+		/* Adaptive Compression */
+		fz_write_string(ctx, out, "\033*b5M");
+
+		color_pcl_compress_column(ctx, writer, sp + x * cw * 3, col_w, band_height, stride);
+	}
+}
+
+static void
+color_pcl_write_trailer(fz_context *ctx, fz_band_writer *writer_)
+{
+}
+
+static void
+color_pcl_drop_band_writer(fz_context *ctx, fz_band_writer *writer_)
+{
+	color_pcl_band_writer *writer = (color_pcl_band_writer *)writer_;
+	fz_free(ctx, writer->linebuf);
+}
+
+fz_band_writer *fz_new_color_pcl_band_writer(fz_context *ctx, fz_output *out, const fz_pcl_options *options)
+{
+	color_pcl_band_writer *writer = fz_new_band_writer(ctx, color_pcl_band_writer, out);
+
+	writer->super.header = color_pcl_write_header;
+	writer->super.band = color_pcl_write_band;
+	writer->super.trailer = color_pcl_write_trailer;
+	writer->super.drop = color_pcl_drop_band_writer;
+
+	if (options)
+		writer->options = *options;
+	else
+		fz_pcl_preset(ctx, &writer->options, "generic");
+
+	return &writer->super;
+}
+
+/*
+ * Mode 2 Row compression routine for the HP DeskJet & LaserJet IIp.
+ * Compresses data from row up to end_row, storing the result
+ * starting at out. Returns the number of bytes stored.
+ * Runs of K<=127 literal bytes are encoded as K-1 followed by
+ * the bytes; runs of 2<=K<=127 identical bytes are encoded as
+ * 257-K followed by the byte.
+ * In the worst case, the result is N+(N/127)+1 bytes long,
+ * where N is the original byte count (end_row - row).
+ */
+static int
+mode2compress(unsigned char *out, const unsigned char *in, int in_len)
+{
+	int x;
+	int out_len = 0;
+	int run;
+
+	for (x = 0; x < in_len; x += run)
+	{
+		/* How far do we have to look to find a value that isn't repeated? */
+		for (run = 1; run < 127 && x+run < in_len; run++)
+			if (in[0] != in[run])
+				break;
+		if (run > 1)
+		{
+			/* We have a run of matching bytes */
+			out[out_len++] = 257-run;
+			out[out_len++] = in[0];
+		}
+		else
+		{
+			/* Now copy as many literals as possible. We only
+			 * break the run at a length of 127, at the end,
+			 * or where we have 3 repeated values. */
+			int i;
+
+			/* How many literals do we need to copy? */
+			for (; run < 127 && x+run+2 < in_len; run++)
+				if (in[run] == in[run+1] && in[run] == in[run+2])
+					break;
+			/* Don't leave stragglers at the end */
+			if (x + run + 2 >= in_len)
+			{
+				run = in_len - x;
+				if (run > 127)
+					run = 127;
+			}
+			out[out_len++] = run-1;
+			for (i = 0; i < run; i++)
+			{
+				out[out_len++] = in[i];
+			}
+		}
+		in += run;
+	}
+
+	return out_len;
+}
+
+/*
+ * Mode 3 compression routine for the HP LaserJet III family.
+ * Compresses bytecount bytes starting at current, storing the result
+ * in compressed, comparing against and updating previous.
+ * Returns the number of bytes stored.	In the worst case,
+ * the number of bytes is bytecount+(bytecount/8)+1.
+ */
+static int
+mode3compress(unsigned char *out, const unsigned char *in, unsigned char *prev, int in_len)
+{
+	unsigned char *compressed = out;
+	const unsigned char *cur = in;
+	const unsigned char *end = in + in_len;
+
+	while (cur < end) {		/* Detect a maximum run of unchanged bytes. */
+		const unsigned char *run = cur;
+		const unsigned char *diff;
+		const unsigned char *stop;
+		int offset, cbyte;
+
+		while (cur < end && *cur == *prev) {
+			cur++, prev++;
+		}
+		if (cur == end)
+			break;		/* rest of row is unchanged */
+		/* Detect a run of up to 8 changed bytes. */
+		/* We know that *cur != *prev. */
+		diff = cur;
+		stop = (end - cur > 8 ? cur + 8 : end);
+		do
+		{
+			*prev++ = *cur++;
+		}
+		while (cur < stop && *cur != *prev);
+		/* Now [run..diff) are unchanged, and */
+		/* [diff..cur) are changed. */
+		/* Generate the command byte(s). */
+		offset = diff - run;
+		cbyte = (cur - diff - 1) << 5;
+		if (offset < 31)
+			*out++ = cbyte + offset;
+		else {
+			*out++ = cbyte + 31;
+			offset -= 31;
+			while (offset >= 255)
+				*out++ = 255, offset -= 255;
+			*out++ = offset;
+		}
+		/* Copy the changed data. */
+		while (diff < cur)
+			*out++ = *diff++;
+	}
+	return out - compressed;
+}
+
+void
+fz_write_bitmap_as_pcl(fz_context *ctx, fz_output *out, const fz_bitmap *bitmap, const fz_pcl_options *pcl)
+{
+	fz_band_writer *writer;
+
+	if (!bitmap || !out)
+		return;
+
+	writer = fz_new_mono_pcl_band_writer(ctx, out, pcl);
+	fz_try(ctx)
+	{
+		fz_write_header(ctx, writer, bitmap->w, bitmap->h, 1, 0, bitmap->xres, bitmap->yres, 0, NULL, NULL);
+		fz_write_band(ctx, writer, bitmap->stride, bitmap->h, bitmap->samples);
+		fz_close_band_writer(ctx, writer);
+	}
+	fz_always(ctx)
+		fz_drop_band_writer(ctx, writer);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+typedef struct mono_pcl_band_writer_s
+{
+	fz_band_writer super;
+	fz_pcl_options options;
+	unsigned char *prev;
+	unsigned char *mode2buf;
+	unsigned char *mode3buf;
+	int top_of_page;
+	int num_blank_lines;
+} mono_pcl_band_writer;
+
+static void
+mono_pcl_write_header(fz_context *ctx, fz_band_writer *writer_, fz_colorspace *cs)
+{
+	mono_pcl_band_writer *writer = (mono_pcl_band_writer *)writer_;
+	fz_output *out = writer->super.out;
+	int w = writer->super.w;
+	int h = writer->super.h;
+	int xres = writer->super.xres;
+	int yres = writer->super.yres;
+	int line_size;
+	int max_mode_2_size;
+	int max_mode_3_size;
+
+	if (writer->super.alpha != 0)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "mono PCL cannot write alpha channel");
+	if (writer->super.s != 0)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "mono PCL cannot write spot colors");
+	if (writer->super.n != 1)
+		fz_throw(ctx, FZ_ERROR_ARGUMENT, "mono PCL must be grayscale");
+
+	line_size = (w + 7)/8;
+	max_mode_2_size = line_size + (line_size/127) + 1;
+	max_mode_3_size = line_size + (line_size/8) + 1;
+
+	writer->prev = fz_calloc(ctx, line_size, sizeof(unsigned char));
+	writer->mode2buf = fz_calloc(ctx, max_mode_2_size, sizeof(unsigned char));
+	writer->mode3buf = fz_calloc(ctx, max_mode_3_size, sizeof(unsigned char));
+	writer->num_blank_lines = 0;
+	writer->top_of_page = 1;
+
+	guess_paper_size(&writer->options, w, h, xres, yres);
+
+	if (writer->options.features & HACK__IS_A_OCE9050)
+	{
+		/* Enter HPGL/2 mode, begin plot, Initialise (start plot), Enter PCL mode */
+		fz_write_string(ctx, out, "\033%1BBPIN;\033%1A");
+	}
+
+	pcl_header(ctx, out, &writer->options, 1, xres, yres, w, h);
+}
+
+static void
+mono_pcl_write_band(fz_context *ctx, fz_band_writer *writer_, int ss, int band_start, int band_height, const unsigned char *data)
+{
+	mono_pcl_band_writer *writer = (mono_pcl_band_writer *)writer_;
+	fz_output *out = writer->super.out;
+	int w = writer->super.w;
+	int yres = writer->super.yres;
+	const unsigned char *out_data;
+	int y, rmask, line_size;
+	int num_blank_lines;
+	int compression = -1;
+	unsigned char *prev = NULL;
+	unsigned char *mode2buf = NULL;
+	unsigned char *mode3buf = NULL;
+	int out_count;
+	const fz_pcl_options *pcl;
+
+	if (!out)
+		return;
+
+	num_blank_lines = writer->num_blank_lines;
+	rmask = ~0 << (-w & 7);
+	line_size = (w + 7)/8;
+	prev = writer->prev;
+	mode2buf = writer->mode2buf;
+	mode3buf = writer->mode3buf;
+	pcl = &writer->options;
+
+	/* Transfer raster graphics. */
+	for (y = 0; y < band_height; y++, data += ss)
+	{
+		const unsigned char *end_data = data + line_size;
+
+		if ((end_data[-1] & rmask) == 0)
+		{
+			end_data--;
+			while (end_data > data && end_data[-1] == 0)
+				end_data--;
+		}
+		if (end_data == data)
+		{
+			/* Blank line */
+			num_blank_lines++;
+			continue;
+		}
+
+		/* We've reached a non-blank line. */
+		/* Put out a spacing command if necessary. */
+		if (writer->top_of_page)
+		{
+			writer->top_of_page = 0;
+			/* We're at the top of a page. */
+			if (pcl->features & PCL_ANY_SPACING)
+			{
+				if (num_blank_lines > 0)
+					fz_write_printf(ctx, out, "\033*p+%dY", num_blank_lines);
+				/* Start raster graphics. */
+				fz_write_string(ctx, out, "\033*r1A");
+			}
+			else if (pcl->features & PCL_MODE_3_COMPRESSION)
+			{
+				/* Start raster graphics. */
+				fz_write_string(ctx, out, "\033*r1A");
+				for (; num_blank_lines; num_blank_lines--)
+					fz_write_string(ctx, out, "\033*b0W");
+			}
+			else
+			{
+				/* Start raster graphics. */
+				fz_write_string(ctx, out, "\033*r1A");
+				for (; num_blank_lines; num_blank_lines--)
+					fz_write_string(ctx, out, "\033*bW");
+			}
+		}
+
+		/* Skip blank lines if any */
+		else if (num_blank_lines != 0)
+		{
+			/* Moving down from current position causes head
+			 * motion on the DeskJet, so if the number of lines
+			 * is small, we're better off printing blanks.
+			 *
+			 * For Canon LBP4i and some others, <ESC>*b<n>Y
+			 * doesn't properly clear the seed row if we are in
+			 * compression mode 3.
+			 */
+			if ((num_blank_lines < MIN_SKIP_LINES && compression != 3) ||
+					!(pcl->features & PCL_ANY_SPACING))
+			{
+				int mode_3ns = ((pcl->features & PCL_MODE_3_COMPRESSION) && !(pcl->features & PCL_ANY_SPACING));
+				if (mode_3ns && compression != 2)
+				{
+					/* Switch to mode 2 */
+					fz_write_string(ctx, out, from3to2);
+					compression = 2;
+				}
+				if (pcl->features & PCL_MODE_3_COMPRESSION)
+				{
+					/* Must clear the seed row. */
+					fz_write_string(ctx, out, "\033*b1Y");
+					num_blank_lines--;
+				}
+				if (mode_3ns)
+				{
+					for (; num_blank_lines; num_blank_lines--)
+						fz_write_string(ctx, out, "\033*b0W");
+				}
+				else
+				{
+					for (; num_blank_lines; num_blank_lines--)
+						fz_write_string(ctx, out, "\033*bW");
+				}
+			}
+			else if (pcl->features & PCL3_SPACING)
+				fz_write_printf(ctx, out, "\033*p+%dY", num_blank_lines * yres);
+			else
+				fz_write_printf(ctx, out, "\033*b%dY", num_blank_lines);
+			/* Clear the seed row (only matters for mode 3 compression). */
+			memset(prev, 0, line_size);
+		}
+		num_blank_lines = 0;
+
+		/* Choose the best compression mode for this particular line. */
+		if (pcl->features & PCL_MODE_3_COMPRESSION)
+		{
+			/* Compression modes 2 and 3 are both available. Try
+			 * both and see which produces the least output data.
+			 */
+			int count3 = mode3compress(mode3buf, data, prev, line_size);
+			int count2 = mode2compress(mode2buf, data, line_size);
+			int penalty3 = (compression == 3 ? 0 : penalty_from2to3);
+			int penalty2 = (compression == 2 ? 0 : penalty_from3to2);
+
+			if (count3 + penalty3 < count2 + penalty2)
+			{
+				if (compression != 3)
+					fz_write_string(ctx, out, from2to3);
+				compression = 3;
+				out_data = (unsigned char *)mode3buf;
+				out_count = count3;
+			}
+			else
+			{
+				if (compression != 2)
+					fz_write_string(ctx, out, from3to2);
+				compression = 2;
+				out_data = (unsigned char *)mode2buf;
+				out_count = count2;
+			}
+		}
+		else if (pcl->features & PCL_MODE_2_COMPRESSION)
+		{
+			out_data = mode2buf;
+			out_count = mode2compress(mode2buf, data, line_size);
+		}
+		else
+		{
+			out_data = data;
+			out_count = line_size;
+		}
+
+		/* Transfer the data */
+		fz_write_printf(ctx, out, "\033*b%dW", out_count);
+		fz_write_data(ctx, out, out_data, out_count);
+	}
+
+	writer->num_blank_lines = num_blank_lines;
+}
+
+static void
+mono_pcl_write_trailer(fz_context *ctx, fz_band_writer *writer_)
+{
+	mono_pcl_band_writer *writer = (mono_pcl_band_writer *)writer_;
+	fz_output *out = writer->super.out;
+
+	/* end raster graphics and eject page */
+	fz_write_string(ctx, out, "\033*rB\f");
+
+	if (writer->options.features & HACK__IS_A_OCE9050)
+	{
+		/* Pen up, pen select, advance full page, reset */
+		fz_write_string(ctx, out, "\033%1BPUSP0PG;\033E");
+	}
+}
+
+static void
+mono_pcl_drop_band_writer(fz_context *ctx, fz_band_writer *writer_)
+{
+	mono_pcl_band_writer *writer = (mono_pcl_band_writer *)writer_;
+
+	fz_free(ctx, writer->prev);
+	fz_free(ctx, writer->mode2buf);
+	fz_free(ctx, writer->mode3buf);
+}
+
+fz_band_writer *fz_new_mono_pcl_band_writer(fz_context *ctx, fz_output *out, const fz_pcl_options *options)
+{
+	mono_pcl_band_writer *writer = fz_new_band_writer(ctx, mono_pcl_band_writer, out);
+
+	writer->super.header = mono_pcl_write_header;
+	writer->super.band = mono_pcl_write_band;
+	writer->super.trailer = mono_pcl_write_trailer;
+	writer->super.drop = mono_pcl_drop_band_writer;
+
+	if (options)
+		writer->options = *options;
+	else
+		fz_pcl_preset(ctx, &writer->options, "generic");
+
+	return &writer->super;
+}
+
+void
+fz_save_pixmap_as_pcl(fz_context *ctx, fz_pixmap *pixmap, char *filename, int append, const fz_pcl_options *pcl)
+{
+	fz_output *out = fz_new_output_with_path(ctx, filename, append);
+	fz_try(ctx)
+	{
+		fz_write_pixmap_as_pcl(ctx, out, pixmap, pcl);
+		fz_close_output(ctx, out);
+	}
+	fz_always(ctx)
+		fz_drop_output(ctx, out);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+void
+fz_save_bitmap_as_pcl(fz_context *ctx, fz_bitmap *bitmap, char *filename, int append, const fz_pcl_options *pcl)
+{
+	fz_output *out = fz_new_output_with_path(ctx, filename, append);
+	fz_try(ctx)
+	{
+		fz_write_bitmap_as_pcl(ctx, out, bitmap, pcl);
+		fz_close_output(ctx, out);
+	}
+	fz_always(ctx)
+		fz_drop_output(ctx, out);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+/* High-level document writer interface */
+
+typedef struct
+{
+	fz_document_writer super;
+	fz_draw_options draw;
+	fz_pcl_options pcl;
+	fz_pixmap *pixmap;
+	int mono;
+	fz_output *out;
+} fz_pcl_writer;
+
+static fz_device *
+pcl_begin_page(fz_context *ctx, fz_document_writer *wri_, fz_rect mediabox)
+{
+	fz_pcl_writer *wri = (fz_pcl_writer*)wri_;
+	return fz_new_draw_device_with_options(ctx, &wri->draw, mediabox, &wri->pixmap);
+}
+
+static void
+pcl_end_page(fz_context *ctx, fz_document_writer *wri_, fz_device *dev)
+{
+	fz_pcl_writer *wri = (fz_pcl_writer*)wri_;
+	fz_bitmap *bitmap = NULL;
+
+	fz_var(bitmap);
+
+	fz_try(ctx)
+	{
+		fz_close_device(ctx, dev);
+		if (wri->mono)
+		{
+			bitmap = fz_new_bitmap_from_pixmap(ctx, wri->pixmap, NULL);
+			fz_write_bitmap_as_pcl(ctx, wri->out, bitmap, &wri->pcl);
+		}
+		else
+		{
+			fz_write_pixmap_as_pcl(ctx, wri->out, wri->pixmap, &wri->pcl);
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_device(ctx, dev);
+		fz_drop_bitmap(ctx, bitmap);
+		fz_drop_pixmap(ctx, wri->pixmap);
+		wri->pixmap = NULL;
+	}
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+pcl_close_writer(fz_context *ctx, fz_document_writer *wri_)
+{
+	fz_pcl_writer *wri = (fz_pcl_writer*)wri_;
+	fz_close_output(ctx, wri->out);
+}
+
+static void
+pcl_drop_writer(fz_context *ctx, fz_document_writer *wri_)
+{
+	fz_pcl_writer *wri = (fz_pcl_writer*)wri_;
+	fz_drop_pixmap(ctx, wri->pixmap);
+	fz_drop_output(ctx, wri->out);
+}
+
+fz_document_writer *
+fz_new_pcl_writer_with_output(fz_context *ctx, fz_output *out, const char *options)
+{
+	fz_pcl_writer *wri = NULL;
+	const char *val;
+
+	fz_var(wri);
+
+	fz_try(ctx)
+	{
+		wri = fz_new_derived_document_writer(ctx, fz_pcl_writer, pcl_begin_page, pcl_end_page, pcl_close_writer, pcl_drop_writer);
+		fz_parse_draw_options(ctx, &wri->draw, options);
+		fz_parse_pcl_options(ctx, &wri->pcl, options);
+		if (fz_has_option(ctx, options, "colorspace", &val))
+			if (fz_option_eq(val, "mono"))
+				wri->mono = 1;
+		wri->out = out;
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_output(ctx, out);
+		fz_free(ctx, wri);
+		fz_rethrow(ctx);
+	}
+
+	return (fz_document_writer*)wri;
+}
+
+fz_document_writer *
+fz_new_pcl_writer(fz_context *ctx, const char *path, const char *options)
+{
+	fz_output *out = fz_new_output_with_path(ctx, path ? path : "out.pcl", 0);
+	return fz_new_pcl_writer_with_output(ctx, out, options);
+}