Skip to content

Instantly share code, notes, and snippets.

@nico
Created December 31, 2023 03:37
Show Gist options
  • Save nico/86fbc9803c4b900a68b7c07d7804ee92 to your computer and use it in GitHub Desktop.
Save nico/86fbc9803c4b900a68b7c07d7804ee92 to your computer and use it in GitHub Desktop.
// A short demo program that uses libjpeg to write cmyk/ycck jpeg files.
// It's possible to write ycck jpegs using ImageMagick's convert and Photoshop's Save As.
// I haven't found a good way to write subsampled true cmyk jpegs using any of cjpeg, convert, Photoshop, or GIMP though.
// (GIMP 2.99.16 has in-progress cmyk saving support, and by picking 4:2:0 subsampling it writes a 2111 CMYK file.
// But 2111 CMYK files don't make any sense, and 4:2:0 is a YCC term and applying it to CMYK is fairly silly.
// Anyways, I wasn't able to create a 2112 CMYK file, which is what is sometimes seen in pratice -- not that
// having C have more resolution than MY makes a ton of sense either.)
//
// compile with:
// clang++ -std=c++11 write_cmyk_ycck_jpegs.cc -I ~/Downloads/libjpeg-turbo-3.0.1 -I ~/Downloads/libjpeg-turbo-3.0.1/build -ljpeg -L ~/Downloads/libjpeg-turbo-3.0.1/build
// run with:
// DYLD_LIBRARY_PATH=$HOME/Downloads/libjpeg-turbo-3.0.1/build ./a.out
#include <stdio.h>
#include <stdlib.h>
#include <vector>
extern "C" {
#include "jpeglib.h"
}
enum class Encoding {
CMYK,
YCCK,
};
enum class WriteAdobeMarker {
No,
Yes,
};
enum class Subsampling {
None,
TwoOneOneOne,
TwoOneOneTwo,
};
// toggles:
// - cmyk vs ycck
// - adobe marker (required for ycck)
// - subsampling
// - channel ids 1 2 3 4 vs 'C' 'M' 'Y' 'K'
void write_cmyk_jpeg(char const* filename, int w, int h, const unsigned char* in_data,
Encoding encoding, WriteAdobeMarker write_adobe_marker, Subsampling subsampling) {
jpeg_compress_struct cinfo;
jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
FILE* outfile;
if ((outfile = fopen(filename, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", filename);
exit(1);
}
jpeg_stdio_dest(&cinfo, outfile);
cinfo.image_width = w;
cinfo.image_height = h;
cinfo.input_components = 4;
cinfo.in_color_space = JCS_CMYK;
jpeg_set_defaults(&cinfo);
cinfo.jpeg_color_space = encoding == Encoding::CMYK ? JCS_CMYK : JCS_YCCK;
cinfo.write_Adobe_marker = write_adobe_marker == WriteAdobeMarker::Yes;
// jpeglib picks 'C' 'M' 'Y' 'K' as channel ids by default for CMYK but 1 2 3 4 for YCCK.
// Let's always write 1 2 3 4.
for (int i = 0; i < 4; ++i)
cinfo.comp_info[i].component_id = i + 1;
switch (subsampling) {
case Subsampling::None:
// Nothing to do.
break;
case Subsampling::TwoOneOneOne:
cinfo.comp_info[0].h_samp_factor = 2;
cinfo.comp_info[0].v_samp_factor = 2;
break;
case Subsampling::TwoOneOneTwo:
cinfo.comp_info[0].h_samp_factor = 2;
cinfo.comp_info[0].v_samp_factor = 2;
cinfo.comp_info[3].h_samp_factor = 2;
cinfo.comp_info[3].v_samp_factor = 2;
break;
}
jpeg_set_quality(&cinfo, 75, TRUE /* limit to baseline-JPEG values */);
jpeg_start_compress(&cinfo, TRUE);
std::vector<unsigned char> data(w * h * 4);
memcpy(data.data(), in_data, data.size());
// Serenity, and as far as I can tell the spec, only wants the channels inverted
// if an Adobe marker is present.
// However, Chrome and Firefox show the images as black if the non-Adobe marker
// images aren't inverted. (Safari is confused and has sampling-factor dependent behavior
// if the Adobe marker is missing.)
// Chrome does this in https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/image-decoders/jpeg/jpeg_image_decoder.cc;l=1061?q=jcs_ycck%20file:blink
// It doesn't check for JPEG_APP14, which is arguably a bug in Chrome.
// I suppose the takeaway is that CMYK images without Adobe marker don't produce consistent results.
// (...but always inverting seems possibly "more compatible" in practice given Chrome and Firefox expect that.)
// Anyway, we do what I think is spec-compliant.
if (write_adobe_marker == WriteAdobeMarker::Yes)
for (unsigned char& b : data)
b = ~b;
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row = &data[cinfo.next_scanline * w * 4];
jpeg_write_scanlines(&cinfo, &row, 1);
}
jpeg_finish_compress(&cinfo);
fclose(outfile);
jpeg_destroy_compress(&cinfo);
}
int main() {
const int W = 400;
const int H = 300;
unsigned char data[W * H * 4];
for (int y = 0; y < H; ++y) {
for (int x = 0; x < W; ++x) {
data[(y * W + x) * 4 + 0] = x * 255 / W;
data[(y * W + x) * 4 + 1] = y * 255 / H;
data[(y * W + x) * 4 + 2] = 0;
data[(y * W + x) * 4 + 3] = 0;
}
}
for (Encoding encoding : { Encoding::CMYK, Encoding::YCCK }) {
for (WriteAdobeMarker write_adobe_marker : { WriteAdobeMarker::No, WriteAdobeMarker::Yes }) {
if (encoding == Encoding::YCCK && write_adobe_marker == WriteAdobeMarker::No)
continue;
for (Subsampling subsampling : { Subsampling::None, Subsampling::TwoOneOneOne, Subsampling::TwoOneOneTwo }) {
char name[80];
snprintf(name, sizeof(name), "test-%s-%s-%s.jpg",
encoding == Encoding::CMYK ? "cmyk" : "ycck",
write_adobe_marker == WriteAdobeMarker::Yes ? "adobe" : "no_adobe",
subsampling == Subsampling::None ? "1111" : subsampling == Subsampling::TwoOneOneOne ? "2111" : "2112");
write_cmyk_jpeg(name, W, H, data, encoding, write_adobe_marker, subsampling);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment