diff mupdf-source/source/pdf/pdf-image-rewriter.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/pdf/pdf-image-rewriter.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,956 @@
+// 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 "mupdf/pdf.h"
+
+#include "../fitz/pixmap-imp.h"
+
+#include <string.h>
+#include <math.h>
+
+typedef struct
+{
+	int num;
+	int gen;
+	float dpi;
+} image_details;
+
+typedef struct
+{
+	int max;
+	int len;
+	int *uimg;
+} image_list;
+
+typedef struct
+{
+	int max;
+	int len;
+	image_details *img;
+} unique_image_list;
+
+typedef struct
+{
+	image_list list;
+	unique_image_list uilist;
+	pdf_image_rewriter_options *opts;
+	int which;
+} image_info;
+
+static float
+dpi_from_ctm(fz_matrix ctm, int w, int h)
+{
+	float expx = sqrtf(ctm.a * ctm.a + ctm.b * ctm.b);
+	float expy = sqrtf(ctm.c * ctm.c + ctm.d * ctm.d);
+	float dpix = w * 72.0f / (expx == 0 ? 1 : expx);
+	float dpiy = h * 72.0f / (expy == 0 ? 1 : expy);
+
+	if (dpix > dpiy)
+		return dpiy;
+
+	return dpix;
+}
+
+static void
+gather_image_rewrite(fz_context *ctx, void *opaque, fz_image **image, fz_matrix ctm, pdf_obj *im_obj)
+{
+	image_info *info = (image_info *)opaque;
+	image_list *ilist = &info->list;
+	unique_image_list *uilist = &info->uilist;
+	int i, num, gen;
+	float dpi;
+
+	if (im_obj == NULL)
+		return; /* Inline image, don't need to pregather that. */
+
+	num = pdf_to_num(ctx, im_obj);
+	gen = pdf_to_gen(ctx, im_obj);
+
+	dpi = dpi_from_ctm(ctm, (*image)->w, (*image)->h);
+
+	/* Find this in the unique image list */
+	for (i = 0; i < uilist->len; i++)
+	{
+		/* Found one already. Keep the smaller of the dpi's. */
+		if (uilist->img[i].num == num &&
+			uilist->img[i].gen == gen)
+		{
+			if (dpi < uilist->img[i].dpi)
+				uilist->img[i].dpi = dpi;
+			break;
+		}
+	}
+
+	if (i == uilist->len)
+	{
+		/* Need to add a new unique image. */
+		if (uilist->max == uilist->len)
+		{
+			int max2 = uilist->max * 2;
+			if (max2 == 0)
+				max2 = 32; /* Arbitrary */
+			uilist->img = fz_realloc(ctx, uilist->img, max2 * sizeof(*uilist->img));
+			uilist->max = max2;
+		}
+
+		uilist->img[uilist->len].num = num;
+		uilist->img[uilist->len].gen = gen;
+		uilist->img[uilist->len].dpi = dpi;
+		uilist->len++;
+	}
+
+	/* Now we need to add an entry in the unique image list saying that entry n in
+	 * that list corresponds to the ith unique image. */
+	if (ilist->max == ilist->len)
+	{
+		int max2 = ilist->max * 2;
+		if (max2 == 0)
+			max2 = 32; /* Arbitrary */
+		ilist->uimg = fz_realloc(ctx, ilist->uimg, max2 * sizeof(*ilist->uimg));
+		ilist->max = max2;
+	}
+
+	ilist->uimg[ilist->len++] = i;
+}
+
+typedef enum
+{
+	IMAGE_COLOR = 0,
+	IMAGE_GRAY,
+	IMAGE_BITONAL
+} image_type;
+
+static image_type
+classify_pixmap(fz_context *ctx, fz_pixmap *pix)
+{
+	/* For now, spots means color. In future we could check to
+	 * see if all the spots were 0? */
+	if (pix->s)
+		return IMAGE_COLOR;
+
+	if (fz_colorspace_is_gray(ctx, pix->colorspace))
+	{
+		int n = pix->n;
+		int h = pix->h;
+		ptrdiff_t span_step = pix->stride - pix->w * n;
+		const unsigned char *s = pix->samples;
+
+		/* Loop until we know it's not bitonal */
+		if (pix->alpha)
+		{
+			while (h--)
+			{
+				int w = pix->w;
+
+				while (w--)
+				{
+					if (s[1] == 0)
+					{
+						/* If alpha is zero, other components don't matter. */
+					}
+					else if (s[0] != 0 && s[0] != 255)
+						return IMAGE_GRAY;
+					s += 2;
+				}
+				s += span_step;
+			}
+			return IMAGE_BITONAL;
+		}
+		else
+		{
+			while (h--)
+			{
+				int w = pix->w;
+
+				while (w--)
+				{
+					if (s[0] != 0 && s[0] != 255)
+						return IMAGE_GRAY;
+					s++;
+				}
+				s += span_step;
+			}
+			return IMAGE_BITONAL;
+		}
+	}
+	else if (fz_colorspace_is_rgb(ctx, pix->colorspace))
+	{
+		int n = pix->n;
+		int h = pix->h;
+		int w;
+		ptrdiff_t span_step = pix->stride - pix->w * n;
+		const unsigned char *s = pix->samples;
+
+		/* Is this safe, cos of profiles? */
+		if (pix->alpha)
+		{
+			/* Loop until we know it's not bitonal */
+			while (h--)
+			{
+				w = pix->w;
+				while (w--)
+				{
+					if (s[3] == 0)
+					{
+						/* If alpha is zero, other components don't matter. */
+					}
+					else if (s[0] == s[1] && s[0] == s[2])
+					{
+						/* Plausibly gray */
+						if (s[0] != 0 && s[0] != 255)
+							goto rgba_not_bitonal; /* But not bitonal */
+						if (s[3] != 0 && s[3] != 255)
+							goto rgba_not_bitonal;
+					}
+					else
+						return IMAGE_COLOR;
+					s += n;
+				}
+				s += span_step;
+			}
+			return IMAGE_BITONAL;
+
+			/* Loop until we know it's not gray */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[3] == 0)
+					{
+						/* If alpha is zero, other components don't matter. */
+					}
+					else if (s[0] != s[1] || s[0] != s[2])
+						return IMAGE_COLOR;
+rgba_not_bitonal:
+					s += n;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+		}
+		else
+		{
+			/* Loop until we know it's not bitonal */
+			while (h--)
+			{
+				w = pix->w;
+				while (w--)
+				{
+					if (s[0] == s[1] && s[0] == s[2])
+					{
+						if (s[0] != 0 && s[0] != 255)
+							goto rgb_not_bitonal;
+					}
+					else
+						return IMAGE_COLOR;
+					s += n;
+				}
+				s += span_step;
+			}
+			return IMAGE_BITONAL;
+
+			/* Loop until we know it's not gray */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[0] != s[1] || s[0] != s[2])
+						return IMAGE_COLOR;
+rgb_not_bitonal:
+					s += n;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+		}
+	}
+	else if (fz_colorspace_is_cmyk(ctx, pix->colorspace))
+	{
+		int n = pix->n;
+		int h = pix->h;
+		int w;
+		ptrdiff_t span_step = pix->stride - pix->w * n;
+		const unsigned char *s = pix->samples;
+
+		if (pix->alpha)
+		{
+			/* Loop until we know it's not bitonal */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[4] == 0)
+					{
+						/* If alpha is 0, other components don't matter. */
+					}
+					else if (s[0] == 0 && s[1] == 0 && s[2] == 0)
+					{
+						if (s[3] != 0 && s[3] != 255)
+							goto cmyka_not_bitonal;
+					}
+					else
+						return IMAGE_COLOR;
+					s += 5;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+
+			/* Loop until we know it's not gray */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[4] == 0)
+					{
+						/* If alpha is 0, other components don't matter. */
+					}
+					else if (s[0] != 0 || s[1] != 0 || s[2] != 0)
+						return IMAGE_COLOR;
+cmyka_not_bitonal:
+					s += 5;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+		}
+		else
+		{
+			/* Loop until we know it's not bitonal */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[0] == 0 && s[1] == 0 && s[2] != 0)
+					{
+						if (s[3] != 0 && s[3] != 255)
+							goto cmyk_not_bitonal;
+					}
+					else
+						return IMAGE_COLOR;
+					s += 4;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+
+			/* Loop until we know it's not gray */
+			while (h--)
+			{
+				w = pix->w;
+
+				while (w--)
+				{
+					if (s[0] != 0 || s[1] != 0 || s[2] != 0)
+						return IMAGE_COLOR;
+cmyk_not_bitonal:
+					s += 4;
+				}
+				s += span_step;
+			}
+			return IMAGE_GRAY;
+		}
+	}
+	return IMAGE_COLOR;
+}
+
+static fz_pixmap *
+resample(fz_context *ctx, fz_pixmap *src, int method, float from_dpi, float to_dpi)
+{
+	int w2 = src->w;
+	int h2 = src->h;
+	int w = (int)(w2 * to_dpi / from_dpi + 0.5f);
+	int h = (int)(h2 * to_dpi / from_dpi + 0.5f);
+	int factor;
+
+	/* Allow for us shrinking an image to 0.*/
+	assert(w >= 0 && h >= 0);
+	if (w == 0)
+		w = 1;
+	if (h == 0)
+		h = 1;
+
+	/* Allow for the possibility that we might only want to make such a tiny change
+	 * in dpi that the image doesn't really resize. */
+	if (w >= w2 && h >= h2)
+		return NULL;
+
+	if (method == FZ_SUBSAMPLE_BICUBIC)
+	{
+		fz_irect clip = { 0, 0, w, h };
+		return fz_scale_pixmap(ctx, src, 0, 0, w, h, &clip);
+	}
+
+	factor = 0;
+	while (1)
+	{
+		int w3 = (w2+1)/2;
+		int h3 = (h2+1)/2;
+		if (w3 <= w || h3 <= h)
+			break;
+		factor++;
+		w2 = w3;
+		h2 = h3;
+	}
+
+	fz_subsample_pixmap(ctx, src, factor);
+
+	return fz_keep_pixmap(ctx, src);
+}
+
+static fz_compressed_buffer *
+fz_recompress_image_as_jpeg(fz_context *ctx, fz_pixmap *pix, const char *quality, fz_colorspace **cs)
+{
+	fz_compressed_buffer *cbuf = NULL;
+	fz_pixmap *rgb = NULL;
+	int q = fz_atoi(quality);
+
+	if (q == 0)
+		q = 75; /* Default quality */
+
+	if (!pix->colorspace)
+		return NULL;
+
+	if (!fz_colorspace_is_cmyk(ctx, pix->colorspace) &&
+		!fz_colorspace_is_gray(ctx, pix->colorspace) &&
+		!fz_colorspace_is_rgb(ctx, pix->colorspace))
+	{
+		/* We're going to need to convert colorspace. */
+		/* It's not gray, so we need a color space - pick rgb. */
+		pix = rgb = fz_convert_pixmap(ctx, pix, fz_device_rgb(ctx), NULL, NULL, fz_default_color_params, 0);
+		*cs = fz_device_rgb(ctx);
+	}
+
+	fz_var(cbuf);
+
+	fz_try(ctx)
+	{
+		cbuf = fz_new_compressed_buffer(ctx);
+		cbuf->buffer = fz_new_buffer_from_pixmap_as_jpeg(ctx, pix, fz_default_color_params, q, 0);
+		cbuf->params.type = FZ_IMAGE_JPEG;
+		cbuf->params.u.jpeg.color_transform = -2;
+	}
+	fz_always(ctx)
+	{
+		if (rgb)
+			fz_drop_pixmap(ctx, rgb);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_compressed_buffer(ctx, cbuf);
+		fz_rethrow(ctx);
+	}
+
+	return cbuf;
+}
+
+static fz_compressed_buffer *
+fz_recompress_image_as_j2k(fz_context *ctx, fz_pixmap *pix, const char *quality)
+{
+	fz_compressed_buffer *cbuf = fz_new_compressed_buffer(ctx);
+	fz_output *out = NULL;
+	int q = fz_atoi(quality);
+
+	if (q <= 0)
+		q = 80; /* Default 1:20 compression */
+	if (q > 100)
+		q = 100;
+
+	fz_var(out);
+
+	fz_try(ctx)
+	{
+		cbuf->buffer = fz_new_buffer(ctx, 1024);
+		out = fz_new_output_with_buffer(ctx, cbuf->buffer);
+
+		fz_write_pixmap_as_jpx(ctx, out, pix, q);
+		cbuf->params.type = FZ_IMAGE_JPX;
+		cbuf->params.u.jpx.smask_in_data = 0;
+	}
+	fz_always(ctx)
+	{
+		fz_drop_output(ctx, out);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_compressed_buffer(ctx, cbuf);
+		fz_rethrow(ctx);
+	}
+
+	return cbuf;
+}
+
+static fz_compressed_buffer *
+fz_recompress_image_as_flate(fz_context *ctx, fz_pixmap *pix, const char *quality)
+{
+	fz_compressed_buffer *cbuf = fz_new_compressed_buffer(ctx);
+	fz_output *out = NULL;
+	fz_output *out2 = NULL;
+	int h = pix->h;
+	size_t n = (size_t) pix->w * pix->n;
+	const unsigned char *samp = pix->samples;
+	ptrdiff_t str = pix->stride;
+	int q = fz_atoi(quality);
+
+	/* Notionally, it's 0-100 */
+	q /= 11;
+	if (q > FZ_DEFLATE_BEST)
+		q = FZ_DEFLATE_BEST;
+	if (q <= 0)
+		q = FZ_DEFLATE_DEFAULT;
+
+	fz_var(out);
+	fz_var(out2);
+
+	fz_try(ctx)
+	{
+		cbuf->buffer = fz_new_buffer(ctx, 1024);
+		out = fz_new_output_with_buffer(ctx, cbuf->buffer);
+		out2 = fz_new_deflate_output(ctx, out, q, 0);
+
+		while (h--)
+		{
+			fz_write_data(ctx, out2, samp, n);
+			samp += str;
+		}
+
+		fz_close_output(ctx, out2);
+		fz_drop_output(ctx, out2);
+		out2 = NULL;
+		fz_close_output(ctx, out);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_output(ctx, out2);
+		fz_drop_output(ctx, out);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_compressed_buffer(ctx, cbuf);
+		fz_rethrow(ctx);
+	}
+
+	cbuf->params.type = FZ_IMAGE_FLATE;
+	cbuf->params.u.flate.bpc = 8;
+	cbuf->params.u.flate.colors = 0;
+	cbuf->params.u.flate.predictor = 0;
+	cbuf->params.u.flate.columns = 0;
+
+	return cbuf;
+}
+
+static fz_compressed_buffer *
+fz_recompress_image_as_fax(fz_context *ctx, fz_pixmap *pix)
+{
+	/* FIXME: Should get default colorspaces from the doc! */
+	fz_default_colorspaces *defcs = fz_new_default_colorspaces(ctx);
+	fz_compressed_buffer *cbuf = NULL;
+	fz_halftone *ht = NULL;
+	fz_bitmap *bmp = NULL;
+	fz_buffer *inv_buffer = NULL;
+
+	fz_var(ht);
+	fz_var(bmp);
+	fz_var(cbuf);
+	fz_var(inv_buffer);
+
+	fz_keep_pixmap(ctx, pix);
+	fz_try(ctx)
+	{
+
+		/* Convert to alphaless grey */
+		if (pix->n != 1)
+		{
+			fz_pixmap *pix2 = fz_convert_pixmap(ctx, pix, fz_device_gray(ctx), NULL, defcs, fz_default_color_params, 0);
+
+			fz_drop_pixmap(ctx, pix);
+			pix = pix2;
+		}
+
+		/* Convert to a bitmap */
+		ht = fz_default_halftone(ctx, 1);
+
+		bmp = fz_new_bitmap_from_pixmap(ctx, pix, ht);
+
+		cbuf = fz_new_compressed_buffer(ctx);
+		cbuf->buffer = fz_compress_ccitt_fax_g4(ctx, bmp->samples, bmp->w, bmp->h, bmp->stride);
+		cbuf->params.type = FZ_IMAGE_FAX;
+		cbuf->params.u.fax.k = -1;
+		cbuf->params.u.fax.columns = pix->w;
+		cbuf->params.u.fax.rows = pix->h;
+
+		fz_invert_bitmap(ctx, bmp);
+		inv_buffer = fz_compress_ccitt_fax_g4(ctx, bmp->samples, bmp->w, bmp->h, bmp->stride);
+
+		/* cbuf->buffer requires "/BlackIs1 true ", so it needs to beats the inverted one by
+		 * at least 15 bytes, or we'll use the inverted one. */
+		if (cbuf->buffer->len + 15 < inv_buffer->len)
+		{
+			cbuf->params.u.fax.black_is_1 = 1;
+		}
+		else
+		{
+			fz_drop_buffer(ctx, cbuf->buffer);
+			cbuf->buffer = inv_buffer;
+			inv_buffer = NULL;
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_bitmap(ctx, bmp);
+		fz_drop_halftone(ctx, ht);
+		fz_drop_pixmap(ctx, pix);
+		fz_drop_buffer(ctx, inv_buffer);
+		fz_drop_default_colorspaces(ctx, defcs);
+	}
+	fz_catch(ctx)
+	{
+		fz_drop_compressed_buffer(ctx, cbuf);
+		fz_rethrow(ctx);
+	}
+
+	return cbuf;
+}
+
+static int method_from_fmt(int fmt)
+{
+	switch (fmt)
+	{
+	case FZ_IMAGE_JPEG:
+		return FZ_RECOMPRESS_JPEG;
+	case FZ_IMAGE_JPX:
+		return FZ_RECOMPRESS_J2K;
+	case FZ_IMAGE_FAX:
+		return FZ_RECOMPRESS_FAX;
+	}
+	return FZ_RECOMPRESS_LOSSLESS;
+}
+
+static fz_image *
+recompress_image(fz_context *ctx, fz_pixmap *pix, int type, int fmt, int method, const char *quality, fz_image *oldimg)
+{
+	int interpolate = oldimg->interpolate;
+	fz_compressed_buffer *cbuf = NULL;
+	fz_colorspace *cs = pix->colorspace;
+	int bpc = 8;
+
+	if (method == FZ_RECOMPRESS_NEVER)
+		return NULL;
+
+	if (method == FZ_RECOMPRESS_SAME)
+		method = method_from_fmt(fmt);
+
+	if (method == FZ_RECOMPRESS_J2K)
+		cbuf = fz_recompress_image_as_j2k(ctx, pix, quality);
+	if (method == FZ_RECOMPRESS_JPEG)
+		cbuf = fz_recompress_image_as_jpeg(ctx, pix, quality, &cs);
+	if (method == FZ_RECOMPRESS_FAX)
+	{
+		cbuf = fz_recompress_image_as_fax(ctx, pix);
+		if (cbuf)
+		{
+			bpc = 1;
+			cs = fz_device_gray(ctx);
+		}
+	}
+	if (cbuf == NULL)
+		cbuf = fz_recompress_image_as_flate(ctx, pix, quality);
+
+	if (cbuf == NULL)
+		return NULL;
+
+	/* fz_new_image_from_compressed_buffer takes ownership of compressed buffer, even
+	 * in failure case. */
+	return fz_new_image_from_compressed_buffer(ctx, pix->w, pix->h, bpc, cs, pix->xres, pix->yres, interpolate, 0, NULL, NULL, cbuf, oldimg->mask);
+}
+
+static void
+do_image_rewrite(fz_context *ctx, void *opaque, fz_image **image, fz_matrix ctm, pdf_obj *im_obj)
+{
+	image_info *info = (image_info *)opaque;
+	image_list *ilist = &info->list;
+	unique_image_list *uilist = &info->uilist;
+	float dpi;
+	fz_pixmap *pix;
+	fz_pixmap *newpix = NULL;
+	image_type type;
+	int fmt = fz_compressed_image_type(ctx, *image);
+	int lossy = fz_is_lossy_image(ctx, *image);
+	size_t orig_len = pdf_dict_get_int64(ctx, im_obj, PDF_NAME(Length));
+
+	/* FIXME: We don't recompress im_obj->mask! */
+
+	/* Can't recompress colorkeyed images, currently. */
+	if ((*image)->use_colorkey)
+		return;
+	/* Can't recompress scalable images. */
+	if ((*image)->scalable)
+		return;
+
+	/* Can't rewrite separation ones, currently, as we can't pdf_add_image a separation image. */
+	if (fz_colorspace_is_indexed(ctx, (*image)->colorspace) &&
+		fz_colorspace_is_device_n(ctx, (*image)->colorspace->u.indexed.base))
+		return;
+	if (fz_colorspace_is_device_n(ctx, (*image)->colorspace))
+		return;
+
+	if (im_obj == NULL)
+		dpi = dpi_from_ctm(ctm, (*image)->w, (*image)->h);
+	else
+		dpi = uilist->img[ilist->uimg[info->which++]].dpi;
+
+	/* What sort of image is this? */
+	pix = fz_get_pixmap_from_image(ctx, *image, NULL, NULL, NULL, NULL);
+	type = classify_pixmap(ctx, pix);
+
+	fz_var(newpix);
+
+	fz_try(ctx)
+	{
+		fz_image *newimg = NULL;
+
+		if (type == IMAGE_BITONAL &&
+			info->opts->bitonal_image_recompress_method != FZ_RECOMPRESS_NEVER &&
+			info->opts->bitonal_image_subsample_threshold != 0 &&
+			dpi > info->opts->bitonal_image_subsample_threshold)
+		{
+			/* Resample a bitonal image. */
+			newpix = resample(ctx, pix, info->opts->bitonal_image_subsample_method, dpi, info->opts->bitonal_image_subsample_to);
+		}
+		else if (type == IMAGE_COLOR && lossy &&
+			info->opts->color_lossy_image_recompress_method != FZ_RECOMPRESS_NEVER &&
+			info->opts->color_lossy_image_subsample_threshold != 0 &&
+			dpi > info->opts->color_lossy_image_subsample_threshold)
+		{
+			/* Resample a lossily encoded color image. */
+			newpix = resample(ctx, pix, info->opts->color_lossy_image_subsample_method, dpi, info->opts->color_lossy_image_subsample_to);
+		}
+		else if (type == IMAGE_COLOR && !lossy &&
+			info->opts->color_lossless_image_recompress_method != FZ_RECOMPRESS_NEVER &&
+			info->opts->color_lossless_image_subsample_threshold != 0 &&
+			dpi > info->opts->color_lossless_image_subsample_threshold)
+		{
+			/* Resample a losslessly color image. */
+			newpix = resample(ctx, pix, info->opts->color_lossless_image_subsample_method, dpi, info->opts->color_lossless_image_subsample_to);
+		}
+		else if (type == IMAGE_GRAY && lossy &&
+			info->opts->gray_lossy_image_recompress_method != FZ_RECOMPRESS_NEVER &&
+			info->opts->gray_lossy_image_subsample_threshold != 0 &&
+			dpi > info->opts->gray_lossy_image_subsample_threshold)
+		{
+			/* Resample a lossily encoded gray image. */
+			newpix = resample(ctx, pix, info->opts->gray_lossy_image_subsample_method, dpi, info->opts->gray_lossy_image_subsample_to);
+		}
+		else if (type == IMAGE_GRAY && !lossy &&
+			info->opts->gray_lossless_image_recompress_method != FZ_RECOMPRESS_NEVER &&
+			info->opts->gray_lossless_image_subsample_threshold != 0 &&
+			dpi > info->opts->gray_lossless_image_subsample_threshold)
+		{
+			/* Resample a losslessly encoded gray image. */
+			newpix = resample(ctx, pix, info->opts->gray_lossless_image_subsample_method, dpi, info->opts->gray_lossless_image_subsample_to);
+		}
+
+		if (newpix)
+		{
+			/* We've scaled (or otherwise converted the image). So it needs to be compressed. */
+			if (type == IMAGE_COLOR)
+			{
+				if (lossy)
+					newimg = recompress_image(ctx, newpix, type, fmt, info->opts->color_lossy_image_recompress_method, info->opts->color_lossy_image_recompress_quality, *image);
+				else
+					newimg = recompress_image(ctx, newpix, type, fmt, info->opts->color_lossless_image_recompress_method, info->opts->color_lossless_image_recompress_quality, *image);
+			}
+			else if (type == IMAGE_GRAY)
+			{
+				if (lossy)
+					newimg = recompress_image(ctx, newpix, type, fmt, info->opts->gray_lossy_image_recompress_method, info->opts->gray_lossy_image_recompress_quality, *image);
+				else
+					newimg = recompress_image(ctx, newpix, type, fmt, info->opts->gray_lossless_image_recompress_method, info->opts->gray_lossless_image_recompress_quality, *image);
+			}
+			else if (type == IMAGE_BITONAL)
+				newimg = recompress_image(ctx, newpix, type, fmt, info->opts->bitonal_image_recompress_method, info->opts->bitonal_image_recompress_quality, *image);
+		}
+		else if (type == IMAGE_COLOR)
+		{
+			if (lossy)
+				newimg = recompress_image(ctx, pix, type, fmt, info->opts->color_lossy_image_recompress_method, info->opts->color_lossy_image_recompress_quality, *image);
+			else
+				newimg = recompress_image(ctx, pix, type, fmt, info->opts->color_lossless_image_recompress_method, info->opts->color_lossless_image_recompress_quality, *image);
+		}
+		else if (type == IMAGE_GRAY)
+		{
+			if (lossy)
+				newimg = recompress_image(ctx, pix, type, fmt, info->opts->gray_lossy_image_recompress_method, info->opts->gray_lossy_image_recompress_quality, *image);
+			else
+				newimg = recompress_image(ctx, pix, type, fmt, info->opts->gray_lossless_image_recompress_method, info->opts->gray_lossless_image_recompress_quality, *image);
+		}
+		else if (type == IMAGE_BITONAL)
+		{
+			newimg = recompress_image(ctx, pix, type, fmt, info->opts->bitonal_image_recompress_method, info->opts->bitonal_image_recompress_quality, *image);
+		}
+
+		if (newimg)
+		{
+			/* fz_image_size gives us the uncompressed size for losslessly compressed images
+			 * as the image holds the uncompressed buffer. But orig_len will be 0 for inline
+			 * images. So we have to combine the two. */
+			size_t oldsize = fz_image_size(ctx, *image);
+			size_t newsize = fz_image_size(ctx, newimg);
+			if (orig_len != 0)
+				oldsize = orig_len;
+			if (oldsize <= newsize)
+			{
+				/* Old one was smaller! Don't mess with it. */
+				fz_drop_image(ctx, newimg);
+			}
+			else
+			{
+				fz_drop_image(ctx, *image);
+				*image = newimg;
+			}
+		}
+	}
+	fz_always(ctx)
+	{
+		fz_drop_pixmap(ctx, newpix);
+		fz_drop_pixmap(ctx, pix);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow(ctx);
+	}
+}
+
+static void
+gather_image_info(fz_context *ctx, pdf_document *doc, int page_num, image_info *info)
+{
+	pdf_page *page = pdf_load_page(ctx, doc, page_num);
+	pdf_filter_options options = { 0 };
+	pdf_filter_factory list[2] = { 0 };
+	pdf_color_filter_options copts = { 0 };
+	pdf_annot *annot;
+
+	copts.opaque = info;
+	copts.color_rewrite = NULL;
+	copts.image_rewrite = gather_image_rewrite;
+	copts.shade_rewrite = NULL;
+	options.filters = list;
+	options.recurse = 1;
+	options.no_update = 1;
+	list[0].filter = pdf_new_color_filter;
+	list[0].options = &copts;
+
+	fz_try(ctx)
+	{
+		pdf_filter_page_contents(ctx, doc, page, &options);
+
+		for (annot = pdf_first_annot(ctx, page); annot != NULL; annot = pdf_next_annot(ctx, annot))
+			pdf_filter_annot_contents(ctx, doc, annot, &options);
+		for (annot = pdf_first_widget(ctx, page); annot != NULL; annot = pdf_next_annot(ctx, annot))
+			pdf_filter_annot_contents(ctx, doc, annot, &options);
+	}
+	fz_always(ctx)
+		fz_drop_page(ctx, &page->super);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+static void
+rewrite_image_info(fz_context *ctx, pdf_document *doc, int page_num, image_info *info)
+{
+	pdf_page *page = pdf_load_page(ctx, doc, page_num);
+	pdf_filter_options options = { 0 };
+	pdf_filter_factory list[2] = { 0 };
+	pdf_color_filter_options copts = { 0 };
+	pdf_annot *annot;
+
+	copts.opaque = info;
+	copts.color_rewrite = NULL;
+	copts.image_rewrite = do_image_rewrite;
+	copts.shade_rewrite = NULL;
+	options.filters = list;
+	options.recurse = 1;
+	list[0].filter = pdf_new_color_filter;
+	list[0].options = &copts;
+
+	fz_try(ctx)
+	{
+		pdf_filter_page_contents(ctx, doc, page, &options);
+
+		for (annot = pdf_first_annot(ctx, page); annot != NULL; annot = pdf_next_annot(ctx, annot))
+			pdf_filter_annot_contents(ctx, doc, annot, &options);
+		for (annot = pdf_first_widget(ctx, page); annot != NULL; annot = pdf_next_annot(ctx, annot))
+			pdf_filter_annot_contents(ctx, doc, annot, &options);
+	}
+	fz_always(ctx)
+		fz_drop_page(ctx, &page->super);
+	fz_catch(ctx)
+		fz_rethrow(ctx);
+}
+
+void pdf_rewrite_images(fz_context *ctx, pdf_document *doc, pdf_image_rewriter_options *opts)
+{
+	int i;
+	int n = pdf_count_pages(ctx, doc);
+	image_info info = { 0 };
+
+	info.opts = opts;
+
+	/* If nothing to do, do nothing! */
+	if (opts->bitonal_image_subsample_threshold == 0 &&
+		opts->gray_lossless_image_subsample_threshold == 0 &&
+		opts->gray_lossy_image_subsample_threshold == 0 &&
+		opts->color_lossless_image_subsample_threshold == 0 &&
+		opts->color_lossy_image_subsample_threshold == 0 &&
+		opts->bitonal_image_recompress_method == FZ_RECOMPRESS_NEVER &&
+		opts->color_lossy_image_recompress_method == FZ_RECOMPRESS_NEVER &&
+		opts->color_lossless_image_recompress_method == FZ_RECOMPRESS_NEVER &&
+		opts->gray_lossy_image_recompress_method == FZ_RECOMPRESS_NEVER &&
+		opts->gray_lossless_image_recompress_method == FZ_RECOMPRESS_NEVER)
+		return;
+
+	/* Pass 1: Gather information */
+	for (i = 0; i < n; i++)
+	{
+		gather_image_info(ctx, doc, i, &info);
+	}
+
+	/* Pass 2: Resample as required */
+	for (i = 0; i < n; i++)
+	{
+		rewrite_image_info(ctx, doc, i, &info);
+	}
+
+	fz_free(ctx, info.list.uimg);
+	fz_free(ctx, info.uilist.img);
+}