Files
dawn/tests/test-channels.cpp
p 2390a18079
Alpine 3.24 Success
Alpine 3.24 Flatpak Success
Arch Linux AUR Success
Cleanup
2026-10-04 05:06:59 +02:00

1346 lines
42 KiB
C++

//
// test-channels.cpp: verify BGRA_PREMUL_4X16LE channel order
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include <dawn-config.h>
#include "test.hpp"
#include <libdn/libdn-loaders.hpp>
#include <libdn/libdn.hpp>
#include <lcms2.h>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <limits>
#include <string>
#include <vector>
using namespace std;
namespace fs = filesystem;
#ifndef DAWN_TEST_FIXTURES_DIR
#error DAWN_TEST_FIXTURES_DIR must be defined
#endif
static bool
near_u16(uint16_t a, uint16_t b, uint16_t tol)
{
return abs(int(a) - int(b)) <= int(tol);
}
namespace
{
struct Pixel {
uint16_t b = 0, g = 0, r = 0, a = 0;
};
} // namespace
static Pixel
pixel0(const dawn::Image &img)
{
const uint16_t *p = dawn::row_u16(img, 0);
return {p[0], p[1], p[2], p[3]};
}
static Pixel
pixel_at(const dawn::Image &img, uint32_t x, uint32_t y)
{
const uint16_t *p = dawn::row_u16(img, y) + x * 4;
return {p[0], p[1], p[2], p[3]};
}
static void
expect_bgra(const char *label, Pixel p, uint16_t b, uint16_t g, uint16_t r,
uint16_t a, uint16_t tol)
{
if (!near_u16(p.b, b, tol) || !near_u16(p.g, g, tol) ||
!near_u16(p.r, r, tol) || !near_u16(p.a, a, tol)) {
test::fail(
"%s: got BGRA (%u,%u,%u,%u) expected (~%u,~%u,~%u,~%u) tol=%u",
label, p.b, p.g, p.r, p.a, b, g, r, a, tol);
}
}
static dawn::ImagePtr
load_fixture(const string &name)
{
fs::path path = fs::path(DAWN_TEST_FIXTURES_DIR) / name;
dawn::OpenContext ctx;
ctx.uri = dawn::path_to_uri(path.string());
ctx.first_frame_only = true;
dawn::Error error;
dawn::ImagePtr img = dawn::open(ctx, &error);
if (!img) {
test::fail(
"open(%s): %s", path.string().c_str(), error.message.c_str());
}
return img;
}
static void
test_pack_helpers()
{
dawn::ImagePtr img = dawn::image_new(1, 1, nullptr);
CHECK(img != nullptr);
// RGBA8 red → B=0 G=0 R=65535 A=65535
{
const uint8_t rgba[] = {255, 0, 0, 255};
dawn::pack_rgba8_to_bgra16(*img, rgba, 4);
expect_bgra("pack_rgba8 red", pixel0(*img), 0, 0, 65535, 65535, 0);
}
{
const uint8_t rgba[] = {0, 255, 0, 255};
dawn::pack_rgba8_to_bgra16(*img, rgba, 4);
expect_bgra("pack_rgba8 green", pixel0(*img), 0, 65535, 0, 65535, 0);
}
{
const uint8_t rgba[] = {0, 0, 255, 255};
dawn::pack_rgba8_to_bgra16(*img, rgba, 4);
expect_bgra("pack_rgba8 blue", pixel0(*img), 65535, 0, 0, 65535, 0);
}
{
const uint8_t rgb[] = {255, 0, 0};
dawn::pack_rgb8_to_bgra16(*img, rgb, 3);
expect_bgra("pack_rgb8 red", pixel0(*img), 0, 0, 65535, 65535, 0);
}
{
// Host-endian 0xAARRGGBB: opaque red
const uint32_t word = 0xFFFF0000u;
dawn::pack_argb32_words_to_bgra16(*img, &word, sizeof word);
expect_bgra("pack_argb32 red", pixel0(*img), 0, 0, 65535, 65535, 0);
}
{
const uint8_t bgra8[] = {0, 0, 255, 255}; // B,G,R,A
dawn::widen_bgra8_to_bgra16(*img, bgra8, 4);
expect_bgra("widen_bgra8 red", pixel0(*img), 0, 0, 65535, 65535, 0);
}
{
// LE R,G,B,A uint16 — 10-bit max red in low 10 bits → scaled
const uint16_t rgba16[] = {1023, 0, 0, 1023};
dawn::pack_rgba16le_to_bgra16(*img, rgba16, 8, 10);
expect_bgra(
"pack_rgba16le 10-bit red", pixel0(*img), 0, 0, 65535, 65535, 0);
}
{
const uint16_t rgb16[] = {0, 0, 65535};
dawn::pack_rgb16le_to_bgra16(*img, rgb16, 6, 16);
expect_bgra("pack_rgb16le blue", pixel0(*img), 65535, 0, 0, 65535, 0);
}
}
static void
test_unpremultiply_alpha_last8()
{
// Two padded rows, so that row padding can be proven untouched.
const uint32_t width = 4, height = 2;
const size_t stride = size_t(width) * 4 + 3;
const uint8_t input[height][4][4] = {
{{0, 10, 20, 0}, {1, 1, 1, 1}, {64, 128, 200, 128}, {10, 20, 30, 255}},
{{255, 255, 255, 255}, {5, 0, 3, 1}, {128, 128, 128, 128},
{0, 0, 0, 0}},
};
const uint8_t expected[height][4][4] = {
{{0, 0, 0, 0}, {255, 255, 255, 1}, {128, 255, 255, 128},
{10, 20, 30, 255}},
{{255, 255, 255, 255}, {255, 0, 255, 1}, {255, 255, 255, 128},
{0, 0, 0, 0}},
};
vector<uint8_t> buffer(stride * height, 0xCD);
for (uint32_t y = 0; y < height; y++)
memcpy(buffer.data() + y * stride, input[y], sizeof input[y]);
dawn::unpremultiply_xxxa8(buffer.data(), width, height, stride);
for (uint32_t y = 0; y < height; y++) {
const uint8_t *row = buffer.data() + y * stride;
for (uint32_t x = 0; x < width * 4; x++) {
const uint8_t *want = &expected[y][0][0];
if (row[x] != want[x]) {
test::fail("row %u byte %u: got %u expected %u", y, x, row[x],
want[x]);
}
}
for (size_t x = size_t(width) * 4; x < stride; x++)
CHECK(row[x] == 0xCD);
}
}
static dawn::ImagePtr
image_1x1(uint16_t b, uint16_t g, uint16_t r, uint16_t a)
{
dawn::ImagePtr img = dawn::image_new(1, 1, nullptr);
if (!img) {
test::fail("image_new failed");
exit(1);
}
uint16_t *p = dawn::row_u16(*img, 0);
p[0] = b;
p[1] = g;
p[2] = r;
p[3] = a;
return img;
}
static void
test_finishing()
{
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
auto p3 = cmm->get_profile_display_p3();
CHECK(srgb != nullptr && p3 != nullptr);
vector<uint8_t> srgb_icc = srgb->to_bytes(), p3_icc = p3->to_bytes();
CHECK(!srgb_icc.empty() && !p3_icc.empty());
dawn::OpenContext plain;
plain.cmm = cmm;
dawn::OpenContext to_srgb = plain;
to_srgb.screen_profile = srgb;
dawn::OpenContext to_p3 = plain;
to_p3.screen_profile = p3;
// Straight pixels are premultiplied, sRGB is assumed and admitted to.
{
dawn::ImagePtr img = image_1x1(1000, 2000, 3000, 32768);
dawn::finish_image(*img, plain, nullptr, false);
expect_bgra(
"straight without target", pixel0(*img), 500, 1000, 1500, 32768, 0);
CHECK(img->profile_assumed);
CHECK(dawn::profiles_equal(img->effective_profile.get(), srgb.get()));
}
// Premultiplied pixels with nowhere to convert them to stay untouched.
{
dawn::ImagePtr img = image_1x1(500, 1000, 1500, 32768);
dawn::finish_image(*img, plain, nullptr, true);
expect_bgra("premultiplied without target", pixel0(*img), 500, 1000,
1500, 32768, 0);
CHECK(img->profile_assumed);
CHECK(img->effective_profile != nullptr);
}
// With a target, they make a round trip through the transform.
{
dawn::ImagePtr img = image_1x1(500, 1000, 1500, 32768);
dawn::finish_image(*img, to_srgb, nullptr, true);
expect_bgra(
"premultiplied to sRGB", pixel0(*img), 500, 1000, 1500, 32768, 257);
CHECK(img->profile_assumed);
}
// A valid embedded profile is what the pixels are then described by.
{
dawn::ImagePtr img = image_1x1(0, 0, 65535, 65535);
img->icc = p3_icc;
dawn::finish_image(*img, plain, nullptr, false);
expect_bgra("embedded ICC", pixel0(*img), 0, 0, 65535, 65535, 0);
CHECK(!img->profile_assumed);
CHECK(dawn::profiles_equal(img->effective_profile.get(), p3.get()));
}
// An unusable one falls back to assumed sRGB, as an absent one does.
{
dawn::ImagePtr img = image_1x1(0, 0, 65535, 65535);
img->icc = {0xDE, 0xAD, 0xBE, 0xEF};
dawn::finish_image(*img, plain, nullptr, false);
expect_bgra("invalid ICC", pixel0(*img), 0, 0, 65535, 65535, 0);
CHECK(img->profile_assumed);
CHECK(dawn::profiles_equal(img->effective_profile.get(), srgb.get()));
}
// A profile the loader already settled on survives an embedded one.
{
dawn::ImagePtr img = image_1x1(0, 0, 65535, 65535);
img->icc = p3_icc;
img->effective_profile = srgb;
dawn::finish_image(*img, plain, nullptr, false);
CHECK(img->effective_profile == srgb);
CHECK(!img->profile_assumed);
}
// An explicit source takes precedence over the embedded profile:
// sRGB red converted to Display P3 lands well inside its gamut, while
// P3 red stays at the edge.
dawn::ImagePtr from_srgb = image_1x1(0, 0, 65535, 65535);
from_srgb->icc = p3_icc;
dawn::finish_image(*from_srgb, to_p3, srgb.get(), false);
// Naming a source leaves describing it to the loader that named it.
CHECK(from_srgb->effective_profile == nullptr);
dawn::ImagePtr from_p3 = image_1x1(0, 0, 65535, 65535);
from_p3->icc = srgb_icc;
dawn::finish_image(*from_p3, to_p3, p3.get(), false);
const Pixel srgb_red = pixel0(*from_srgb), p3_red = pixel0(*from_p3);
CHECK(srgb_red.r + 1000 < p3_red.r);
CHECK(srgb_red.g > p3_red.g + 1000);
CHECK(srgb_red.a == 65535 && p3_red.a == 65535);
// Frames without a profile of their own inherit the page's.
{
dawn::ImagePtr page = image_1x1(1000, 2000, 3000, 32768);
page->icc = p3_icc;
page->frame_next = image_1x1(1000, 2000, 3000, 32768);
page->frame_next->frame_previous = page;
dawn::finish_frames(*page, plain, nullptr, false);
expect_bgra("frame page", pixel0(*page), 500, 1000, 1500, 32768, 0);
expect_bgra(
"frame tail", pixel0(*page->frame_next), 500, 1000, 1500, 32768, 0);
CHECK(page->frame_next->effective_profile == page->effective_profile);
CHECK(!page->frame_next->profile_assumed);
CHECK(dawn::profiles_equal(page->effective_profile.get(), p3.get()));
}
// Including the assumption made for a page without one.
{
dawn::ImagePtr page = image_1x1(0, 0, 65535, 65535);
page->frame_next = image_1x1(0, 0, 65535, 65535);
page->frame_next->frame_previous = page;
dawn::finish_frames(*page, plain, nullptr, false);
CHECK(page->profile_assumed && page->frame_next->profile_assumed);
CHECK(page->frame_next->effective_profile == page->effective_profile);
}
}
static void
test_solid(const char *path, uint16_t b, uint16_t g, uint16_t r, uint16_t tol)
{
dawn::ImagePtr img = load_fixture(path);
if (!img)
return;
CHECK(img->width == 1 && img->height == 1);
expect_bgra(path, pixel0(*img), b, g, r, 65535, tol);
}
static void
test_loaders_solid()
{
struct Solid {
const char *path;
uint16_t b, g, r, tolerance = 0;
};
const Solid solids[] = {
{"red.png", 0, 0, 65535},
{"green.png", 0, 65535, 0},
{"blue.png", 65535, 0, 0},
{"white.png", 65535, 65535, 65535},
{"black.png", 0, 0, 0},
{"red16.png", 0, 0, 65535},
{"green16.png", 0, 65535, 0},
{"blue16.png", 65535, 0, 0},
{"red.bmp", 0, 0, 65535},
{"green.bmp", 0, 65535, 0},
{"blue.bmp", 65535, 0, 0},
{"red.tga", 0, 0, 65535},
{"green.tga", 0, 65535, 0},
{"blue.tga", 65535, 0, 0},
{"red.webp", 0, 0, 65535},
{"green.webp", 0, 65535, 0},
{"blue.webp", 65535, 0, 0},
{"red.jpg", 0, 0, 65535, 514},
{"green.jpg", 0, 65535, 0, 514},
{"blue.jpg", 65535, 0, 0, 514},
};
for (const Solid &solid : solids)
test_solid(solid.path, solid.b, solid.g, solid.r, solid.tolerance);
#if DAWN_WITH_LIBTIFF
test_solid("red.tif", 0, 0, 65535, 0);
test_solid("green.tif", 0, 65535, 0, 0);
test_solid("blue.tif", 65535, 0, 0, 0);
#endif
}
static void
test_jpeg_cms_8_to_16()
{
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
CHECK(srgb != nullptr);
const uint8_t src[4] = {0, 0, 255, 255};
uint8_t dst[8] = {};
CHECK(cmm->transform_bgra8_to_bgra16(
src, dst, 1, 1, srgb.get(), srgb.get(), true));
const uint16_t *p = reinterpret_cast<const uint16_t *>(dst);
expect_bgra("bgra8→16 premul red", {p[0], p[1], p[2], p[3]}, 0, 0, 65535,
65535, 257 * 2);
dawn::OpenContext ctx;
ctx.cmm = cmm;
ctx.screen_profile = srgb;
ctx.first_frame_only = true;
ctx.uri = dawn::path_to_uri(
(fs::path(DAWN_TEST_FIXTURES_DIR) / "blue.jpg").string());
dawn::Error error;
dawn::ImagePtr img = dawn::open(ctx, &error);
if (!img) {
test::fail("jpeg cms blue.jpg: %s", error.message.c_str());
return;
}
expect_bgra("jpeg cms blue.jpg", pixel0(*img), 65535, 0, 0, 65535, 257 * 2);
}
static void
test_jpeg_fatal_error()
{
vector<uint8_t> bytes = test::fixture("blue.jpg");
CHECK(bytes.size() >= 2);
if (bytes.size() < 2)
return;
// Replace the final EOI with an invalid marker. The header and scanline are
// valid, so libjpeg reports the fatal error only while finishing the
// decompression, after the image and temporary pixel buffer have been made.
CHECK(bytes[bytes.size() - 2] == 0xff && bytes.back() == 0xd9);
if (bytes[bytes.size() - 2] != 0xff || bytes.back() != 0xd9)
return;
bytes.back() = 0x02;
for (bool enhance : {false, true}) {
dawn::OpenContext ctx;
ctx.enhance = enhance;
ctx.first_frame_only = true;
dawn::Error error;
dawn::ImagePtr img = dawn::load_jpeg(bytes, ctx, &error);
CHECK(img == nullptr);
CHECK(error);
CHECK(!error.message.empty());
}
}
static void
test_cmyk_cms_opaque()
{
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
CHECK(srgb != nullptr);
const vector<uint8_t> bytes = test::fixture("cmyk-lab.icc");
if (bytes.empty())
return;
auto src = cmm->get_profile(bytes);
CHECK(src != nullptr);
dawn::ImagePtr img = dawn::image_new(1, 1, nullptr);
CHECK(img != nullptr);
const uint8_t cmyk[4] = {0, 255, 255, 0};
cmm->convert_cmyk8(
cmyk, img->data.data(), img->width, img->height, src.get(), srgb.get());
Pixel p = pixel0(*img);
CHECK(p.a == 65535);
CHECK(unsigned(p.b) + p.g + p.r != 0);
}
static void
test_cms_tiled()
{
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
CHECK(srgb != nullptr);
// Above the serial threshold so row bands run on more than one worker.
constexpr uint32_t w = 600;
constexpr uint32_t h = 128;
vector<uint8_t> src(size_t(w) * h * 4);
for (size_t i = 0; i < src.size(); i += 4) {
src[i + 0] = 0;
src[i + 1] = 0;
src[i + 2] = 255;
src[i + 3] = 255;
}
vector<uint8_t> dst(size_t(w) * h * dawn::kBytesPerPixel, uint8_t(0x5a));
CHECK(cmm->transform_bgra8_to_bgra16(
src.data(), dst.data(), w, h, srgb.get(), srgb.get(), true));
const uint16_t *p = reinterpret_cast<const uint16_t *>(dst.data());
for (uint32_t i = 0; i < w * h; i++) {
if (!near_u16(p[i * 4 + 0], 0, 257 * 2) ||
!near_u16(p[i * 4 + 1], 0, 257 * 2) ||
!near_u16(p[i * 4 + 2], 65535, 257 * 2) ||
!near_u16(p[i * 4 + 3], 65535, 0)) {
test::fail("tiled cms: pixel %u BGRA (%u,%u,%u,%u)", i,
p[i * 4 + 0], p[i * 4 + 1], p[i * 4 + 2], p[i * 4 + 3]);
break;
}
}
}
static void
test_rgbw_2x2()
{
dawn::ImagePtr img = load_fixture("rgbw_2x2.png");
if (!img)
return;
CHECK(img->width == 2 && img->height == 2);
expect_bgra("rgbw[0,0] red", pixel_at(*img, 0, 0), 0, 0, 65535, 65535, 0);
expect_bgra("rgbw[1,0] green", pixel_at(*img, 1, 0), 0, 65535, 0, 65535, 0);
expect_bgra("rgbw[0,1] blue", pixel_at(*img, 0, 1), 65535, 0, 0, 65535, 0);
expect_bgra(
"rgbw[1,1] white", pixel_at(*img, 1, 1), 65535, 65535, 65535, 65535, 0);
}
static void
test_premul_alpha()
{
dawn::ImagePtr img = load_fixture("red_a128.png");
if (!img)
return;
// Straight (255,0,0,128) → widen → premul: R=A=128*257=32896, B=G=0
constexpr uint16_t half = 128u * 257u;
expect_bgra("red_a128 premul", pixel0(*img), 0, 0, half, half, 1);
}
static void
test_large_icc_and_p3_red()
{
const fs::path path =
fs::path(DAWN_TEST_FIXTURES_DIR) / "display-p3-red_vs_srgb-red.png";
const vector<uint8_t> bytes = test::fixture(path.filename().string());
auto cmm = dawn::Cmm::get_default();
double whitepoint[2] = {0.3127, 0.3290};
double adobe_primaries[6] = {
0.6400, 0.3300, 0.2100, 0.7100, 0.1500, 0.0600};
auto target = cmm->get_profile_parametric(2.2, whitepoint, adobe_primaries);
CHECK(target != nullptr);
dawn::OpenContext context;
context.uri = dawn::path_to_uri(path.string());
context.cmm = cmm;
context.screen_profile = target;
context.first_frame_only = true;
dawn::Error error;
dawn::ImagePtr image = dawn::load_wuffs(bytes, context, &error);
if (!image) {
test::fail("Wuffs P3 fixture: %s", error.message.c_str());
return;
}
CHECK(image->width == 200 && image->height == 100);
CHECK(image->icc.size() > 8192);
const Pixel p3_red = pixel_at(*image, 50, 50);
const Pixel srgb_red = pixel_at(*image, 150, 50);
// Both source colors are encoded in the image's Display P3 profile. After
// conversion to an AdobeRGB-like target, P3 red remains visibly wider-gamut
// than sRGB red; they must not collapse to the same output pixel.
CHECK(p3_red.r > srgb_red.r + 2000);
CHECK(p3_red.g + 1000 < srgb_red.g);
CHECK(p3_red.b + 1000 < srgb_red.b);
}
static void
test_svg_solid(const char *path, uint16_t b, uint16_t g, uint16_t r)
{
dawn::ImagePtr img = load_fixture(path);
if (!img)
return;
CHECK(img->width >= 2 && img->height >= 2);
CHECK(img->render != nullptr);
// Sample the interior to dodge any residual edge filtering.
uint32_t x = img->width / 2;
uint32_t y = img->height / 2;
constexpr uint16_t tol = 257;
expect_bgra(path, pixel_at(*img, x, y), b, g, r, 65535, tol);
dawn::OpenContext ctx;
dawn::ImagePtr scaled = img->render->render(ctx, 2.0, nullptr);
if (!scaled) {
test::fail("%s: render(scale=2) failed", path);
return;
}
CHECK(scaled->width == img->width * 2);
CHECK(scaled->height == img->height * 2);
expect_bgra((string(path) + "@2x").c_str(),
pixel_at(*scaled, scaled->width / 2, scaled->height / 2), b, g, r,
65535, tol);
}
static void
test_svg()
{
test_svg_solid("red.svg", 0, 0, 65535);
test_svg_solid("green.svg", 0, 65535, 0);
test_svg_solid("blue.svg", 65535, 0, 0);
dawn::ImagePtr rgbw = load_fixture("rgbw_2x2.svg");
if (rgbw) {
CHECK(rgbw->width == 2 && rgbw->height == 2);
constexpr uint16_t tol = 257 * 2;
expect_bgra(
"svg rgbw[0,0]", pixel_at(*rgbw, 0, 0), 0, 0, 65535, 65535, tol);
expect_bgra(
"svg rgbw[1,0]", pixel_at(*rgbw, 1, 0), 0, 65535, 0, 65535, tol);
expect_bgra(
"svg rgbw[0,1]", pixel_at(*rgbw, 0, 1), 65535, 0, 0, 65535, tol);
expect_bgra("svg rgbw[1,1]", pixel_at(*rgbw, 1, 1), 65535, 65535, 65535,
65535, tol);
}
// Rerendering colour-manages as the context asks: sRGB red lands well
// inside Display P3, where it is no longer a primary.
dawn::ImagePtr red = load_fixture("red.svg");
if (red && red->render) {
auto cmm = dawn::Cmm::get_default();
dawn::OpenContext ctx;
ctx.cmm = cmm;
ctx.screen_profile = cmm->get_profile_display_p3();
dawn::Error error;
dawn::ImagePtr p3 = red->render->render(ctx, 1.0, &error);
if (!p3) {
test::fail("red.svg: P3 render: %s", error.message.c_str());
} else {
Pixel px = pixel_at(*p3, p3->width / 2, p3->height / 2);
CHECK(px.r < 63000);
CHECK(px.g > 2000);
CHECK(px.a == 65535);
}
}
dawn::ImagePtr half = load_fixture("red_a128.svg");
if (half) {
CHECK(half->render != nullptr);
// Cairo/resvg premul ~50% red: R≈A≈32768, B=G=0
constexpr uint16_t mid = 32768;
constexpr uint16_t tol = 257 * 4;
expect_bgra("svg red_a128",
pixel_at(*half, half->width / 2, half->height / 2), 0, 0, mid, mid,
tol);
}
}
static void
test_render_dimensions()
{
const double nan = numeric_limits<double>::quiet_NaN();
const double infinity = numeric_limits<double>::infinity();
const double limit = double(dawn::kMaxDimension);
struct Rejected {
const char *label;
double width, height;
} rejected[] = {
{"NaN width", nan, 1},
{"NaN height", 1, nan},
{"infinite width", infinity, 1},
{"infinite height", 1, infinity},
{"negative infinity", -infinity, 1},
{"zero", 0, 1},
{"negative", -1, 1},
{"over the limit", limit + 1, 1},
{"a fraction over the limit", 1, limit + 0.5},
};
uint32_t width = 123, height = 456;
for (const Rejected &entry : rejected) {
dawn::Error error;
if (dawn::render_dimensions(
entry.width, entry.height, &width, &height, &error))
test::fail("render_dimensions: %s accepted", entry.label);
else
CHECK(!error.message.empty());
}
// Nothing is written on failure, and no error is required either.
CHECK(width == 123 && height == 456);
CHECK(dawn::render_dimensions(0.25, limit, &width, &height, nullptr));
CHECK(width == 1 && height == uint32_t(limit));
CHECK(dawn::render_dimensions(64., 32.5, &width, &height, nullptr));
CHECK(width == 64 && height == 33);
}
#if DAWN_WITH_LIBWMF
// A 64x32 placeable metafile at 72 units per inch, drawing one rectangle.
static const uint8_t kWmf[] = {0xd7, 0xcd, 0xc6, 0x9a, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x40, 0x00, 0x20, 0x00, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00,
0x39, 0x57, 0x01, 0x00, 0x09, 0x00, 0x00, 0x03, 0x1d, 0x00, 0x00, 0x00,
0x00, 0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00,
0x0b, 0x02, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0c, 0x02,
0x20, 0x00, 0x40, 0x00, 0x07, 0x00, 0x00, 0x00, 0x1b, 0x04, 0x20, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00};
#endif
// Loads one vector document with a named backend, then checks that its
// closure rounds half pixels up, measures what it renders, and refuses
// impossible scales.
static void
test_rerender(const char *label, dawn::LoadFn *load, span<const uint8_t> data,
const dawn::OpenContext &ctx)
{
dawn::Error error;
dawn::ImagePtr image = load(data, ctx, &error);
if (!image || !image->render) {
test::fail("%s: %s", label, error.message.c_str());
return;
}
const uint32_t w = image->width, h = image->height;
dawn::ImagePtr scaled = image->render->render(ctx, 1.5, &error);
if (!scaled) {
test::fail("%s: 1.5x: %s", label, error.message.c_str());
} else if (scaled->width != uint32_t(ceil(w * 1.5)) ||
scaled->height != uint32_t(ceil(h * 1.5))) {
test::fail("%s: 1.5x of %ux%u gave %ux%u", label, w, h, scaled->width,
scaled->height);
}
for (double scale : {0., -1., 1e9, numeric_limits<double>::infinity(),
numeric_limits<double>::quiet_NaN()}) {
dawn::Error rejected;
if (image->render->render(ctx, scale, &rejected))
test::fail("%s: scale %g accepted", label, scale);
else
CHECK(!rejected.message.empty());
}
CHECK(image->nominal_width == w && image->nominal_height == h);
for (uint32_t divisor : {1u, 2u, 3u}) {
dawn::OpenContext fit_ctx = ctx;
fit_ctx.target_width = w / divisor;
fit_ctx.target_height = h * 2;
dawn::ImagePtr fitted = load(data, fit_ctx, &error);
if (!fitted) {
test::fail("%s: 1/%u: %s", label, divisor, error.message.c_str());
continue;
}
CHECK(fitted->nominal_width == w && fitted->nominal_height == h);
const float fit = min({float(fit_ctx.target_width) / float(w),
float(fit_ctx.target_height) / float(h), 1.f});
dawn::ImagePtr direct = image->render->render(ctx, fit, &error);
CHECK(direct && fitted->width == direct->width &&
fitted->height == direct->height);
uint32_t dw = 0, dh = 0;
CHECK(image->render->dimensions(fit, &dw, &dh, nullptr) &&
fitted->width == dw && fitted->height == dh);
if (divisor == 1)
CHECK(fitted->width == w && fitted->height == h);
else
CHECK(fitted->height < h);
}
}
// A two-page PDF, assembled rather than spelled out, so that its
// cross-reference offsets cannot drift away from what they point at.
static string
minimal_pdf()
{
const string objects[] = {
"<< /Type /Catalog /Pages 2 0 R >>",
"<< /Type /Pages /Kids [3 0 R 5 0 R] /Count 2 >>",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 64 32]\n"
" /Contents 4 0 R /Resources << >> >>",
"<< /Length 24 >>\nstream\n1 0 0 rg 0 0 64 32 re f\nendstream",
"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 32 64]\n"
" /Contents 4 0 R /Resources << >> >>",
};
string pdf = "%PDF-1.4\n";
vector<size_t> offsets;
int number = 0;
for (const string &object : objects) {
offsets.push_back(pdf.size());
pdf += to_string(++number) + " 0 obj\n" + object + "\nendobj\n";
}
const string size = to_string(offsets.size() + 1);
const size_t xref = pdf.size();
pdf += "xref\n0 " + size + "\n0000000000 65535 f \n";
for (size_t offset : offsets) {
// Entries are exactly twenty bytes, hence the trailing space.
char entry[32] = "";
snprintf(entry, sizeof entry, "%010zu 00000 n \n", offset);
pdf += entry;
}
pdf += "trailer\n<< /Size " + size + " /Root 1 0 R >>\nstartxref\n" +
to_string(xref) + "\n%%EOF\n";
return pdf;
}
static void
test_vector_rerender()
{
const vector<uint8_t> svg = test::fixture("red.svg");
if (svg.empty())
return;
// Go through each backend directly: loader fallback would otherwise
// let a broken one hide behind the next.
dawn::OpenContext svg_ctx;
svg_ctx.uri = dawn::path_to_uri(
(fs::path(DAWN_TEST_FIXTURES_DIR) / "red.svg").string());
test_rerender("resvg", &dawn::load_resvg, svg, svg_ctx);
#if DAWN_WITH_LIBRSVG
test_rerender("librsvg", &dawn::load_librsvg, svg, svg_ctx);
#endif
#if DAWN_WITH_LIBWMF
dawn::OpenContext wmf_ctx;
test_rerender("libwmf", &dawn::load_libwmf, kWmf, wmf_ctx);
#endif
#if DAWN_WITH_POPPLER
const string pdf = minimal_pdf();
dawn::OpenContext pdf_ctx;
// One PDF unit per pixel, so that the page's own size comes back.
pdf_ctx.screen_dpi = 72;
pdf_ctx.first_frame_only = true;
test_rerender("Poppler", &dawn::load_poppler,
{(const uint8_t *) pdf.data(), pdf.size()}, pdf_ctx);
#endif
}
// Later pages of a document only say what they measure, until rendered.
static void
test_pdf_later_pages()
{
#if DAWN_WITH_POPPLER
const string pdf = minimal_pdf();
dawn::OpenContext ctx;
ctx.screen_dpi = 72;
dawn::Error error;
dawn::ImagePtr first = dawn::load_poppler(
{(const uint8_t *) pdf.data(), pdf.size()}, ctx, &error);
if (!first || !first->page_next) {
test::fail("Poppler: %s", error.message.c_str());
return;
}
CHECK(!first->data.empty());
CHECK(first->nominal_width == 64 && first->nominal_height == 32);
const dawn::Image &later = *first->page_next;
CHECK(later.data.empty() && later.render);
CHECK(!later.width && !later.height);
CHECK(later.nominal_width == 32 && later.nominal_height == 64);
CHECK(later.effective_profile && !later.profile_assumed);
dawn::ImagePtr rendered = later.render->render(ctx, 1., &error);
if (!rendered)
test::fail("Poppler: page 2: %s", error.message.c_str());
else
CHECK(rendered->width == 32 && rendered->height == 64 &&
!rendered->data.empty());
#endif
}
// The metafile measures itself in inches, so scale 1 is as many pixels as the
// screen puts in one, rather than libwmf's own 72 units per inch.
static void
test_wmf_dpi()
{
#if DAWN_WITH_LIBWMF
for (int dpi : {72, 144}) {
dawn::OpenContext ctx;
ctx.screen_dpi = dpi;
dawn::Error error;
dawn::ImagePtr image = dawn::load_libwmf(kWmf, ctx, &error);
if (!image) {
test::fail("WMF at %d DPI: %s", dpi, error.message.c_str());
continue;
}
const uint32_t scale = uint32_t(dpi) / 72;
if (image->width != 64 * scale || image->height != 32 * scale) {
test::fail(
"WMF at %d DPI: %ux%u", dpi, image->width, image->height);
}
}
#endif
}
static void
near_xy(const char *label, double x, double y, double xe, double ye, double tol)
{
if (fabs(x - xe) > tol || fabs(y - ye) > tol) {
test::fail(
"%s: xy=(%.5f, %.5f) expected (~%.5f, ~%.5f)", label, x, y, xe, ye);
}
}
static void
test_chromaticities()
{
CHECK(dawn::profile_chromaticities(nullptr).model ==
dawn::ColorModel::Unknown);
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
CHECK(srgb != nullptr);
dawn::Chromaticities s = dawn::profile_chromaticities(srgb.get());
CHECK(s.model == dawn::ColorModel::Rgb);
CHECK(s.have_primaries);
CHECK(s.n == 3);
near_xy("sRGB R", s.x[0], s.y[0], 0.6400, 0.3300, 0.002);
near_xy("sRGB G", s.x[1], s.y[1], 0.3000, 0.6000, 0.002);
near_xy("sRGB B", s.x[2], s.y[2], 0.1500, 0.0600, 0.002);
CHECK(s.have_white);
near_xy("sRGB W", s.wx, s.wy, 0.3127, 0.3290, 0.002);
auto display_p3 = cmm->get_profile_display_p3();
CHECK(display_p3 != nullptr);
CHECK(dawn::profile_transfer(display_p3.get()) == dawn::Transfer::Srgb);
dawn::Chromaticities d = dawn::profile_chromaticities(display_p3.get());
CHECK(d.have_primaries && d.n == 3);
near_xy("Display P3 R", d.x[0], d.y[0], 0.6800, 0.3200, 0.002);
near_xy("Display P3 G", d.x[1], d.y[1], 0.2650, 0.6900, 0.002);
near_xy("Display P3 B", d.x[2], d.y[2], 0.1500, 0.0600, 0.002);
double wp[2] = {0.3127, 0.3290};
double adobe[6] = {0.6400, 0.3300, 0.2100, 0.7100, 0.1500, 0.0600};
auto ad = cmm->get_profile_parametric(2.2, wp, adobe);
CHECK(ad != nullptr);
dawn::Chromaticities a = dawn::profile_chromaticities(ad.get());
CHECK(a.have_primaries && a.n == 3);
near_xy("Adobe G", a.x[1], a.y[1], 0.2100, 0.7100, 0.002);
dawn::ImagePtr red = load_fixture("red.png");
if (red) {
CHECK(red->effective_profile != nullptr);
CHECK(red->profile_assumed);
dawn::Chromaticities e =
dawn::profile_chromaticities(red->effective_profile.get());
CHECK(e.have_primaries && e.n == 3);
near_xy("assumed sRGB R", e.x[0], e.y[0], 0.6400, 0.3300, 0.002);
}
const fs::path p3 =
fs::path(DAWN_TEST_FIXTURES_DIR) / "display-p3-red_vs_srgb-red.png";
const vector<uint8_t> bytes = test::fixture(p3.filename().string());
if (!bytes.empty()) {
dawn::OpenContext ctx;
ctx.uri = dawn::path_to_uri(p3.string());
ctx.cmm = cmm;
ctx.first_frame_only = true;
dawn::Error error;
dawn::ImagePtr img = dawn::load_wuffs(bytes, ctx, &error);
if (img && img->effective_profile) {
CHECK(!img->profile_assumed);
dawn::Chromaticities p =
dawn::profile_chromaticities(img->effective_profile.get());
CHECK(p.have_primaries && p.n == 3);
near_xy("P3 R", p.x[0], p.y[0], 0.680, 0.320, 0.01);
near_xy("P3 G", p.x[1], p.y[1], 0.265, 0.690, 0.01);
} else {
test::fail("P3 effective_profile missing");
}
}
}
// Each fixture would come out with a different transfer function if the loader
// ranked its colour chunks differently. iCCP, which outranks all of these, is
// covered by the Display P3 file in test_chromaticities().
static void
test_png_colour_chunk(
const char *name, dawn::Transfer transfer, double red_x, double red_y)
{
dawn::ImagePtr image = load_fixture(name);
if (!image)
return;
CHECK(!image->profile_assumed);
if (dawn::profile_transfer(image->effective_profile.get()) != transfer)
test::fail("%s: unexpected transfer function", name);
dawn::Chromaticities c =
dawn::profile_chromaticities(image->effective_profile.get());
CHECK(c.have_primaries && c.n == 3);
if (c.n == 3)
near_xy(name, c.x[0], c.y[0], red_x, red_y, 0.002);
}
static void
test_png_colour_chunks()
{
// The first file also carries a gAMA of 1.0, which the sRGB chunk beats.
test_png_colour_chunk("srgb-chunk.png", dawn::Transfer::Srgb, 0.64, 0.33);
test_png_colour_chunk("gama22.png", dawn::Transfer::AdobeRgb, 0.64, 0.33);
test_png_colour_chunk("chrm-p3.png", dawn::Transfer::Srgb, 0.68, 0.32);
test_png_colour_chunk(
"chrm-p3-gama1.png", dawn::Transfer::Linear, 0.68, 0.32);
}
#if DAWN_WITH_LIBTIFF
static dawn::ImagePtr
load_tiff_fixture(const char *name, const shared_ptr<dawn::Cmm> &cmm,
const shared_ptr<dawn::Profile> &screen, vector<string> *warnings)
{
const vector<uint8_t> bytes = test::fixture(name);
if (bytes.empty())
return nullptr;
// The format dispatch would give load_tiff_ep() and LibRaw a go first.
dawn::OpenContext ctx;
ctx.uri =
dawn::path_to_uri((fs::path(DAWN_TEST_FIXTURES_DIR) / name).string());
ctx.cmm = cmm;
ctx.screen_profile = screen;
ctx.warnings = warnings;
dawn::Error error;
dawn::ImagePtr image = dawn::load_tiff(bytes, ctx, &error);
if (!image)
test::fail("%s: %s", name, error.message.c_str());
return image;
}
// A TIFF stating its colour numerically must not come out as invented sRGB.
static void
test_tiff_colour(
const char *name, dawn::Transfer transfer, double green_x, double green_y)
{
dawn::ImagePtr image =
load_tiff_fixture(name, dawn::Cmm::get_default(), nullptr, nullptr);
if (!image)
return;
CHECK(!image->profile_assumed);
if (dawn::profile_transfer(image->effective_profile.get()) != transfer)
test::fail("%s: unexpected transfer function", name);
dawn::Chromaticities c =
dawn::profile_chromaticities(image->effective_profile.get());
CHECK(c.have_primaries && c.n == 3);
if (c.n != 3)
return;
near_xy(name, c.x[0], c.y[0], 0.64, 0.33, 0.002);
near_xy(name, c.x[1], c.y[1], green_x, green_y, 0.002);
}
static void
test_tiff_colorimetry()
{
auto cmm = dawn::Cmm::get_default();
// Both decoding paths: 16-bit scanlines, and TIFFRGBAImage.
test_tiff_colour("adobergb16.tif", dawn::Transfer::Srgb, 0.21, 0.71);
test_tiff_colour("adobergb8.tif", dawn::Transfer::Srgb, 0.21, 0.71);
test_tiff_colour("exif-gamma.tif", dawn::Transfer::AdobeRgb, 0.21, 0.71);
test_tiff_colour("exif-srgb.tif", dawn::Transfer::Srgb, 0.30, 0.60);
test_tiff_colour("palette-transfer.tif", dawn::Transfer::Srgb, 0.21, 0.71);
// Chromaticities outside an RGB raster describe nothing about it, and
// the solid fixtures carry no colorimetry at all.
for (const char *name : {"grey-primaries.tif", "red.tif"}) {
dawn::ImagePtr image = load_tiff_fixture(name, cmm, nullptr, nullptr);
if (image)
CHECK(image->profile_assumed);
}
// A stated sRGB moves no pixel; a wider gamut has to move several.
auto srgb = cmm->get_profile_sRGB();
if (dawn::ImagePtr i =
load_tiff_fixture("exif-srgb.tif", cmm, srgb, nullptr))
expect_bgra("exif-srgb.tif", pixel0(*i), 96 * 257, 128 * 257, 192 * 257,
65535, 64);
if (dawn::ImagePtr i =
load_tiff_fixture("adobergb16.tif", cmm, srgb, nullptr)) {
// The red channel carries the conversion, and stays in gamut,
// so that clipping cannot stand in for it.
Pixel p = pixel0(*i);
if (near_u16(p.r, 192 * 257, 2000) || p.r >= 65535)
test::fail("adobergb16.tif: unconverted BGRA (%u,%u,%u,%u)", p.b,
p.g, p.r, p.a);
}
// TransferFunction tables, over the whole range the bit depth encodes.
const struct {
const char *name;
double gamma[3];
} tabulated[] = {
{"transfer8.tif", {1.0, 1.0, 1.0}},
{"transfer16.tif", {1.5, 2.0, 3.0}},
};
for (const auto &t : tabulated) {
dawn::ImagePtr image = load_tiff_fixture(t.name, cmm, nullptr, nullptr);
if (!image)
continue;
CHECK(!image->profile_assumed);
const auto e = dawn::profile_encoding(image->effective_profile.get());
CHECK(e.matrix_trc);
for (size_t c = 0; c < 3; c++)
CHECK(abs(e.decode[2048][c] - pow(.5, t.gamma[c])) < .001);
}
// A palette image's three curves cannot be had from libtiff, so the
// profile falls back to sRGB rather than repeating the one it gets.
if (dawn::ImagePtr i =
load_tiff_fixture("palette-transfer.tif", cmm, nullptr, nullptr)) {
const auto e = dawn::profile_encoding(i->effective_profile.get());
CHECK(e.matrix_trc);
for (size_t c = 0; c < 3; c++)
CHECK(abs(e.decode[2048][c] - .21404114) < .001);
}
// An unreadable Exif is missing metadata, not a lost file: every page
// must survive it, with its pixels, and the good one in the middle has
// to leave the page loop where it found it.
vector<string> warnings;
dawn::ImagePtr pages =
load_tiff_fixture("exif-pages.tif", cmm, nullptr, &warnings);
CHECK(!warnings.empty());
if (!pages)
return;
const struct {
const char *label;
uint16_t b, g, r;
bool assumed;
} expected[] = {
{"exif-pages.tif unreadable", 96 * 257, 128 * 257, 192 * 257, true},
{"exif-pages.tif valid", 192 * 257, 128 * 257, 96 * 257, false},
{"exif-pages.tif malformed", 96 * 257, 192 * 257, 128 * 257, true},
};
size_t page = 0;
for (dawn::Image *p = pages.get(); p; p = p->page_next.get(), page++) {
if (page >= size(expected)) {
test::fail("exif-pages.tif: more than %zu pages", size(expected));
break;
}
CHECK(p->profile_assumed == expected[page].assumed);
expect_bgra(expected[page].label, pixel0(*p), expected[page].b,
expected[page].g, expected[page].r, 65535, 0);
}
CHECK(page == size(expected));
}
#endif // DAWN_WITH_LIBTIFF
// Raw previews state no colour of their own, their container does.
static void
test_tiff_ep_colour()
{
auto cmm = dawn::Cmm::get_default();
dawn::OpenContext ctx;
ctx.cmm = cmm;
ctx.screen_profile = cmm->get_profile_sRGB();
const struct {
const char *name;
dawn::Transfer transfer;
double green_x, green_y;
bool converted;
} fixtures[] = {
{"nikon-srgb.nef", dawn::Transfer::Srgb, 0.30, 0.60, false},
{"nikon-adobergb.nef", dawn::Transfer::AdobeRgb, 0.21, 0.71, true},
{"preview-adobergb.tif", dawn::Transfer::AdobeRgb, 0.21, 0.71, true},
};
for (const auto &fixture : fixtures) {
dawn::Error error;
dawn::ImagePtr image =
dawn::load_tiff_ep(test::fixture(fixture.name), ctx, &error);
if (!image) {
test::fail("%s: %s", fixture.name, error.message.c_str());
continue;
}
CHECK(!image->profile_assumed);
if (dawn::profile_transfer(image->effective_profile.get()) !=
fixture.transfer)
test::fail("%s: unexpected transfer function", fixture.name);
dawn::Chromaticities c =
dawn::profile_chromaticities(image->effective_profile.get());
CHECK(c.have_primaries && c.n == 3);
if (c.n == 3)
near_xy(fixture.name, c.x[1], c.y[1], fixture.green_x,
fixture.green_y, 0.002);
// Only the red channel of the mid colour stays in gamut either way.
const uint16_t red = pixel0(*image).r;
if (near_u16(red, 192 * 257, 2 * 257) == fixture.converted ||
red >= 65535)
test::fail("%s: red %u", fixture.name, red);
}
}
static void
test_png_text_after_idat()
{
fs::path path = fs::path(DAWN_TEST_FIXTURES_DIR) / "text-after-idat.png";
const vector<uint8_t> bytes = test::fixture(path.filename().string());
dawn::OpenContext ctx;
ctx.uri = dawn::path_to_uri(path.string());
ctx.first_frame_only = false;
dawn::Error error;
dawn::ImagePtr image = dawn::open_from_data(bytes, ctx, &error);
if (!image) {
test::fail("text-after-idat: %s", error.message.c_str());
return;
}
CHECK(!error);
CHECK(image->width == 1 && image->height == 1);
auto it = image->text.find("prompt");
CHECK(it != image->text.end());
if (it != image->text.end())
CHECK(it->second == "hello");
}
static void
test_profile_transfer()
{
CHECK(dawn::profile_transfer(nullptr) == dawn::Transfer::Srgb);
auto cmm = dawn::Cmm::get_default();
auto srgb = cmm->get_profile_sRGB();
CHECK(srgb != nullptr);
CHECK(dawn::profile_transfer(srgb.get()) == dawn::Transfer::Srgb);
auto g22 = cmm->get_profile_sRGB_gamma(2.2);
CHECK(g22 != nullptr);
CHECK(dawn::profile_transfer(g22.get()) == dawn::Transfer::AdobeRgb);
auto lin = cmm->get_profile_sRGB_gamma(1.0);
CHECK(lin != nullptr);
CHECK(dawn::profile_transfer(lin.get()) == dawn::Transfer::Linear);
auto g18 = cmm->get_profile_sRGB_gamma(1.8);
CHECK(g18 != nullptr);
CHECK(dawn::profile_transfer(g18.get()) == dawn::Transfer::Srgb);
}
static void
test_profile_encoding()
{
auto cmm = make_shared<dawn::Cmm>();
for (double gamma : {1.0, 1.8, 2.2, 2.6}) {
auto profile = cmm->get_profile_sRGB_gamma(gamma);
const auto encoding = dawn::profile_encoding(profile.get());
CHECK(encoding.matrix_trc);
CHECK(abs(encoding.decode[2048][0] - pow(.5, gamma)) < .0001);
CHECK(abs(encoding.encode[2048][0] - pow(.5, 1 / gamma)) < .0001);
}
const auto srgb = dawn::profile_encoding(cmm->get_profile_sRGB().get());
const auto p3 = dawn::profile_encoding(cmm->get_profile_display_p3().get());
CHECK(srgb.matrix_trc);
CHECK(p3.matrix_trc);
CHECK(abs(srgb.decode[2048][0] - .21404114) < .0001);
CHECK(abs(srgb.encode[2048][0] - .73535698) < .0001);
CHECK(abs(p3.decode[2048][0] - srgb.decode[2048][0]) < .0001);
CHECK(abs(p3.encode[2048][0] - srgb.encode[2048][0]) < .0001);
CHECK(abs(p3.rgb_to_xyz[0][0] - srgb.rgb_to_xyz[0][0]) > .05);
CHECK(!dawn::profile_encoding(nullptr).matrix_trc);
// Mixed TRCs must not collapse to a single guessed gamma.
cmsHPROFILE mixed = cmsCreate_sRGBProfile();
cmsSetProfileVersion(mixed, 4.3);
const cmsTagSignature tags[] = {
cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag};
const double gammas[] = {1.3, 1.8, 2.6};
for (int c = 0; c < 3; c++) {
cmsToneCurve *curve = cmsBuildGamma(nullptr, gammas[c]);
CHECK(cmsWriteTag(mixed, tags[c], curve));
cmsFreeToneCurve(curve);
}
auto import = [&] {
cmsUInt32Number size = 0;
CHECK(cmsSaveProfileToMem(mixed, nullptr, &size));
vector<uint8_t> bytes(size);
CHECK(cmsSaveProfileToMem(mixed, bytes.data(), &size));
return cmm->get_profile(bytes);
};
const auto curves = dawn::profile_encoding(import().get());
CHECK(curves.matrix_trc);
for (size_t c = 0; c < 3; c++) {
CHECK(abs(curves.decode[2048][c] - pow(.5, gammas[c])) < .0001);
CHECK(abs(curves.encode[2048][c] - pow(.5, 1 / gammas[c])) < .0001);
}
// Even with valid matrix/TRC tags, a LUT profile is explicitly approximate.
cmsSetProfileVersion(mixed, 2.1);
cmsPipeline *lut = cmsPipelineAlloc(nullptr, 3, 3);
cmsUInt16Number table[24] = {};
CHECK(cmsPipelineInsertStage(
lut, cmsAT_END, cmsStageAllocCLut16bit(nullptr, 2, 3, 3, table)));
CHECK(cmsWriteTag(mixed, cmsSigAToB0Tag, lut));
cmsPipelineFree(lut);
const auto fallback = dawn::profile_encoding(import().get());
CHECK(!fallback.matrix_trc);
CHECK(fallback.decode == dawn::profile_encoding(nullptr).decode);
cmsCloseProfile(mixed);
}
// White lands on white, and each display colourant on its own primary.
static void
test_display_matrices()
{
auto cmm = make_shared<dawn::Cmm>();
const auto srgb = dawn::profile_encoding(cmm->get_profile_sRGB().get());
const auto p3 = dawn::profile_encoding(cmm->get_profile_display_p3().get());
const double srgb_xy[6] = {.64, .33, .30, .60, .15, .06};
const double bt2020_xy[6] = {.708, .292, .170, .797, .131, .046};
const dawn::RgbMatrix colourants = dawn::display_colourants_d65(srgb);
for (size_t c = 0; c < 3; c++) {
const double sum =
colourants[c][0] + colourants[c][1] + colourants[c][2];
CHECK(abs(colourants[c][0] / sum - srgb_xy[c * 2]) < .001);
CHECK(abs(colourants[c][1] / sum - srgb_xy[c * 2 + 1]) < .001);
}
const dawn::RgbMatrix identity = dawn::display_to_primaries(srgb, srgb_xy);
for (size_t c = 0; c < 3; c++)
for (size_t r = 0; r < 3; r++)
CHECK(abs(identity[c][r] - (c == r)) < .002);
// Display P3 red leaves sRGB, as scRGB lets it, but not BT.2020, whose
// conversion from P3 is only a hair negative, in blue from red.
const dawn::RgbMatrix to_srgb = dawn::display_to_primaries(p3, srgb_xy);
CHECK(to_srgb[0][0] > 1.1 && to_srgb[0][1] < 0 && to_srgb[0][2] < 0);
const dawn::RgbMatrix to_bt2020 = dawn::display_to_primaries(p3, bt2020_xy);
for (size_t r = 0; r < 3; r++) {
double white = 0;
for (size_t c = 0; c < 3; c++) {
CHECK(to_bt2020[c][r] > -.002);
white += to_bt2020[c][r];
}
CHECK(abs(white - 1) < .002);
}
}
int
main()
{
return test::run({
{"packing", test_pack_helpers},
{"unpremultiply alpha last", test_unpremultiply_alpha_last8},
{"finishing", test_finishing},
{"solid image loaders", test_loaders_solid},
{"JPEG CMS", test_jpeg_cms_8_to_16},
{"JPEG fatal error", test_jpeg_fatal_error},
{"CMYK CMS", test_cmyk_cms_opaque},
{"tiled CMS", test_cms_tiled},
{"RGBW image", test_rgbw_2x2},
{"premultiplied alpha", test_premul_alpha},
{"large ICC profile", test_large_icc_and_p3_red},
{"SVG rendering", test_svg},
{"render dimensions", test_render_dimensions},
{"vector rerendering", test_vector_rerender},
{"PDF later pages", test_pdf_later_pages},
{"WMF display size", test_wmf_dpi},
{"chromaticities", test_chromaticities},
{"PNG text", test_png_text_after_idat},
{"PNG colour chunks", test_png_colour_chunks},
#if DAWN_WITH_LIBTIFF
{"TIFF colorimetry", test_tiff_colorimetry},
#endif
{"TIFF/EP colour", test_tiff_ep_colour},
{"profile transfer", test_profile_transfer},
{"profile encoding", test_profile_encoding},
{"display matrices", test_display_matrices},
});
}