/*
* OpenSlide, a library for reading whole slide image files
*
* Copyright (c) 2007-2012 Carnegie Mellon University
* Copyright (c) 2021-2022 Benjamin Gilbert
* All rights reserved.
*
* OpenSlide is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, version 2.1.
*
* OpenSlide is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with OpenSlide. If not, see
* .
*
*/
#include
#include "openslide-private.h"
#include "openslide-decode-tifflike.h"
#include
#include
#include
#include
#include
#include
#include "openslide-error.h"
const char _openslide_release_info[] = "OpenSlide " SUFFIXED_VERSION ", copyright (C) 2007-2024 Carnegie Mellon University and others.\nLicensed under the GNU Lesser General Public License, version 2.1.";
static const char * const EMPTY_STRING_ARRAY[] = { NULL };
static const struct _openslide_format *formats[] = {
&_openslide_format_synthetic,
&_openslide_format_mirax,
&_openslide_format_zeiss,
&_openslide_format_dicom,
&_openslide_format_hamamatsu_vms_vmu,
&_openslide_format_hamamatsu_ndpi,
&_openslide_format_sakura,
&_openslide_format_trestle,
&_openslide_format_aperio,
&_openslide_format_leica,
&_openslide_format_philips_tiff,
&_openslide_format_ventana,
&_openslide_format_generic_tiff,
NULL,
};
static bool openslide_was_dynamically_loaded;
// called from shared-library constructor!
static void __attribute__((constructor)) _openslide_init(void) {
// init libxml2
xmlInitParser();
// parse debug options
_openslide_debug_init();
openslide_was_dynamically_loaded = true;
}
static void destroy_associated_image(gpointer data) {
struct _openslide_associated_image *img = data;
img->ops->destroy(img);
}
static bool level_in_range(openslide_t *osr, int32_t level) {
if (level < 0) {
return false;
}
if (level > osr->level_count - 1) {
return false;
}
return true;
}
// pixman 0.38.x produces corrupt output. Test for this at runtime, since
// we might have been compiled with a different version, and the distro
// might have backported a fix.
// https://github.com/openslide/openslide/issues/278
// https://gitlab.freedesktop.org/pixman/pixman/-/commit/8256c235
static void *verify_pixman_works(void *arg G_GNUC_UNUSED) {
const int DIM = 16;
g_autofree uint32_t *dest = g_new0(uint32_t, DIM * DIM);
g_autofree uint32_t *src = g_new(uint32_t, DIM * DIM);
memset(src, 0xff, DIM * DIM * 4);
{
g_autoptr(cairo_surface_t) dest_surface =
cairo_image_surface_create_for_data((unsigned char *) dest,
CAIRO_FORMAT_ARGB32,
DIM, DIM, DIM * 4);
g_autoptr(cairo_t) cr = cairo_create(dest_surface);
// important
cairo_set_operator(cr, CAIRO_OPERATOR_SATURATE);
g_autoptr(cairo_surface_t) src_surface =
cairo_image_surface_create_for_data((unsigned char *) src,
CAIRO_FORMAT_ARGB32,
DIM, DIM, DIM * 4);
// fractional Y is important
cairo_set_source_surface(cr, src_surface, 0, 0.2);
cairo_paint(cr);
}
// white pixel if working, transparent if broken
return GINT_TO_POINTER(dest[8 * 16 + 8] != 0);
}
static const struct _openslide_format *detect_format(const char *filename,
struct _openslide_tifflike **tl_OUT) {
GError *tmp_err = NULL;
g_autoptr(_openslide_tifflike) tl =
_openslide_tifflike_create(filename, &tmp_err);
if (!tl) {
if (_openslide_debug(OPENSLIDE_DEBUG_DETECTION)) {
g_message("tifflike: %s", tmp_err->message);
}
g_clear_error(&tmp_err);
}
for (const struct _openslide_format **cur = formats; *cur; cur++) {
const struct _openslide_format *format = *cur;
g_assert(format->name && format->vendor &&
format->detect && format->open);
if (format->detect(filename, tl, &tmp_err)) {
// success!
if (tl_OUT) {
*tl_OUT = g_steal_pointer(&tl);
}
return format;
}
// reset for next format
if (_openslide_debug(OPENSLIDE_DEBUG_DETECTION)) {
g_message("%s: %s", format->name, tmp_err->message);
}
g_clear_error(&tmp_err);
}
// no match
return NULL;
}
static bool open_backend(openslide_t *osr,
const struct _openslide_format *format,
const char *filename,
struct _openslide_tifflike *tl,
struct _openslide_hash *quickhash1,
GError **err) {
if (!format->open(osr, filename, tl, quickhash1, err)) {
if (err && !*err) {
// error-handling bug in open function
g_warning("%s opener failed without setting error", format->name);
// assume the worst
g_set_error(err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_FAILED,
"Unknown error");
}
return false;
}
if (err && *err) {
// error-handling bug in open function
g_warning("%s opener succeeded but set error", format->name);
return false;
}
return true;
}
const char *openslide_detect_vendor(const char *filename) {
g_assert(openslide_was_dynamically_loaded);
const struct _openslide_format *format = detect_format(filename, NULL);
if (!format) {
return NULL;
}
return format->vendor;
}
static int cmpstring(const void *p1, const void *p2) {
return strcmp(* (char * const *) p1, * (char * const *) p2);
}
static const char **strv_from_hashtable_keys(GHashTable *h) {
guint size;
const char **result = (const char **) g_hash_table_get_keys_as_array(h,
&size);
qsort(result, size, sizeof(char *), cmpstring);
return result;
}
openslide_t *openslide_open(const char *filename) {
g_assert(openslide_was_dynamically_loaded);
// detect format
g_autoptr(_openslide_tifflike) tl = NULL;
const struct _openslide_format *format = detect_format(filename, &tl);
if (!format) {
// not a slide file
return NULL;
}
// alloc memory
g_autoptr(openslide_t) osr = g_new0(openslide_t, 1);
osr->properties = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free, g_free);
osr->associated_images = g_hash_table_new_full(g_str_hash, g_str_equal,
g_free,
destroy_associated_image);
// refuse to run on unpatched pixman 0.38.x
static GOnce pixman_once = G_ONCE_INIT;
g_once(&pixman_once, verify_pixman_works, NULL);
if (!GPOINTER_TO_INT(pixman_once.retval)) {
GError *tmp_err = NULL;
g_set_error(&tmp_err, OPENSLIDE_ERROR, OPENSLIDE_ERROR_FAILED,
"pixman 0.38.x does not render correctly; upgrade or downgrade pixman");
_openslide_propagate_error(osr, tmp_err);
return g_steal_pointer(&osr);
}
// open backend
g_autoptr(_openslide_hash) quickhash1 = _openslide_hash_quickhash1_create();
GError *tmp_err = NULL;
if (!open_backend(osr, format, filename, tl, quickhash1, &tmp_err)) {
// failed to read slide
_openslide_propagate_error(osr, tmp_err);
return g_steal_pointer(&osr);
}
g_assert(osr->levels);
// compute downsamples if not done already
int64_t blw, blh;
openslide_get_level0_dimensions(osr, &blw, &blh);
if (osr->level_count && osr->levels[0]->downsample == 0) {
osr->levels[0]->downsample = 1.0;
}
for (int32_t i = 1; i < osr->level_count; i++) {
struct _openslide_level *l = osr->levels[i];
if (l->downsample == 0) {
l->downsample =
(((double) blh / (double) l->h) +
((double) blw / (double) l->w)) / 2.0;
}
}
// check downsamples
for (int32_t i = 1; i < osr->level_count; i++) {
//g_debug("downsample: %g", osr->levels[i]->downsample);
if (osr->levels[i]->downsample < osr->levels[i - 1]->downsample) {
g_warning("Downsampled images not correctly ordered: %g < %g",
osr->levels[i]->downsample, osr->levels[i - 1]->downsample);
return NULL;
}
}
// set hash property
const char *hash_str = _openslide_hash_get_string(quickhash1);
if (hash_str != NULL) {
g_hash_table_insert(osr->properties,
g_strdup(OPENSLIDE_PROPERTY_NAME_QUICKHASH1),
g_strdup(hash_str));
}
// set other properties
g_hash_table_insert(osr->properties,
g_strdup(OPENSLIDE_PROPERTY_NAME_VENDOR),
g_strdup(format->vendor));
if (osr->icc_profile_size) {
g_hash_table_insert(osr->properties,
g_strdup(OPENSLIDE_PROPERTY_NAME_ICC_SIZE),
g_strdup_printf("%"PRId64, osr->icc_profile_size));
}
g_hash_table_insert(osr->properties,
g_strdup(_OPENSLIDE_PROPERTY_NAME_LEVEL_COUNT),
g_strdup_printf("%d", osr->level_count));
bool should_have_geometry = false; // initialize for gcc 4.4
for (int32_t i = 0; i < osr->level_count; i++) {
struct _openslide_level *l = osr->levels[i];
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_LEVEL_WIDTH, i),
g_strdup_printf("%"PRId64, l->w));
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_LEVEL_HEIGHT, i),
g_strdup_printf("%"PRId64, l->h));
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_LEVEL_DOWNSAMPLE, i),
_openslide_format_double(l->downsample));
// tile geometry
bool have_geometry = (l->tile_w > 0 && l->tile_h > 0);
if (i == 0) {
should_have_geometry = have_geometry;
}
if (have_geometry != should_have_geometry) {
g_warning("Inconsistent tile geometry hints between levels");
}
if (have_geometry) {
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_LEVEL_TILE_WIDTH, i),
g_strdup_printf("%"PRId64, l->tile_w));
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_LEVEL_TILE_HEIGHT, i),
g_strdup_printf("%"PRId64, l->tile_h));
}
}
// fill in associated image names and set properties
osr->associated_image_names = strv_from_hashtable_keys(osr->associated_images);
for (const char **name = osr->associated_image_names; *name != NULL; name++) {
struct _openslide_associated_image *img =
g_hash_table_lookup(osr->associated_images, *name);
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_ASSOCIATED_WIDTH, *name),
g_strdup_printf("%"PRId64, img->w));
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_ASSOCIATED_HEIGHT, *name),
g_strdup_printf("%"PRId64, img->h));
if (img->icc_profile_size) {
g_hash_table_insert(osr->properties,
g_strdup_printf(_OPENSLIDE_PROPERTY_NAME_TEMPLATE_ASSOCIATED_ICC_SIZE, *name),
g_strdup_printf("%"PRId64, img->icc_profile_size));
}
}
// ensure NULL values don't leak into properties
GHashTableIter iter;
char *name;
char *value;
g_hash_table_iter_init(&iter, osr->properties);
while (g_hash_table_iter_next(&iter, (void *) &name, (void *) &value)) {
if (!value) {
g_warning("Property \"%s\" has NULL value", name);
g_hash_table_iter_remove(&iter);
}
}
// fill in property names
osr->property_names = strv_from_hashtable_keys(osr->properties);
// start cache if the backend hasn't already done it
if (!osr->cache) {
osr->cache = _openslide_cache_binding_create(DEFAULT_CACHE_SIZE);
}
return g_steal_pointer(&osr);
}
void openslide_close(openslide_t *osr) {
if (osr->ops) {
(osr->ops->destroy)(osr);
}
g_hash_table_unref(osr->associated_images);
g_hash_table_unref(osr->properties);
g_free(osr->associated_image_names);
g_free(osr->property_names);
if (osr->cache) {
_openslide_cache_binding_destroy(osr->cache);
}
g_free(g_atomic_pointer_get(&osr->error));
g_free(osr);
}
void openslide_get_level0_dimensions(openslide_t *osr,
int64_t *w, int64_t *h) {
openslide_get_level_dimensions(osr, 0, w, h);
}
void openslide_get_level_dimensions(openslide_t *osr, int32_t level,
int64_t *w, int64_t *h) {
*w = -1;
*h = -1;
if (openslide_get_error(osr)) {
return;
}
if (!level_in_range(osr, level)) {
return;
}
*w = osr->levels[level]->w;
*h = osr->levels[level]->h;
}
int32_t openslide_get_level_count(openslide_t *osr) {
if (openslide_get_error(osr)) {
return -1;
}
return osr->level_count;
}
int32_t openslide_get_best_level_for_downsample(openslide_t *osr,
double downsample) {
if (openslide_get_error(osr)) {
return -1;
}
// too small, return first
if (downsample < osr->levels[0]->downsample) {
return 0;
}
// find where we are in the middle
for (int32_t i = 1; i < osr->level_count; i++) {
if (downsample < osr->levels[i]->downsample) {
return i - 1;
}
}
// too big, return last
return osr->level_count - 1;
}
double openslide_get_level_downsample(openslide_t *osr, int32_t level) {
if (openslide_get_error(osr) || !level_in_range(osr, level)) {
return -1.0;
}
return osr->levels[level]->downsample;
}
static bool read_region_area(openslide_t *osr,
uint32_t *dest, int64_t stride,
int64_t x, int64_t y,
int32_t level,
int64_t w, int64_t h,
GError **err) {
// create the cairo surface for the dest
g_autoptr(cairo_surface_t) surface = NULL;
if (dest) {
surface =
cairo_image_surface_create_for_data((unsigned char *) dest,
CAIRO_FORMAT_ARGB32,
w, h, stride);
} else {
// nil surface
surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 0, 0);
}
// create the cairo context
g_autoptr(cairo_t) cr = cairo_create(surface);
// saturate those seams away!
cairo_set_operator(cr, CAIRO_OPERATOR_SATURATE);
if (level_in_range(osr, level)) {
struct _openslide_level *l = osr->levels[level];
// offset if given negative coordinates
double ds = l->downsample;
int64_t tx = 0;
int64_t ty = 0;
if (x < 0) {
tx = (-x) / ds;
x = 0;
w -= tx;
}
if (y < 0) {
ty = (-y) / ds;
y = 0;
h -= ty;
}
cairo_translate(cr, tx, ty);
// paint
if (w > 0 && h > 0) {
if (!osr->ops->paint_region(osr, cr, x, y, l, w, h, err)) {
return false;
}
}
}
// done
if (!_openslide_check_cairo_status(cr, err)) {
return false;
}
return true;
}
void openslide_read_region(openslide_t *osr,
uint32_t *dest,
int64_t x, int64_t y,
int32_t level,
int64_t w, int64_t h) {
if (w < 0 || h < 0) {
GError *tmp_err = g_error_new(OPENSLIDE_ERROR, OPENSLIDE_ERROR_FAILED,
"negative width (%"PRId64") "
"or negative height (%"PRId64") "
"not allowed", w, h);
_openslide_propagate_error(osr, tmp_err);
return;
}
// clear the dest
if (dest) {
memset(dest, 0, w * h * 4);
}
// now that it's cleared, return if an error occurred
if (openslide_get_error(osr)) {
return;
}
// Break the work into smaller pieces if the region is large, because:
// 1. Cairo will not allow surfaces larger than 32767 pixels on a side.
// 2. cairo_image_surface_create_for_data() creates a surface backed by a
// pixman_image_t, and Pixman requires that every byte of that image
// be addressable in 31 bits.
const int64_t d = 4096;
double ds = openslide_get_level_downsample(osr, level);
for (int64_t row = 0; row < (h + d - 1) / d; row++) {
for (int64_t col = 0; col < (w + d - 1) / d; col++) {
// calculate surface coordinates and size
int64_t sx = x + col * d * ds; // level 0 plane
int64_t sy = y + row * d * ds; // level 0 plane
int64_t sw = MIN(w - col * d, d); // level plane
int64_t sh = MIN(h - row * d, d); // level plane
// paint
GError *tmp_err = NULL;
if (!read_region_area(osr,
dest ? dest + w * row * d + col * d : NULL, w * 4,
sx, sy, level, sw, sh,
&tmp_err)) {
_openslide_propagate_error(osr, tmp_err);
if (dest) {
// ensure we don't return a partial result
memset(dest, 0, w * h * 4);
}
return;
}
}
}
}
const char * const *openslide_get_property_names(openslide_t *osr) {
if (openslide_get_error(osr)) {
return EMPTY_STRING_ARRAY;
}
return osr->property_names;
}
const char *openslide_get_property_value(openslide_t *osr, const char *name) {
if (openslide_get_error(osr)) {
return NULL;
}
return g_hash_table_lookup(osr->properties, name);
}
int64_t openslide_get_icc_profile_size(openslide_t *osr) {
if (openslide_get_error(osr)) {
return -1;
}
return osr->icc_profile_size;
}
void openslide_read_icc_profile(openslide_t *osr, void *dest) {
if (openslide_get_error(osr)) {
memset(dest, 0, osr->icc_profile_size);
return;
}
if (!osr->icc_profile_size) {
return;
}
g_assert(osr->ops->read_icc_profile);
GError *tmp_err = NULL;
if (!osr->ops->read_icc_profile(osr, dest, &tmp_err)) {
_openslide_propagate_error(osr, tmp_err);
memset(dest, 0, osr->icc_profile_size);
}
}
const char * const *openslide_get_associated_image_names(openslide_t *osr) {
if (openslide_get_error(osr)) {
return EMPTY_STRING_ARRAY;
}
return osr->associated_image_names;
}
void openslide_get_associated_image_dimensions(openslide_t *osr, const char *name,
int64_t *w, int64_t *h) {
*w = -1;
*h = -1;
if (openslide_get_error(osr)) {
return;
}
struct _openslide_associated_image *img = g_hash_table_lookup(osr->associated_images,
name);
if (img) {
*w = img->w;
*h = img->h;
}
}
void openslide_read_associated_image(openslide_t *osr,
const char *name,
uint32_t *dest) {
struct _openslide_associated_image *img =
g_hash_table_lookup(osr->associated_images, name);
if (!img) {
return;
}
size_t pixels = img->w * img->h;
if (openslide_get_error(osr)) {
memset(dest, 0, pixels * sizeof(uint32_t));
return;
}
GError *tmp_err = NULL;
if (!img->ops->get_argb_data(img, dest, &tmp_err)) {
_openslide_propagate_error(osr, tmp_err);
// ensure we don't return a partial result
memset(dest, 0, pixels * sizeof(uint32_t));
}
}
int64_t openslide_get_associated_image_icc_profile_size(openslide_t *osr,
const char *name) {
if (openslide_get_error(osr)) {
return -1;
}
struct _openslide_associated_image *img =
g_hash_table_lookup(osr->associated_images, name);
if (!img) {
return -1;
}
return img->icc_profile_size;
}
void openslide_read_associated_image_icc_profile(openslide_t *osr,
const char *name,
void *dest) {
struct _openslide_associated_image *img =
g_hash_table_lookup(osr->associated_images, name);
if (!img) {
return;
}
if (openslide_get_error(osr)) {
memset(dest, 0, img->icc_profile_size);
return;
}
if (!img->icc_profile_size) {
return;
}
g_assert(img->ops->read_icc_profile);
GError *tmp_err = NULL;
if (!img->ops->read_icc_profile(img, dest, &tmp_err)) {
_openslide_propagate_error(osr, tmp_err);
memset(dest, 0, img->icc_profile_size);
}
}
openslide_cache_t *openslide_cache_create(size_t capacity) {
return _openslide_cache_create(capacity);
}
void openslide_set_cache(openslide_t *osr, openslide_cache_t *cache) {
if (openslide_get_error(osr)) {
return;
}
_openslide_cache_binding_set(osr->cache, cache);
}
void openslide_cache_release(openslide_cache_t *cache) {
_openslide_cache_release(cache);
}
const char *openslide_get_version(void) {
return SUFFIXED_VERSION;
}