Created
December 31, 2023 03:37
-
-
Save nico/86fbc9803c4b900a68b7c07d7804ee92 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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