diff --git a/include/mupdf/fitz/compress.h b/include/mupdf/fitz/compress.h index 82d9af391c..9acf1cfd72 100644 --- a/include/mupdf/fitz/compress.h +++ b/include/mupdf/fitz/compress.h @@ -25,6 +25,7 @@ #include "mupdf/fitz/system.h" #include "mupdf/fitz/buffer.h" +#include "mupdf/fitz/pixmap.h" typedef enum { diff --git a/include/mupdf/fitz/image.h b/include/mupdf/fitz/image.h index f9e7931840..e57b0f9ef4 100644 --- a/include/mupdf/fitz/image.h +++ b/include/mupdf/fitz/image.h @@ -421,6 +421,13 @@ void fz_set_pixmap_image_tile(fz_context *ctx, fz_pixmap_image *cimg, fz_pixmap */ fz_pixmap *fz_load_jpx(fz_context *ctx, const unsigned char *data, size_t size, fz_colorspace *cs); +/** + Exposed because compression and decompression need to share this. +*/ +void opj_lock(fz_context *ctx); +void opj_unlock(fz_context *ctx); + + /** Exposed for CBZ. */ diff --git a/include/mupdf/fitz/write-pixmap.h b/include/mupdf/fitz/write-pixmap.h index 7fc78c789c..10bb40f7cf 100644 --- a/include/mupdf/fitz/write-pixmap.h +++ b/include/mupdf/fitz/write-pixmap.h @@ -235,6 +235,22 @@ void fz_save_pixmap_as_jpeg(fz_context *ctx, fz_pixmap *pixmap, const char *file */ void fz_write_pixmap_as_png(fz_context *ctx, fz_output *out, const fz_pixmap *pixmap); +/** + Pixmap data as JP2K with no subsampling. + + quality = 100 = lossless + otherwise for a factor of x compression use 100-x. (so 80 is 1:20 compression) +*/ +void fz_write_pixmap_as_jpx(fz_context *ctx, fz_output *out, fz_pixmap *pix, int quality); + +/** + Save pixmap data as JP2K with no subsampling. + + quality = 100 = lossless + otherwise for a factor of x compression use 100-x. (so 80 is 1:20 compression) +*/ +void fz_save_pixmap_as_jpx(fz_context *ctx, fz_pixmap *pixmap, const char *filename, int q); + /** Create a new png band writer (greyscale or RGB, with or without alpha). @@ -251,6 +267,7 @@ fz_buffer *fz_new_buffer_from_image_as_pnm(fz_context *ctx, fz_image *image, fz_ fz_buffer *fz_new_buffer_from_image_as_pam(fz_context *ctx, fz_image *image, fz_color_params color_params); fz_buffer *fz_new_buffer_from_image_as_psd(fz_context *ctx, fz_image *image, fz_color_params color_params); fz_buffer *fz_new_buffer_from_image_as_jpeg(fz_context *ctx, fz_image *image, fz_color_params color_params, int quality, int invert_cmyk); +fz_buffer *fz_new_buffer_from_image_as_jpx(fz_context *ctx, fz_image *image, fz_color_params color_params, int quality); /** Reencode a given pixmap as a PNG into a buffer. @@ -262,6 +279,7 @@ fz_buffer *fz_new_buffer_from_pixmap_as_pnm(fz_context *ctx, fz_pixmap *pixmap, fz_buffer *fz_new_buffer_from_pixmap_as_pam(fz_context *ctx, fz_pixmap *pixmap, fz_color_params color_params); fz_buffer *fz_new_buffer_from_pixmap_as_psd(fz_context *ctx, fz_pixmap *pix, fz_color_params color_params); fz_buffer *fz_new_buffer_from_pixmap_as_jpeg(fz_context *ctx, fz_pixmap *pixmap, fz_color_params color_params, int quality, int invert_cmyk); +fz_buffer *fz_new_buffer_from_pixmap_as_jpx(fz_context *ctx, fz_pixmap *pix, fz_color_params color_params, int quality); /** Save a pixmap as a pnm (greyscale or rgb, no alpha). diff --git a/platform/win32/libmupdf.vcxproj b/platform/win32/libmupdf.vcxproj index 5bd249418f..8d83b0831d 100644 --- a/platform/win32/libmupdf.vcxproj +++ b/platform/win32/libmupdf.vcxproj @@ -1064,6 +1064,7 @@ + diff --git a/platform/win32/libmupdf.vcxproj.filters b/platform/win32/libmupdf.vcxproj.filters index d5a27d92c1..a68ba7ce3b 100644 --- a/platform/win32/libmupdf.vcxproj.filters +++ b/platform/win32/libmupdf.vcxproj.filters @@ -636,6 +636,9 @@ pdf + + fitz + diff --git a/source/fitz/encode-jpx.c b/source/fitz/encode-jpx.c new file mode 100644 index 0000000000..b6c79925a6 --- /dev/null +++ b/source/fitz/encode-jpx.c @@ -0,0 +1,345 @@ +// Copyright (C) 2004-2021 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 +// +// Alternative licensing terms are available from the licensor. +// For commercial licensing, see or contact +// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco, +// CA 94129, USA, for further information. + +#include "mupdf/fitz.h" + +#include + + +static opj_image_t * +image_from_pixmap(fz_context *ctx, fz_pixmap *pix) +{ + opj_image_cmptparm_t cmptparm[FZ_MAX_COLORS] = { 0 }; + OPJ_INT32 *data[FZ_MAX_COLORS]; + int i; + opj_image_t *image; + OPJ_COLOR_SPACE cs; + + if (pix->alpha || pix->s) + fz_throw(ctx, FZ_ERROR_GENERIC, "No spots/alpha for JPX encode"); + + if (fz_colorspace_is_cmyk(ctx, pix->colorspace)) + cs = OPJ_CLRSPC_CMYK; + else if (fz_colorspace_is_rgb(ctx, pix->colorspace)) + cs = OPJ_CLRSPC_SRGB; + else if (fz_colorspace_is_gray(ctx, pix->colorspace)) + cs = OPJ_CLRSPC_GRAY; + else + fz_throw(ctx, FZ_ERROR_GENERIC, "Invalid colorspace for JPX encode"); + + /* Create image */ + for (i = 0; i < pix->n; ++i) + { + cmptparm[i].prec = 8; + cmptparm[i].sgnd = 0; + cmptparm[i].dx = 1; + cmptparm[i].dy = 1; + cmptparm[i].w = (OPJ_UINT32)pix->w; + cmptparm[i].h = (OPJ_UINT32)pix->h; + } + + image = opj_image_create(pix->n, &cmptparm[0], cs); + if (image == NULL) + fz_throw(ctx, FZ_ERROR_GENERIC, "OPJ image creation failed"); + + image->x0 = 0; + image->y0 = 0; + image->x1 = pix->w; + image->y1 = pix->h; + + for (i = 0; i < pix->n; ++i) + data[i] = image->comps[i].data; + + { + int w = pix->w; + int stride = pix->stride; + int n = pix->n; + int x, y, k; + unsigned char *s = pix->samples; + for (y = pix->h; y > 0; y--) + { + unsigned char *s2 = s; + s += stride; + for (k = 0; k < n; k++) + { + unsigned char *s3 = s2++; + OPJ_INT32 *d = data[k]; + data[k] += w; + for (x = w; x > 0; x--) + { + *d++ = (*s3); + s3 += n; + } + } + } + } + + return image; +} + +typedef struct +{ + fz_context *ctx; /* Safe */ + fz_output *out; +} my_stream; + +static void +close_stm(void *user_data) +{ + my_stream *stm = (my_stream *)user_data; + + /* Nothing to see here. */ + fz_close_output(stm->ctx, stm->out); +} + +static OPJ_SIZE_T +write_stm(void *p_buffer, OPJ_SIZE_T p_nb_bytes, void *p_user_data) +{ + my_stream *stm = (my_stream *)p_user_data; + + fz_try(stm->ctx) + fz_write_data(stm->ctx, stm->out, p_buffer, p_nb_bytes); + fz_catch(stm->ctx) + return -1; + + return p_nb_bytes; +} + +static OPJ_OFF_T skip_stm(OPJ_OFF_T p_nb_bytes, void *p_user_data) +{ + my_stream *stm = (my_stream *)p_user_data; + + stm = stm; + + return -1; +} + +static OPJ_BOOL seek_stm(OPJ_OFF_T p_nb_bytes, void *p_user_data) +{ + my_stream *stm = (my_stream *)p_user_data; + + stm = stm; + + return 0; +} + +static void +info_callback(const char *msg, void *client_data) +{ + fz_context *ctx = (fz_context *)client_data; + + fz_warn(ctx, "INFO: %s", msg); +} + +static void +warning_callback(const char *msg, void *client_data) +{ + fz_context *ctx = (fz_context *)client_data; + + fz_warn(ctx, "WARNING: %s", msg); +} + +static void +error_callback(const char *msg, void *client_data) +{ + fz_context *ctx = (fz_context *)client_data; + + fz_warn(ctx, "ERROR: %s", msg); +} + +void +fz_write_pixmap_as_jpx(fz_context *ctx, fz_output *out, fz_pixmap *pix, int q) +{ + opj_cparameters_t parameters; /* compression parameters */ + + opj_stream_t *l_stream = 00; + opj_codec_t* l_codec = 00; + opj_image_t *image = NULL; + OPJ_BOOL bSuccess; + + my_stream stm; + + fz_var(image); + + opj_lock(ctx); + fz_try(ctx) + { + image = image_from_pixmap(ctx, pix); + stm.ctx = ctx; + stm.out = out; + + /* set encoding parameters to default values */ + opj_set_default_encoder_parameters(¶meters); + + /* Decide if MCT should be used */ + /* mct = 1 -> rgb data should be converted to ycc */ + parameters.tcp_mct = (pix->n >= 3) ? 1 : 0; + + parameters.irreversible = 1; + + /* JPEG-2000 codestream */ + l_codec = opj_create_compress(OPJ_CODEC_J2K); + /* Use OPJ_CODEC_JP2 for JPEG 2000 compressed image data, but that requires seeking. */ + + /* catch events using our callbacks and give a local context */ + opj_set_info_handler(l_codec, info_callback, ctx); + opj_set_warning_handler(l_codec, warning_callback, ctx); + opj_set_error_handler(l_codec, error_callback, ctx); + + if (q == 100) + { + /* Lossless compression */ + } + else + { + /* 20:1 compression is reasonable */ + parameters.tcp_numlayers = 1; + parameters.tcp_rates[0] = (100-q); + parameters.cp_disto_alloc = 1; + } + + /* The compressor will refuse to encode if the following condition + * is true. We work around this by enabling tiling for such images. + * An alternative would be to reduce numresolution. Not sure which + * is better. */ + if (fz_mini(pix->w, pix->h) < 1<<(parameters.numresolution-1)) + { + parameters.cp_tx0 = 0; + parameters.cp_ty0 = 0; + parameters.tile_size_on = OPJ_TRUE; + parameters.cp_tdx = 1<<(parameters.numresolution-1); + parameters.cp_tdy = 1<<(parameters.numresolution-1); + } + + if (! opj_setup_encoder(l_codec, ¶meters, image)) + { + opj_destroy_codec(l_codec); + opj_image_destroy(image); + fz_throw(ctx, FZ_ERROR_GENERIC, "OpenJPEG encoder setup failed"); + } + + /* open a byte stream for writing and allocate memory for all tiles */ + l_stream = opj_stream_create(OPJ_J2K_STREAM_CHUNK_SIZE, OPJ_FALSE); + if (!l_stream) + { + opj_destroy_codec(l_codec); + opj_image_destroy(image); + fz_throw(ctx, FZ_ERROR_GENERIC, "OpenJPEG encoder setup failed (stream creation)"); + } + + opj_stream_set_user_data(l_stream, &stm, close_stm); + opj_stream_set_user_data_length(l_stream, 0); + //opj_stream_set_read_function(l_stream, opj_read_from_file); + opj_stream_set_write_function(l_stream, write_stm); + opj_stream_set_skip_function(l_stream, skip_stm); + opj_stream_set_seek_function(l_stream, seek_stm); + + /* encode the image */ + bSuccess = opj_start_compress(l_codec, image, l_stream); + if (!bSuccess) + { + opj_destroy_codec(l_codec); + opj_image_destroy(image); + fz_throw(ctx, FZ_ERROR_GENERIC, "OpenJPEG encode failed"); + } + + bSuccess = bSuccess && opj_encode(l_codec, l_stream); + bSuccess = bSuccess && opj_end_compress(l_codec, l_stream); + + opj_stream_destroy(l_stream); + + /* free remaining compression structures */ + opj_destroy_codec(l_codec); + + /* free image data */ + opj_image_destroy(image); + + if (!bSuccess) + fz_throw(ctx, FZ_ERROR_GENERIC, "Encoding failed"); + } + fz_always(ctx) + opj_unlock(ctx); + fz_catch(ctx) + fz_rethrow(ctx); +} + +void +fz_save_pixmap_as_jpx(fz_context *ctx, fz_pixmap *pixmap, const char *filename, int q) +{ + fz_output *out = fz_new_output_with_path(ctx, filename, 0); + fz_try(ctx) + { + fz_write_pixmap_as_jpx(ctx, out, pixmap, q); + fz_close_output(ctx, out); + } + fz_always(ctx) + { + fz_drop_output(ctx, out); + } + fz_catch(ctx) + { + fz_rethrow(ctx); + } +} + +static fz_buffer * +jpx_from_pixmap(fz_context *ctx, fz_pixmap *pix, fz_color_params color_params, int quality, int drop) +{ + fz_buffer *buf = NULL; + fz_output *out = NULL; + + fz_var(buf); + fz_var(out); + + fz_try(ctx) + { + buf = fz_new_buffer(ctx, 1024); + out = fz_new_output_with_buffer(ctx, buf); + fz_write_pixmap_as_jpx(ctx, out, pix, quality); + fz_close_output(ctx, out); + } + fz_always(ctx) + { + if (drop) + fz_drop_pixmap(ctx, pix); + fz_drop_output(ctx, out); + } + fz_catch(ctx) + { + fz_drop_buffer(ctx, buf); + fz_rethrow(ctx); + } + return buf; +} + +fz_buffer * +fz_new_buffer_from_image_as_jpx(fz_context *ctx, fz_image *image, fz_color_params color_params, int quality) +{ + fz_pixmap *pix = fz_get_pixmap_from_image(ctx, image, NULL, NULL, NULL, NULL); + return jpx_from_pixmap(ctx, pix, color_params, quality, 1); +} + +fz_buffer * +fz_new_buffer_from_pixmap_as_jpx(fz_context *ctx, fz_pixmap *pix, fz_color_params color_params, int quality) +{ + return jpx_from_pixmap(ctx, pix, color_params, quality, 0); +} diff --git a/source/pdf/pdf-image-rewriter.c b/source/pdf/pdf-image-rewriter.c index b625a955fc..66d2f29c9d 100644 --- a/source/pdf/pdf-image-rewriter.c +++ b/source/pdf/pdf-image-rewriter.c @@ -489,7 +489,37 @@ fz_recompress_image_as_jpeg(fz_context *ctx, fz_pixmap *pix, const char *quality static fz_compressed_buffer * fz_recompress_image_as_j2k(fz_context *ctx, fz_pixmap *pix, const char *quality) { - return NULL; + 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 *