#include "mupdf/fitz.h" #include "icc34.h" #include #define SAVEICCPROFILE 0 #define ICC_HEADER_SIZE 128 #define ICC_TAG_SIZE 12 #define ICC_NUMBER_COMMON_TAGS 2 #define ICC_XYZPT_SIZE 12 #define ICC_DATATYPE_SIZE 8 #define D50_X 0.9642f #define D50_Y 1.0f #define D50_Z 0.8249f static const char copy_right[] = "Copyright Artifex Software 2020"; #if SAVEICCPROFILE unsigned int icc_debug_index = 0; #endif typedef struct { icTagSignature sig; icUInt32Number offset; icUInt32Number size; unsigned char byte_padding; } fz_icc_tag; #if SAVEICCPROFILE static void save_profile(fz_context *ctx, fz_buffer *buf, const char *name) { char full_file_name[50]; fz_snprintf(full_file_name, sizeof full_file_name, "profile%d-%s.icc", icc_debug_index, name); fz_save_buffer(ctx, buf, full_file_name); icc_debug_index++; } #endif static void fz_append_byte_n(fz_context *ctx, fz_buffer *buf, int c, int n) { int k; for (k = 0; k < n; k++) fz_append_byte(ctx, buf, c); } static int get_padding(int x) { return (4 - x % 4) % 4; } static void setdatetime(fz_context *ctx, icDateTimeNumber *datetime) { datetime->day = 0; datetime->hours = 0; datetime->minutes = 0; datetime->month = 0; datetime->seconds = 0; datetime->year = 0; } static void add_gammadata(fz_context *ctx, fz_buffer *buf, unsigned short gamma, icTagTypeSignature curveType) { fz_append_int32_be(ctx, buf, curveType); fz_append_byte_n(ctx, buf, 0, 4); /* one entry for gamma */ fz_append_int32_be(ctx, buf, 1); /* The encode (8frac8) gamma, with padding */ fz_append_int16_be(ctx, buf, gamma); /* pad two bytes */ fz_append_byte_n(ctx, buf, 0, 2); } static unsigned short float2u8Fixed8(fz_context *ctx, float number_in) { return (unsigned short)(number_in * 256); } static void add_xyzdata(fz_context *ctx, fz_buffer *buf, icS15Fixed16Number temp_XYZ[]) { int j; fz_append_int32_be(ctx, buf, icSigXYZType); fz_append_byte_n(ctx, buf, 0, 4); for (j = 0; j < 3; j++) fz_append_int32_be(ctx, buf, temp_XYZ[j]); } static icS15Fixed16Number double2XYZtype(fz_context *ctx, float number_in) { short s; unsigned short m; if (number_in < 0) number_in = 0; s = (short)number_in; m = (unsigned short)((number_in - s) * 65536); return (icS15Fixed16Number) ((s << 16) | m); } static void get_D50(fz_context *ctx, icS15Fixed16Number XYZ[]) { XYZ[0] = double2XYZtype(ctx, D50_X); XYZ[1] = double2XYZtype(ctx, D50_Y); XYZ[2] = double2XYZtype(ctx, D50_Z); } static void get_XYZ_doubletr(fz_context *ctx, icS15Fixed16Number XYZ[], float vector[]) { XYZ[0] = double2XYZtype(ctx, vector[0]); XYZ[1] = double2XYZtype(ctx, vector[1]); XYZ[2] = double2XYZtype(ctx, vector[2]); } static void add_desc_tag(fz_context *ctx, fz_buffer *buf, const char text[], fz_icc_tag tag_list[], int curr_tag) { size_t len = strlen(text); fz_append_int32_be(ctx, buf, icSigTextDescriptionType); fz_append_byte_n(ctx, buf, 0, 4); fz_append_int32_be(ctx, buf, (int)len + 1); fz_append_string(ctx, buf, text); /* 1 + 4 + 4 + 2 + 1 + 67 */ fz_append_byte_n(ctx, buf, 0, 79); fz_append_byte_n(ctx, buf, 0, tag_list[curr_tag].byte_padding); } static void add_text_tag(fz_context *ctx, fz_buffer *buf, const char text[], fz_icc_tag tag_list[], int curr_tag) { fz_append_int32_be(ctx, buf, icSigTextType); fz_append_byte_n(ctx, buf, 0, 4); fz_append_string(ctx, buf, text); fz_append_byte(ctx, buf, 0); fz_append_byte_n(ctx, buf, 0, tag_list[curr_tag].byte_padding); } static void add_common_tag_data(fz_context *ctx, fz_buffer *buf, fz_icc_tag tag_list[], const char *desc_name) { add_desc_tag(ctx, buf, desc_name, tag_list, 0); add_text_tag(ctx, buf, copy_right, tag_list, 1); } static void init_common_tags(fz_context *ctx, fz_icc_tag tag_list[], int num_tags, int *last_tag, const char *desc_name) { int curr_tag, temp_size; if (*last_tag < 0) curr_tag = 0; else curr_tag = (*last_tag) + 1; tag_list[curr_tag].offset = ICC_HEADER_SIZE + num_tags * ICC_TAG_SIZE + 4; tag_list[curr_tag].sig = icSigProfileDescriptionTag; /* temp_size = DATATYPE_SIZE + 4 (zeros) + 4 (len) + strlen(desc_name) + 1 (null) + 4 + 4 + 2 + 1 + 67 + bytepad; */ temp_size = (int)strlen(desc_name) + 91; tag_list[curr_tag].byte_padding = get_padding(temp_size); tag_list[curr_tag].size = temp_size + tag_list[curr_tag].byte_padding; curr_tag++; tag_list[curr_tag].offset = tag_list[curr_tag - 1].offset + tag_list[curr_tag - 1].size; tag_list[curr_tag].sig = icSigCopyrightTag; /* temp_size = DATATYPE_SIZE + 4 (zeros) + strlen(copy_right) + 1 (null); */ temp_size = (int)strlen(copy_right) + 9; tag_list[curr_tag].byte_padding = get_padding(temp_size); tag_list[curr_tag].size = temp_size + tag_list[curr_tag].byte_padding; *last_tag = curr_tag; } static void copy_header(fz_context *ctx, fz_buffer *buffer, icHeader *header) { fz_append_int32_be(ctx, buffer, header->size); fz_append_byte_n(ctx, buffer, 0, 4); fz_append_int32_be(ctx, buffer, header->version); fz_append_int32_be(ctx, buffer, header->deviceClass); fz_append_int32_be(ctx, buffer, header->colorSpace); fz_append_int32_be(ctx, buffer, header->pcs); fz_append_byte_n(ctx, buffer, 0, 12); fz_append_int32_be(ctx, buffer, header->magic); fz_append_int32_be(ctx, buffer, header->platform); fz_append_byte_n(ctx, buffer, 0, 24); fz_append_int32_be(ctx, buffer, header->illuminant.X); fz_append_int32_be(ctx, buffer, header->illuminant.Y); fz_append_int32_be(ctx, buffer, header->illuminant.Z); fz_append_byte_n(ctx, buffer, 0, 48); } static void setheader_common(fz_context *ctx, icHeader *header) { header->cmmId = 0; header->version = 0x02200000; setdatetime(ctx, &(header->date)); header->magic = icMagicNumber; header->platform = icSigMacintosh; header->flags = 0; header->manufacturer = 0; header->model = 0; header->attributes[0] = 0; header->attributes[1] = 0; header->renderingIntent = 3; header->illuminant.X = double2XYZtype(ctx, (float) 0.9642); header->illuminant.Y = double2XYZtype(ctx, (float) 1.0); header->illuminant.Z = double2XYZtype(ctx, (float) 0.8249); header->creator = 0; memset(header->reserved, 0, 44); } static void copy_tagtable(fz_context *ctx, fz_buffer *buf, fz_icc_tag *tag_list, int num_tags) { int k; fz_append_int32_be(ctx, buf, num_tags); for (k = 0; k < num_tags; k++) { fz_append_int32_be(ctx, buf, tag_list[k].sig); fz_append_int32_be(ctx, buf, tag_list[k].offset); fz_append_int32_be(ctx, buf, tag_list[k].size); } } static void init_tag(fz_context *ctx, fz_icc_tag tag_list[], int *last_tag, icTagSignature tagsig, int datasize) { int curr_tag = (*last_tag) + 1; tag_list[curr_tag].offset = tag_list[curr_tag - 1].offset + tag_list[curr_tag - 1].size; tag_list[curr_tag].sig = tagsig; tag_list[curr_tag].byte_padding = get_padding(ICC_DATATYPE_SIZE + datasize); tag_list[curr_tag].size = ICC_DATATYPE_SIZE + datasize + tag_list[curr_tag].byte_padding; *last_tag = curr_tag; } static void matrixmult(fz_context *ctx, float leftmatrix[], int nlrow, int nlcol, float rightmatrix[], int nrrow, int nrcol, float result[]) { float *curr_row; int k, l, j, ncols, nrows; float sum; nrows = nlrow; ncols = nrcol; if (nlcol == nrrow) { for (k = 0; k < nrows; k++) { curr_row = &(leftmatrix[k*nlcol]); for (l = 0; l < ncols; l++) { sum = 0.0; for (j = 0; j < nlcol; j++) sum = sum + curr_row[j] * rightmatrix[j*nrcol + l]; result[k*ncols + l] = sum; } } } } static void apply_adaption(fz_context *ctx, float matrix[], float in[], float out[]) { out[0] = matrix[0] * in[0] + matrix[1] * in[1] + matrix[2] * in[2]; out[1] = matrix[3] * in[0] + matrix[4] * in[1] + matrix[5] * in[2]; out[2] = matrix[6] * in[0] + matrix[7] * in[1] + matrix[8] * in[2]; } /* Compute the CAT02 transformation to get us from the Cal White point to the D50 white point */ static void gsicc_create_compute_cam(fz_context *ctx, float white_src[], float *cam) { float cat02matrix[] = { 0.7328f, 0.4296f, -0.1624f, -0.7036f, 1.6975f, 0.0061f, 0.003f, 0.0136f, 0.9834f }; float cat02matrixinv[] = { 1.0961f, -0.2789f, 0.1827f, 0.4544f, 0.4735f, 0.0721f, -0.0096f, -0.0057f, 1.0153f }; float vonkries_diag[9]; float temp_matrix[9]; float lms_wp_src[3], lms_wp_des[3]; int k; float d50[3] = { D50_X, D50_Y, D50_Z }; matrixmult(ctx, cat02matrix, 3, 3, white_src, 3, 1, lms_wp_src); matrixmult(ctx, cat02matrix, 3, 3, d50, 3, 1, lms_wp_des); memset(&(vonkries_diag[0]), 0, sizeof(float) * 9); for (k = 0; k < 3; k++) { if (lms_wp_src[k] > 0) vonkries_diag[k * 3 + k] = lms_wp_des[k] / lms_wp_src[k]; else vonkries_diag[k * 3 + k] = 1; } matrixmult(ctx, &(vonkries_diag[0]), 3, 3, cat02matrix, 3, 3, temp_matrix); matrixmult(ctx, &(cat02matrixinv[0]), 3, 3, temp_matrix, 3, 3, cam); } /* The algorithm used by the following few routines comes from * LCMS2. We use this for converting the XYZ primaries + whitepoint * supplied to fz_new_icc_data_from_cal into the correct form for * writing into the profile. * * Prior to this code, we were, in some cases (such as with * the color data from pal8v4.bmp) ending up with a profile where * the whitepoint did not match properly. */ // Determinant lower than that are assumed zero (used on matrix invert) #define MATRIX_DET_TOLERANCE 0.0001 static int matrix_invert(double *out, const double *in) { double det, c0, c1, c2, absdet; c0 = in[4]*in[8] - in[5]*in[7]; c1 = -in[3]*in[8] + in[5]*in[6]; c2 = in[3]*in[7] - in[4]*in[6]; det = in[0]*c0 + in[1]*c1 + in[2]*c2; absdet = det; if (absdet < 0) absdet = -absdet; if (absdet < MATRIX_DET_TOLERANCE) return 1; // singular matrix; can't invert out[0] = c0/det; out[1] = (in[2]*in[7] - in[1]*in[8])/det; out[2] = (in[1]*in[5] - in[2]*in[4])/det; out[3] = c1/det; out[4] = (in[0]*in[8] - in[2]*in[6])/det; out[5] = (in[2]*in[3] - in[0]*in[5])/det; out[6] = c2/det; out[7] = (in[1]*in[6] - in[0]*in[7])/det; out[8] = (in[0]*in[4] - in[1]*in[3])/det; return 0; } static void transform_vector(double *out, const double *mat, const double *v) { out[0] = mat[0]*v[0] + mat[1]*v[1] + mat[2]*v[2]; out[1] = mat[3]*v[0] + mat[4]*v[1] + mat[5]*v[2]; out[2] = mat[6]*v[0] + mat[7]*v[1] + mat[8]*v[2]; } static void matrix_compose(double *out, const double *a, const double *b) { out[0] = a[0]*b[0] + a[1]*b[3] + a[2]*b[6]; out[1] = a[0]*b[1] + a[1]*b[4] + a[2]*b[7]; out[2] = a[0]*b[2] + a[1]*b[5] + a[2]*b[8]; out[3] = a[3]*b[0] + a[4]*b[3] + a[5]*b[6]; out[4] = a[3]*b[1] + a[4]*b[4] + a[5]*b[7]; out[5] = a[3]*b[2] + a[4]*b[5] + a[5]*b[8]; out[6] = a[6]*b[0] + a[7]*b[3] + a[8]*b[6]; out[7] = a[6]*b[1] + a[7]*b[4] + a[8]*b[7]; out[8] = a[6]*b[2] + a[7]*b[5] + a[8]*b[8]; } static int adaptation_matrix(double *out, const double *fromXYZ, const double *toXYZ) { // Bradford matrix static const double LamRigg[9] = { 0.8951, 0.2664, -0.1614, -0.7502, 1.7135, 0.0367, 0.0389, -0.0685, 1.0296 }; double chad_inv[9]; double fromRGB[3]; double toRGB[3]; double cone[9]; double tmp[9]; /* This should never fail, because it's the same operation * every time! Could save the work here... */ if (matrix_invert(chad_inv, LamRigg)) return 1; transform_vector(fromRGB, LamRigg, fromXYZ); transform_vector(toRGB, LamRigg, toXYZ); // Build matrix cone[0] = toRGB[0]/fromRGB[0]; cone[1] = 0.0; cone[2] = 0.0; cone[3] = 0.0; cone[4] = toRGB[1]/fromRGB[1]; cone[5] = 0.0; cone[6] = 0.0; cone[7] = 0.0; cone[8] = toRGB[2]/fromRGB[2]; // Normalize matrix_compose(tmp, cone, LamRigg); matrix_compose(out, chad_inv, tmp); return 0; } static int build_rgb2XYZ_transfer_matrix(double *out, double *xyYwhite, const double *xyYprimaries) { double whitepoint[3], coef[3]; double xn, yn; double xr, yr; double xg, yg; double xb, yb; double primaries[9]; double result[9]; double mat[9]; double bradford[9]; static double d50XYZ[3] = { 0.9642, 1.0, 0.8249 }; xn = xyYwhite[0]; yn = xyYwhite[1]; xr = xyYprimaries[0]; yr = xyYprimaries[1]; xg = xyYprimaries[3]; yg = xyYprimaries[4]; xb = xyYprimaries[6]; yb = xyYprimaries[7]; // Build Primaries matrix primaries[0] = xr; primaries[1] = xg; primaries[2] = xb; primaries[3] = yr; primaries[4] = yg; primaries[5] = yb; primaries[6] = (1-xr-yr); primaries[7] = (1-xg-yg); primaries[8] = (1-xb-yb); // Result = Primaries ^ (-1) inverse matrix if (matrix_invert(&result[0], &primaries[0])) return 1; /* Convert whitepoint from xyY to XYZ. Isn't this where * we came in? I think we've effectively normalised during * this process. */ whitepoint[0] = xn/yn; whitepoint[1] = 1; whitepoint[2] = (1-xn-yn)/yn; // Across inverse primaries ... transform_vector(coef, result, whitepoint); // Give us the Coefs, then I build transformation matrix mat[0] = coef[0]*xr; mat[1] = coef[1]*xg; mat[2] = coef[2]*xb; mat[3] = coef[0]*yr; mat[4] = coef[1]*yg; mat[5] = coef[2]*yb; mat[6] = coef[0]*(1.0-xr-yr); mat[7] = coef[1]*(1.0-xg-yg); mat[8] = coef[2]*(1.0-xb-yb); if (adaptation_matrix(bradford, whitepoint, d50XYZ)) return 1; matrix_compose(out, bradford, mat); return 0; } fz_buffer * fz_new_icc_data_from_cal(fz_context *ctx, float wp[3], float bp[3], float *gamma, float matrix[9], int n) { fz_icc_tag *tag_list; icProfile iccprofile; icHeader *header = &(iccprofile.header); fz_buffer *profile = NULL; size_t profile_size; int k; int num_tags; unsigned short encode_gamma; int last_tag; icS15Fixed16Number temp_XYZ[3]; icTagSignature TRC_Tags[3] = { icSigRedTRCTag, icSigGreenTRCTag, icSigBlueTRCTag }; int trc_tag_size; float cat02[9]; float black_adapt[3]; const char *desc_name; /* common */ setheader_common(ctx, header); header->pcs = icSigXYZData; profile_size = ICC_HEADER_SIZE; header->deviceClass = icSigInputClass; if (n == 3) { desc_name = "CalRGB"; header->colorSpace = icSigRgbData; num_tags = 10; /* common (2) + rXYZ, gXYZ, bXYZ, rTRC, gTRC, bTRC, bkpt, wtpt */ } else { desc_name = "CalGray"; header->colorSpace = icSigGrayData; num_tags = 5; /* common (2) + GrayTRC, bkpt, wtpt */ TRC_Tags[0] = icSigGrayTRCTag; } tag_list = Memento_label(fz_malloc(ctx, sizeof(fz_icc_tag) * num_tags), "icc_tag_list"); /* precompute sizes and offsets */ profile_size += ICC_TAG_SIZE * num_tags; profile_size += 4; /* number of tags.... */ last_tag = -1; init_common_tags(ctx, tag_list, num_tags, &last_tag, desc_name); if (n == 3) { init_tag(ctx, tag_list, &last_tag, icSigRedColorantTag, ICC_XYZPT_SIZE); init_tag(ctx, tag_list, &last_tag, icSigGreenColorantTag, ICC_XYZPT_SIZE); init_tag(ctx, tag_list, &last_tag, icSigBlueColorantTag, ICC_XYZPT_SIZE); } init_tag(ctx, tag_list, &last_tag, icSigMediaWhitePointTag, ICC_XYZPT_SIZE); init_tag(ctx, tag_list, &last_tag, icSigMediaBlackPointTag, ICC_XYZPT_SIZE); /* 4 for count, 2 for gamma, Extra 2 bytes for 4 byte alignment requirement */ trc_tag_size = 8; for (k = 0; k < n; k++) init_tag(ctx, tag_list, &last_tag, TRC_Tags[k], trc_tag_size); for (k = 0; k < num_tags; k++) profile_size += tag_list[k].size; fz_var(profile); /* Allocate buffer */ fz_try(ctx) { profile = fz_new_buffer(ctx, profile_size); /* Header */ header->size = (icUInt32Number)profile_size; copy_header(ctx, profile, header); /* Tag table */ copy_tagtable(ctx, profile, tag_list, num_tags); /* Common tags */ add_common_tag_data(ctx, profile, tag_list, desc_name); /* Get the cat02 matrix */ gsicc_create_compute_cam(ctx, wp, cat02); /* The matrix */ if (n == 3) { /* Convert whitepoint from XYZ to xyY */ double xyz = wp[0] + wp[1] + wp[2]; double whitexyY[3] = { wp[0] / xyz, wp[1] / xyz, 1.0 }; /* Convert primaries from XYZ to xyY */ double matrix012 = matrix[0] + matrix[1] + matrix[2]; double matrix345 = matrix[3] + matrix[4] + matrix[5]; double matrix678 = matrix[6] + matrix[7] + matrix[8]; double primariesxyY[9] = { matrix[0] / matrix012, matrix[1] / matrix012, matrix[1], matrix[3] / matrix345, matrix[4] / matrix345, matrix[5], matrix[6] / matrix678, matrix[7] / matrix678, matrix[8] }; double primaries[9]; if (build_rgb2XYZ_transfer_matrix(primaries, whitexyY, primariesxyY)) fz_throw(ctx, FZ_ERROR_ARGUMENT, "CalRGB profile creation failed; bad values"); for (k = 0; k < 3; k++) { float primary[3] = { primaries[k+0], primaries[k+3], primaries[k+6] }; get_XYZ_doubletr(ctx, temp_XYZ, primary); add_xyzdata(ctx, profile, temp_XYZ); } } /* White and black points. WP is D50 */ get_D50(ctx, temp_XYZ); add_xyzdata(ctx, profile, temp_XYZ); /* Black point. Apply cat02*/ apply_adaption(ctx, cat02, bp, &(black_adapt[0])); get_XYZ_doubletr(ctx, temp_XYZ, &(black_adapt[0])); add_xyzdata(ctx, profile, temp_XYZ); /* Gamma */ for (k = 0; k < n; k++) { encode_gamma = float2u8Fixed8(ctx, gamma[k]); add_gammadata(ctx, profile, encode_gamma, icSigCurveType); } } fz_always(ctx) fz_free(ctx, tag_list); fz_catch(ctx) { fz_drop_buffer(ctx, profile); fz_rethrow(ctx); } #if SAVEICCPROFILE if (n == 3) save_profile(ctx, profile, "calRGB"); else save_profile(ctx, profile, "calGray"); #endif return profile; }