Skip to content

Instantly share code, notes, and snippets.

@a4lg
Last active February 13, 2024 15:32
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save a4lg/4c81d2357d4365029a9f66635cd86bb6 to your computer and use it in GitHub Desktop.
Save a4lg/4c81d2357d4365029a9f66635cd86bb6 to your computer and use it in GitHub Desktop.
Lossless JPEG tiling program for non-optimized images
/*
tilejpeg : lossless JPEG tiling program for non-optimized images
tilejpeg.cpp
Tile JPEG Program
Copyright (C) 2016 Tsukasa OI <floss_tilejpeg@irq.a4lg.com>
Based on the program code on <http://apostata.web.fc2.com/tilejpeg/index.html>.
Permission to use, copy, modify, and/or distribute this software for
any purpose with or without fee is hereby granted, provided that the
above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#define TILEJPEG_PROGNAME "tilejpeg"
#define TILEJPEG_PROGVER "0.1"
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <new>
#include <jpeglib.h>
#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#else
#include <unistd.h>
#endif
enum TILEJPEG_MODE
{
TILEJPEG_TO_TILE,
TILEJPEG_ARGERR,
TILEJPEG_HELP,
TILEJPEG_VERSION,
};
enum TILEJPEG_STATUS
{
TILEJPEG_OTHER,
TILEJPEG_READING,
TILEJPEG_TILING,
TILEJPEG_WRITING,
};
static enum TILEJPEG_MODE prog_mode = TILEJPEG_ARGERR;
static enum TILEJPEG_STATUS prog_status = TILEJPEG_OTHER;
static const char* prog_name = TILEJPEG_PROGNAME;
static const char* prog_cur_filename = nullptr;
static bool is_trim_enabled = false;
static bool is_edge_trim_enabled = false;
static JDIMENSION trim_x = 0, trim_y = 0; // Trim to fixed size {X} by {Y} (before tiling image)
static JDIMENSION tile_x = 0, tile_y = 0; // Tile {X} by {Y} images
static const char* out_filename = nullptr;
// Print error message and exit (override with standard error_exit)
static void tilejpeg_exit_with_error(j_common_ptr cptr)
{
char msg[JMSG_LENGTH_MAX];
cptr->err->format_message(cptr, msg);
if (prog_cur_filename)
{
const char* msg_format = "%s: jpeglib raised an error while processing (%s)";
switch (prog_status)
{
case TILEJPEG_READING:
msg_format = "%s: jpeglib raised an error while reading (%s)";
break;
case TILEJPEG_TILING:
msg_format = "%s: jpeglib raised an error while making tiled image (%s)";
break;
case TILEJPEG_WRITING:
msg_format = "%s: jpeglib raised an error while writing (%s)";
break;
}
fprintf(stderr, msg_format, prog_cur_filename, msg);
}
else
{
const char* msg_format = "error: jpeglib raised an error while processing (%s)";
switch (prog_status)
{
case TILEJPEG_TILING:
msg_format = "error: jpeglib raised an error while making tiled image (%s)";
break;
case TILEJPEG_WRITING:
msg_format = "error: jpeglib raised an error while writing image (%s)";
break;
}
fprintf(stderr, msg_format, msg);
}
exit(1);
}
static bool parse_arg_dimension(const char* arg, JDIMENSION* x, JDIMENSION* y)
{
long tmp = 0;
char* strp = nullptr;
if (!*arg)
{
fprintf(stderr, "error: on dimension `%s': cannot be empty.\n", arg);
return false;
}
errno = 0;
tmp = strtol(arg, &strp, 10);
if (arg == strp)
{
fprintf(stderr, "error: on dimension `%s': cannot parse X dimension value.\n", arg);
return false;
}
if (tmp <= 0)
{
fprintf(stderr, "error: on dimension `%s': X dimension value must be positive.\n", arg);
return false;
}
if ((errno == ERANGE && tmp == std::numeric_limits<long>::max()) || tmp > JPEG_MAX_DIMENSION)
{
fprintf(stderr, "error: on dimension `%s': X dimension value is too large.\n", arg);
return false;
}
*x = static_cast<JDIMENSION>(tmp);
if (*strp++ != 'x')
{
fprintf(stderr, "error: on dimension `%s': must specified by the format {X}x{Y}.\n", arg);
return false;
}
if (!*strp)
{
fprintf(stderr, "error: on dimension `%s': Y dimension value must not be empty.\n", arg);
return false;
}
tmp = strtol(strp, &strp, 10);
if (*strp)
{
fprintf(stderr, "error: on dimension `%s': cannot parse Y dimension value.\n", arg);
return false;
}
if (tmp <= 0)
{
fprintf(stderr, "error: on dimension `%s': Y dimension value must be positive.\n", arg);
return false;
}
if ((errno == ERANGE && tmp == std::numeric_limits<long>::max()) || tmp > JPEG_MAX_DIMENSION)
{
fprintf(stderr, "error: on dimension `%s': Y dimension value is too large.\n", arg);
return false;
}
*y = static_cast<JDIMENSION>(tmp);
return true;
}
static void parse_args(int argc, char** argv, int* files_start)
{
prog_name = argv[0];
prog_mode = TILEJPEG_ARGERR;
int p = 1;
bool is_tile_given = false;
for (; p < argc; p++)
{
if (argv[p][0] == '-')
{
size_t l = strlen(argv[p]);
if (!strcmp(argv[p] + 1, "t")
|| !strcmp(argv[p] + 1, "-tile")
|| !strncmp(argv[p] + 1, "-tile=", 6))
{
// -t DIM | --tile DIM | --tile=DIM
const char* a = nullptr;
if (l == 2 || l == 6)
{
if (++p == argc)
{
fprintf(stderr, "error: `%s' option requires dimension.\n", argv[p - 1]);
return;
}
a = argv[p];
}
else
{
a = argv[p] + 6 + 1;
}
if (!parse_arg_dimension(a, &tile_x, &tile_y))
return;
is_tile_given = true;
}
else if (!strcmp(argv[p] + 1, "T")
|| !strcmp(argv[p] + 1, "-trim")
|| !strncmp(argv[p] + 1, "-trim=", 6))
{
// -T DIM | --trim DIM | --trim=DIM
const char* a = nullptr;
if (l == 2 || l == 6)
{
if (++p == argc)
{
fprintf(stderr, "error: `%s' option requires dimension.\n", argv[p - 1]);
return;
}
a = argv[p];
}
else
{
a = argv[p] + 6 + 1;
}
if (!parse_arg_dimension(a, &trim_x, &trim_y))
return;
is_trim_enabled = true;
}
else if (!strcmp(argv[p] + 1, "o")
|| !strcmp(argv[p] + 1, "-output")
|| !strncmp(argv[p] + 1, "-output=", 8))
{
// -o FILENAME | --output FILENAME | --output=FILENAME
const char* a = nullptr;
if (l == 2 || l == 8)
{
if (++p == argc)
{
fprintf(stderr, "error: `%s' option requires file name.\n", argv[p - 1]);
return;
}
a = argv[p];
}
else
{
a = argv[p] + 8 + 1;
}
out_filename = a;
}
else if (!strcmp(argv[p] + 1, "h") || !strcmp(argv[p] + 1, "-help"))
{
// -h | --help
prog_mode = TILEJPEG_HELP;
return;
}
else if (!strcmp(argv[p] + 1, "v") || !strcmp(argv[p] + 1, "-version"))
{
// -v | --version
prog_mode = TILEJPEG_VERSION;
return;
}
else if (!strcmp(argv[p] + 1, "e") || !strcmp(argv[p] + 1, "-trim-edges"))
{
// -e | --trim-edges
is_edge_trim_enabled = true;
}
else if (!strcmp(argv[p] + 1, "E") || !strcmp(argv[p] + 1, "-no-trim-edges"))
{
// -E | --no-trim-edges
is_edge_trim_enabled = false;
}
else if (!strcmp(argv[p] + 1, "-"))
{
// --
++p;
break;
}
else
{
fprintf(stderr, "error: unrecognized option `%s'.\n", argv[p]);
return;
}
}
else
{
break;
}
}
*files_start = p;
if (!is_tile_given)
{
fprintf(stderr,
"error: tile (-t|--tile) option must be specified.\n"
" use -h to show help.\n"
);
return;
}
prog_mode = TILEJPEG_TO_TILE;
}
int main(int argc, char** argv)
{
jpeg_error_mgr errmgr = {};
/*
Parse arguments
*/
unsigned nfiles = 0; // number of files to be given
int files_start = 0; // argv[files_start] is expected to be the first file argument
prog_status = TILEJPEG_OTHER;
{
parse_args(argc, argv, &files_start);
if (prog_mode == TILEJPEG_TO_TILE)
{
// check extra consistencies
assert(tile_x != 0);
assert(tile_y != 0);
if (tile_x > std::numeric_limits<int>::max()
|| tile_y > std::numeric_limits<int>::max()
|| std::numeric_limits<int>::max() / tile_x < tile_y)
{
// error if tile_x*tile_y overflows
fprintf(stderr, "error: there's too many files to be given from command line arguments.\n");
prog_mode = TILEJPEG_ARGERR;
}
else if ((argc - files_start) != static_cast<int>(tile_x * tile_y))
{
// error if too few or too many files are given
fprintf(stderr, "error: number of files did not match.\n");
prog_mode = TILEJPEG_ARGERR;
}
}
switch (prog_mode)
{
case TILEJPEG_TO_TILE:
break;
case TILEJPEG_HELP:
fprintf(
stderr,
"usage: %s\n [--output FILE] --tile {X}x{Y} [--trim {X}x{Y}] FILES...\n"
" -o|--output FILE write the output to FILE (defaults to stdout)\n"
" -t|--tile {X}x{Y} tile X files horizontally and Y files vertically\n"
" -T|--trim {X}x{Y} trim images to {X}x{Y} size first\n"
" -e|--trim-edges enable trimming edges for rightmost or bottom tiles\n"
" -E|--no-trim-edges disable trimming edges for rightmost or\n"
" bottom tiles (default)\n",
prog_name
);
return 0;
case TILEJPEG_VERSION:
fprintf(
stderr,
TILEJPEG_PROGNAME " " TILEJPEG_PROGVER " by Tsukasa OI.\n"
" Based on `TileJpeg' program code on <http://apostata.web.fc2.com/tilejpeg/index.html>.\n"
);
return 0;
case TILEJPEG_ARGERR: // fall-through
default:
return 1;
}
nfiles = static_cast<unsigned>(tile_x * tile_y);
}
// Please note that this program performs tiling only once (per process).
// So minor memory leaks are ignored because they are completely harmless.
/*
Read JPEG files to tile
*/
jpeg_decompress_struct* in_images = new(std::nothrow) jpeg_decompress_struct[nfiles];
jvirt_barray_ptr** in_coeffs = new(std::nothrow) jvirt_barray_ptr*[nfiles];
if (!in_images || !in_coeffs)
{
perror(prog_name);
return 1;
}
prog_status = TILEJPEG_READING;
{
bool quant_tbls_used[NUM_QUANT_TBLS] = {};
jpeg_decompress_struct* in_images_s = in_images;
jvirt_barray_ptr** in_coeffs_s = in_coeffs;
char** files = argv + files_start;
for (JDIMENSION y = 0; y < tile_y; y++)
{
for (JDIMENSION x = 0; x < tile_x; x++, in_images_s++, in_coeffs_s++)
{
// read JPEG file
jpeg_decompress_struct* in_image = in_images_s;
prog_cur_filename = *files++;
FILE* fp = fopen(prog_cur_filename, "rb");
if (!fp)
{
perror(prog_cur_filename);
return 1;
}
in_image->err = jpeg_std_error(&errmgr);
in_image->err->error_exit = tilejpeg_exit_with_error;
jpeg_create_decompress(in_image);
jpeg_stdio_src(in_image, fp);
jpeg_read_header(in_image, TRUE);
*in_coeffs_s = jpeg_read_coefficients(in_image);
jpeg_stdio_src(in_image, nullptr);
fclose(fp);
// check image
if (in_image->arith_code)
{
fprintf(stderr, "%s: arithmetic coded JPEG file is not supported.\n", prog_cur_filename);
return 1;
}
if (in_image->scale_num != in_image->scale_denom)
{
fprintf(stderr, "%s: JPEG file with DCT scaling is not supported.\n", prog_cur_filename);
return 1;
}
if (in_image->scale_num != in_image->scale_denom)
{
fprintf(stderr, "%s: JPEG file with DCT scaling is not supported.\n", prog_cur_filename);
return 1;
}
if (in_image->jpeg_color_space == JCS_UNKNOWN)
{
fprintf(stderr, "%s: unknown JPEG colorspace.\n", prog_cur_filename);
return 1;
}
// check that all images have the same color definitions and quantization tables
if (in_image == in_images)
{
// first image
for (int i = 0; i < in_image->num_components; i++)
quant_tbls_used[in_image->comp_info[i].quant_tbl_no] = true;
if (is_trim_enabled)
{
if (trim_x % static_cast<JDIMENSION>(in_image->max_h_samp_factor * 8) != 0)
{
fprintf(stderr, "%s: trim width is not a multiple of MCU width.\n", prog_cur_filename);
return 1;
}
if (trim_y % static_cast<JDIMENSION>(in_image->max_v_samp_factor * 8) != 0)
{
fprintf(stderr, "%s: trim height is not a multiple of MCU width.\n", prog_cur_filename);
return 1;
}
}
}
else
{
// second or later images
jpeg_decompress_struct* image_0 = in_images + 0;
if (in_image->num_components != image_0->num_components)
{
fprintf(stderr, "%s: image didn't have the same number of color components.\n", prog_cur_filename);
return 1;
}
if (in_image->jpeg_color_space != image_0->jpeg_color_space)
{
fprintf(stderr, "%s: image didn't have the same color space.\n", prog_cur_filename);
return 1;
}
for (int i = 0; i < image_0->num_components; i++)
{
// Note: although it could rearrange color components, it doesn't.
if (in_image->comp_info[i].component_id != image_0->comp_info[i].component_id
|| in_image->comp_info[i].h_samp_factor != image_0->comp_info[i].h_samp_factor
|| in_image->comp_info[i].v_samp_factor != image_0->comp_info[i].v_samp_factor
|| in_image->comp_info[i].quant_tbl_no != image_0->comp_info[i].quant_tbl_no)
{
fprintf(stderr, "%s: image didn't have the same color component definition at index %d.\n",
prog_cur_filename, i);
return 1;
}
}
for (int i = 0; i < NUM_QUANT_TBLS; i++)
{
// Note: although it could rearrange quantization tables, it doesn't.
if (!quant_tbls_used[i])
continue;
if (memcmp(in_image->quant_tbl_ptrs[i]->quantval, image_0->quant_tbl_ptrs[i]->quantval,
sizeof(in_image->quant_tbl_ptrs[i]->quantval)))
{
fprintf(stderr, "%s: image didn't have the same quantization table at index %d.\n",
prog_cur_filename, i);
return 1;
}
}
}
if (is_trim_enabled)
{
// check trimmed images
if ((x != tile_x - 1 || is_edge_trim_enabled) && trim_x > in_image->image_width)
{
fprintf(stderr, "%s: image width is too small to trim.\n", prog_cur_filename);
return 1;
}
if ((y != tile_y - 1 || is_edge_trim_enabled) && trim_y > in_image->image_height)
{
fprintf(stderr, "%s: image height is too small to trim.\n", prog_cur_filename);
return 1;
}
}
else
{
// check non-trimmed non-edge images
if (x != tile_x - 1 && in_image->image_width % static_cast<JDIMENSION>(in_image->max_h_samp_factor * 8) != 0)
{
fprintf(stderr, "%s: image width is not a multiple of MCU width.\n", prog_cur_filename);
return 1;
}
if (y != tile_y - 1 && in_image->image_height % static_cast<JDIMENSION>(in_image->max_v_samp_factor * 8) != 0)
{
fprintf(stderr, "%s: image height is not a multiple of MCU width.\n", prog_cur_filename);
return 1;
}
}
// check non-leftmost images
if (x != 0)
{
jpeg_decompress_struct* image_l = in_image - 1;
if ((!is_trim_enabled || (!is_edge_trim_enabled && y == tile_y - 1))
&& in_image->image_height != image_l->image_height)
{
fprintf(stderr, "%s: image in the same row didn't have the same height.\n", prog_cur_filename);
return 1;
}
}
// check non-topmost images
if (y != 0)
{
jpeg_decompress_struct* image_t = in_image - tile_x;
if ((!is_trim_enabled || (!is_edge_trim_enabled && x == tile_x - 1))
&& in_image->image_width != image_t->image_width)
{
fprintf(stderr, "%s: image in the same column didn't have the same width.\n", prog_cur_filename);
return 1;
}
}
}
}
}
/*
Tile JPEG images
*/
prog_cur_filename = out_filename;
prog_status = TILEJPEG_TILING;
jvirt_barray_ptr* out_coeffs = new(std::nothrow) jvirt_barray_ptr[in_images[0].num_components];
if (!out_coeffs)
{
perror(prog_name);
return 1;
}
jpeg_compress_struct out_image;
out_image.err = jpeg_std_error(&errmgr);
out_image.err->error_exit = tilejpeg_exit_with_error;
jpeg_create_compress(&out_image);
{
// compute output image size
JDIMENSION out_x = 0, out_y = 0;
for (unsigned x = 0; x < tile_x; x++)
{
JDIMENSION w = (!is_trim_enabled || (!is_edge_trim_enabled && x == tile_x - 1))
? in_images[x].image_width : trim_x;
if (JPEG_MAX_DIMENSION - out_x < w)
{
fprintf(stderr, "error: output image width is too large.\n");
return 1;
}
out_x += w;
}
for (unsigned y = 0; y < tile_y; y++)
{
JDIMENSION h = (!is_trim_enabled || (!is_edge_trim_enabled && y == tile_y - 1))
? in_images[y * tile_x].image_height : trim_y;
if (JPEG_MAX_DIMENSION - out_y < h)
{
fprintf(stderr, "error: output image height is too large.\n");
return 1;
}
out_y += h;
}
// create output JPEG image (with output parameters)
jpeg_copy_critical_parameters(in_images + 0, &out_image);
out_image.image_width = out_x;
out_image.image_height = out_y;
/*
Change here to 0 if you see compile errors
*/
#if 1
out_image.jpeg_width = out_x;
out_image.jpeg_height = out_y;
#endif
for (int i = 0; i < out_image.num_components; i++)
{
// compute memory-based size
JDIMENSION mem_x = 0, mem_y = 0;
for (unsigned x = 0; x < tile_x; x++)
{
mem_x += (!is_trim_enabled || (!is_edge_trim_enabled && x == tile_x - 1))
? in_images[x].comp_info[i].width_in_blocks
: trim_x / DCTSIZE / in_images[0].max_h_samp_factor
* in_images[0].comp_info[i].h_samp_factor;
}
for (unsigned y = 0; y < tile_y; y++)
{
mem_y += (!is_trim_enabled || (!is_edge_trim_enabled && y == tile_y - 1))
? in_images[y * tile_x].comp_info[i].height_in_blocks
: trim_y / DCTSIZE / in_images[0].max_v_samp_factor
* in_images[0].comp_info[i].v_samp_factor;
}
while (mem_x % in_images[0].max_h_samp_factor)
mem_x++;
while (mem_y % in_images[0].max_v_samp_factor)
mem_y++;
// allocate and then realize virtual buffer
out_coeffs[i] = out_image.mem->request_virt_barray(
(j_common_ptr)&out_image,
JPOOL_IMAGE, TRUE, mem_x, mem_y,
in_images[0].comp_info[i].v_samp_factor
);
out_image.mem->realize_virt_arrays((j_common_ptr)&out_image);
// copy images
JDIMENSION yd = 0;
for (unsigned y = 0; y < tile_y; y++)
{
JDIMENSION blk_y = (!is_trim_enabled || (!is_edge_trim_enabled && y == tile_y - 1))
? in_images[y * tile_x].comp_info[i].height_in_blocks
: trim_y / DCTSIZE / in_images[0].max_v_samp_factor
* in_images[0].comp_info[i].v_samp_factor;
for (JDIMENSION yy = 0; yy < blk_y; yy++)
{
JBLOCKROW dstrow = out_image.mem->access_virt_barray((j_common_ptr)&out_image, out_coeffs[i], yd++, 1, TRUE)[0];
for (unsigned x = 0; x < tile_x; x++)
{
JBLOCKROW srcrow = in_images[x + y * tile_x].mem->access_virt_barray(
(j_common_ptr)(in_images + x + y * tile_x),
in_coeffs[x + y * tile_x][i],
yy, 1, FALSE
)[0];
JDIMENSION blk_x = (!is_trim_enabled || (!is_edge_trim_enabled && x == tile_x - 1))
? in_images[x].comp_info[i].width_in_blocks
: trim_x / DCTSIZE / in_images[0].max_h_samp_factor
* in_images[0].comp_info[i].h_samp_factor;
memcpy(dstrow, srcrow, blk_x * sizeof(srcrow[0]));
dstrow += blk_x;
}
}
}
}
}
/*
Write output JPEG image
*/
prog_cur_filename = out_filename;
prog_status = TILEJPEG_WRITING;
FILE* fp = nullptr;
if (out_filename)
fp = fopen(out_filename, "wb");
else
{
#ifdef _WIN32
fp = stdout;
if (_setmode(_fileno(stdout), _O_BINARY) == -1)
{
perror("stdout");
return 1;
}
#else
int fd = dup(fileno(stdout));
if (fd == -1)
{
perror("stdout");
return 1;
}
fp = fdopen(fd, "wb");
#endif
}
if (!fp)
{
if (out_filename)
perror(out_filename);
else
perror(prog_name);
return 1;
}
jpeg_stdio_dest(&out_image, fp);
jpeg_write_coefficients(&out_image, out_coeffs);
jpeg_finish_compress(&out_image);
fclose(fp);
return 0;
}
@mildsunrise
Copy link

mildsunrise commented Mar 31, 2023

thank you so much, this is just what I was looking for 💙 worked flawlessly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment