bool skcms_Transform()

in modules/skcms/skcms.cc [2561:2883]


bool skcms_Transform(const void*             src,
                     skcms_PixelFormat       srcFmt,
                     skcms_AlphaFormat       srcAlpha,
                     const skcms_ICCProfile* srcProfile,
                     void*                   dst,
                     skcms_PixelFormat       dstFmt,
                     skcms_AlphaFormat       dstAlpha,
                     const skcms_ICCProfile* dstProfile,
                     size_t                  nz) {
    const size_t dst_bpp = bytes_per_pixel(dstFmt),
                 src_bpp = bytes_per_pixel(srcFmt);
    // Let's just refuse if the request is absurdly big.
    if (nz * dst_bpp > INT_MAX || nz * src_bpp > INT_MAX) {
        return false;
    }
    int n = (int)nz;

    // Null profiles default to sRGB. Passing null for both is handy when doing format conversion.
    if (!srcProfile) {
        srcProfile = skcms_sRGB_profile();
    }
    if (!dstProfile) {
        dstProfile = skcms_sRGB_profile();
    }

    // We can't transform in place unless the PixelFormats are the same size.
    if (dst == src && dst_bpp != src_bpp) {
        return false;
    }
    // TODO: more careful alias rejection (like, dst == src + 1)?

    Op          program[32];
    const void* context[32];

    Op*          ops      = program;
    const void** contexts = context;

    auto add_op = [&](Op o) {
        *ops++ = o;
        *contexts++ = nullptr;
    };

    auto add_op_ctx = [&](Op o, const void* c) {
        *ops++ = o;
        *contexts++ = c;
    };

    auto add_curve_ops = [&](const skcms_Curve* curves, int numChannels) {
        OpAndArg oa[4];
        assert(numChannels <= ARRAY_COUNT(oa));

        int numOps = select_curve_ops(curves, numChannels, oa);

        for (int i = 0; i < numOps; ++i) {
            add_op_ctx(oa[i].op, oa[i].arg);
        }
    };

    // These are always parametric curves of some sort.
    skcms_Curve dst_curves[3];
    dst_curves[0].table_entries =
    dst_curves[1].table_entries =
    dst_curves[2].table_entries = 0;

    skcms_Matrix3x3        from_xyz;

    switch (srcFmt >> 1) {
        default: return false;
        case skcms_PixelFormat_A_8              >> 1: add_op(Op::load_a8);          break;
        case skcms_PixelFormat_G_8              >> 1: add_op(Op::load_g8);          break;
        case skcms_PixelFormat_GA_88            >> 1: add_op(Op::load_ga88);        break;
        case skcms_PixelFormat_ABGR_4444        >> 1: add_op(Op::load_4444);        break;
        case skcms_PixelFormat_RGB_565          >> 1: add_op(Op::load_565);         break;
        case skcms_PixelFormat_RGB_888          >> 1: add_op(Op::load_888);         break;
        case skcms_PixelFormat_RGBA_8888        >> 1: add_op(Op::load_8888);        break;
        case skcms_PixelFormat_RGBA_1010102     >> 1: add_op(Op::load_1010102);     break;
        case skcms_PixelFormat_RGB_101010x_XR   >> 1: add_op(Op::load_101010x_XR);  break;
        case skcms_PixelFormat_RGBA_10101010_XR >> 1: add_op(Op::load_10101010_XR); break;
        case skcms_PixelFormat_RGB_161616LE     >> 1: add_op(Op::load_161616LE);    break;
        case skcms_PixelFormat_RGBA_16161616LE  >> 1: add_op(Op::load_16161616LE);  break;
        case skcms_PixelFormat_RGB_161616BE     >> 1: add_op(Op::load_161616BE);    break;
        case skcms_PixelFormat_RGBA_16161616BE  >> 1: add_op(Op::load_16161616BE);  break;
        case skcms_PixelFormat_RGB_hhh_Norm     >> 1: add_op(Op::load_hhh);         break;
        case skcms_PixelFormat_RGBA_hhhh_Norm   >> 1: add_op(Op::load_hhhh);        break;
        case skcms_PixelFormat_RGB_hhh          >> 1: add_op(Op::load_hhh);         break;
        case skcms_PixelFormat_RGBA_hhhh        >> 1: add_op(Op::load_hhhh);        break;
        case skcms_PixelFormat_RGB_fff          >> 1: add_op(Op::load_fff);         break;
        case skcms_PixelFormat_RGBA_ffff        >> 1: add_op(Op::load_ffff);        break;

        case skcms_PixelFormat_RGBA_8888_sRGB >> 1:
            add_op(Op::load_8888);
            add_op_ctx(Op::tf_rgb, skcms_sRGB_TransferFunction());
            break;
    }
    if (srcFmt == skcms_PixelFormat_RGB_hhh_Norm ||
        srcFmt == skcms_PixelFormat_RGBA_hhhh_Norm) {
        add_op(Op::clamp);
    }
    if (srcFmt & 1) {
        add_op(Op::swap_rb);
    }
    skcms_ICCProfile gray_dst_profile;
    switch (dstFmt >> 1) {
        case skcms_PixelFormat_G_8:
        case skcms_PixelFormat_GA_88:
            // When transforming to gray, stop at XYZ (by setting toXYZ to identity), then transform
            // luminance (Y) by the destination transfer function.
            gray_dst_profile = *dstProfile;
            skcms_SetXYZD50(&gray_dst_profile, &skcms_XYZD50_profile()->toXYZD50);
            dstProfile = &gray_dst_profile;
            break;
        default:
            break;
    }

    if (srcProfile->data_color_space == skcms_Signature_CMYK) {
        // Photoshop creates CMYK images as inverse CMYK.
        // These happen to be the only ones we've _ever_ seen.
        add_op(Op::invert);
        // With CMYK, ignore the alpha type, to avoid changing K or conflating CMY with K.
        srcAlpha = skcms_AlphaFormat_Unpremul;
    }

    if (srcAlpha == skcms_AlphaFormat_Opaque) {
        add_op(Op::force_opaque);
    } else if (srcAlpha == skcms_AlphaFormat_PremulAsEncoded) {
        add_op(Op::unpremul);
    }

    if (dstProfile != srcProfile) {

        if (!prep_for_destination(dstProfile,
                                  &from_xyz,
                                  &dst_curves[0].parametric,
                                  &dst_curves[1].parametric,
                                  &dst_curves[2].parametric)) {
            return false;
        }

        if (srcProfile->has_A2B) {
            if (srcProfile->A2B.input_channels) {
                add_curve_ops(srcProfile->A2B.input_curves,
                              (int)srcProfile->A2B.input_channels);
                add_op(Op::clamp);
                add_op_ctx(Op::clut_A2B, &srcProfile->A2B);
            }

            if (srcProfile->A2B.matrix_channels == 3) {
                add_curve_ops(srcProfile->A2B.matrix_curves, /*numChannels=*/3);

                static const skcms_Matrix3x4 I = {{
                    {1,0,0,0},
                    {0,1,0,0},
                    {0,0,1,0},
                }};
                if (0 != memcmp(&I, &srcProfile->A2B.matrix, sizeof(I))) {
                    add_op_ctx(Op::matrix_3x4, &srcProfile->A2B.matrix);
                }
            }

            if (srcProfile->A2B.output_channels == 3) {
                add_curve_ops(srcProfile->A2B.output_curves, /*numChannels=*/3);
            }

            if (srcProfile->pcs == skcms_Signature_Lab) {
                add_op(Op::lab_to_xyz);
            }

        } else if (srcProfile->has_trc && srcProfile->has_toXYZD50) {
            add_curve_ops(srcProfile->trc, /*numChannels=*/3);
        } else {
            return false;
        }

        // A2B sources are in XYZD50 by now, but TRC sources are still in their original gamut.
        assert (srcProfile->has_A2B || srcProfile->has_toXYZD50);

        if (dstProfile->has_B2A) {
            // B2A needs its input in XYZD50, so transform TRC sources now.
            if (!srcProfile->has_A2B) {
                add_op_ctx(Op::matrix_3x3, &srcProfile->toXYZD50);
            }

            if (dstProfile->pcs == skcms_Signature_Lab) {
                add_op(Op::xyz_to_lab);
            }

            if (dstProfile->B2A.input_channels == 3) {
                add_curve_ops(dstProfile->B2A.input_curves, /*numChannels=*/3);
            }

            if (dstProfile->B2A.matrix_channels == 3) {
                static const skcms_Matrix3x4 I = {{
                    {1,0,0,0},
                    {0,1,0,0},
                    {0,0,1,0},
                }};
                if (0 != memcmp(&I, &dstProfile->B2A.matrix, sizeof(I))) {
                    add_op_ctx(Op::matrix_3x4, &dstProfile->B2A.matrix);
                }

                add_curve_ops(dstProfile->B2A.matrix_curves, /*numChannels=*/3);
            }

            if (dstProfile->B2A.output_channels) {
                add_op(Op::clamp);
                add_op_ctx(Op::clut_B2A, &dstProfile->B2A);

                add_curve_ops(dstProfile->B2A.output_curves,
                              (int)dstProfile->B2A.output_channels);
            }
        } else {
            // This is a TRC destination.
            // We'll concat any src->xyz matrix with our xyz->dst matrix into one src->dst matrix.
            // (A2B sources are already in XYZD50, making that src->xyz matrix I.)
            static const skcms_Matrix3x3 I = {{
                { 1.0f, 0.0f, 0.0f },
                { 0.0f, 1.0f, 0.0f },
                { 0.0f, 0.0f, 1.0f },
            }};
            const skcms_Matrix3x3* to_xyz = srcProfile->has_A2B ? &I : &srcProfile->toXYZD50;

            // There's a chance the source and destination gamuts are identical,
            // in which case we can skip the gamut transform.
            if (0 != memcmp(&dstProfile->toXYZD50, to_xyz, sizeof(skcms_Matrix3x3))) {
                // Concat the entire gamut transform into from_xyz,
                // now slightly misnamed but it's a handy spot to stash the result.
                from_xyz = skcms_Matrix3x3_concat(&from_xyz, to_xyz);
                add_op_ctx(Op::matrix_3x3, &from_xyz);
            }

            // Encode back to dst RGB using its parametric transfer functions.
            OpAndArg oa[3];
            int numOps = select_curve_ops(dst_curves, /*numChannels=*/3, oa);
            for (int index = 0; index < numOps; ++index) {
                assert(oa[index].op != Op::table_r &&
                       oa[index].op != Op::table_g &&
                       oa[index].op != Op::table_b &&
                       oa[index].op != Op::table_a);
                add_op_ctx(oa[index].op, oa[index].arg);
            }
        }
    }

    // Clamp here before premul to make sure we're clamping to normalized values _and_ gamut,
    // not just to values that fit in [0,1].
    //
    // E.g. r = 1.1, a = 0.5 would fit fine in fixed point after premul (ra=0.55,a=0.5),
    // but would be carrying r > 1, which is really unexpected for downstream consumers.
    if (dstFmt < skcms_PixelFormat_RGB_hhh) {
        add_op(Op::clamp);
    }

    if (dstProfile->data_color_space == skcms_Signature_CMYK) {
        // Photoshop creates CMYK images as inverse CMYK.
        // These happen to be the only ones we've _ever_ seen.
        add_op(Op::invert);

        // CMYK has no alpha channel, so make sure dstAlpha is a no-op.
        dstAlpha = skcms_AlphaFormat_Unpremul;
    }

    if (dstAlpha == skcms_AlphaFormat_Opaque) {
        add_op(Op::force_opaque);
    } else if (dstAlpha == skcms_AlphaFormat_PremulAsEncoded) {
        add_op(Op::premul);
    }
    if (dstFmt & 1) {
        add_op(Op::swap_rb);
    }
    switch (dstFmt >> 1) {
        default: return false;
        case skcms_PixelFormat_A_8              >> 1: add_op(Op::store_a8);          break;
        case skcms_PixelFormat_G_8              >> 1: add_op(Op::store_g8);          break;
        case skcms_PixelFormat_GA_88            >> 1: add_op(Op::store_ga88);        break;
        case skcms_PixelFormat_ABGR_4444        >> 1: add_op(Op::store_4444);        break;
        case skcms_PixelFormat_RGB_565          >> 1: add_op(Op::store_565);         break;
        case skcms_PixelFormat_RGB_888          >> 1: add_op(Op::store_888);         break;
        case skcms_PixelFormat_RGBA_8888        >> 1: add_op(Op::store_8888);        break;
        case skcms_PixelFormat_RGBA_1010102     >> 1: add_op(Op::store_1010102);     break;
        case skcms_PixelFormat_RGB_161616LE     >> 1: add_op(Op::store_161616LE);    break;
        case skcms_PixelFormat_RGBA_16161616LE  >> 1: add_op(Op::store_16161616LE);  break;
        case skcms_PixelFormat_RGB_161616BE     >> 1: add_op(Op::store_161616BE);    break;
        case skcms_PixelFormat_RGBA_16161616BE  >> 1: add_op(Op::store_16161616BE);  break;
        case skcms_PixelFormat_RGB_hhh_Norm     >> 1: add_op(Op::store_hhh);         break;
        case skcms_PixelFormat_RGBA_hhhh_Norm   >> 1: add_op(Op::store_hhhh);        break;
        case skcms_PixelFormat_RGB_101010x_XR   >> 1: add_op(Op::store_101010x_XR);  break;
        case skcms_PixelFormat_RGBA_10101010_XR >> 1: add_op(Op::store_10101010_XR); break;
        case skcms_PixelFormat_RGB_hhh          >> 1: add_op(Op::store_hhh);         break;
        case skcms_PixelFormat_RGBA_hhhh        >> 1: add_op(Op::store_hhhh);        break;
        case skcms_PixelFormat_RGB_fff          >> 1: add_op(Op::store_fff);         break;
        case skcms_PixelFormat_RGBA_ffff        >> 1: add_op(Op::store_ffff);        break;

        case skcms_PixelFormat_RGBA_8888_sRGB >> 1:
            add_op_ctx(Op::tf_rgb, skcms_sRGB_Inverse_TransferFunction());
            add_op(Op::store_8888);
            break;
    }

    assert(ops      <= program + ARRAY_COUNT(program));
    assert(contexts <= context + ARRAY_COUNT(context));

    auto run = baseline::run_program;
    switch (cpu_type()) {
        case CpuType::SKX:
            #if !defined(SKCMS_DISABLE_SKX)
                run = skx::run_program;
                break;
            #endif

        case CpuType::HSW:
            #if !defined(SKCMS_DISABLE_HSW)
                run = hsw::run_program;
                break;
            #endif

        case CpuType::Baseline:
            break;
    }

    run(program, context, ops - program, (const char*)src, (char*)dst, n, src_bpp,dst_bpp);
    return true;
}