Replace Glycin and GdkPixbuf

From one kind of pain to another kind of pain.
This commit is contained in:
p
2026-09-08 21:27:37 +02:00
parent fc80afd810
commit 65885e47e7
29 changed files with 2414 additions and 565 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- name: Install dependencies
run: |
brew install cmake pkgconf glslang molten-vk qtbase resvg \
brew install cmake pkgconf glslang molten-vk qtbase resvg rust \
shared-mime-info vulkan-loader jpeg-turbo webp zlib
- name: Build package
+1
View File
@@ -2,5 +2,6 @@
/.flatpak/
/build/
/build-*/
/libdnrs/Cargo.lock
compile_commands.json
.cache/
+17 -26
View File
@@ -110,12 +110,20 @@ dawn_optional_pkg(LIBHEIF "HEIF/AVIF support" libheif)
dawn_optional_pkg(LIBJXL "JPEG XL support" libjxl)
dawn_optional_pkg(OPENJPEG "JPEG 2000 support" libopenjp2)
dawn_optional_pkg(LIBTIFF "TIFF support" libtiff-4)
dawn_optional_pkg(GLYCIN "glycin fallback" glycin-2)
dawn_optional_pkg(GDKPIXBUF "gdk-pixbuf fallback" gdk-pixbuf-2.0)
dawn_optional_pkg(LIBRAW "LibRaw support" libraw)
dawn_optional_pkg(LIBWMF "WMF support" libwmf)
# jxrlib ships no pkg-config file, and its headers include one another by
# bare name, so the whole package directory has to go on the include path.
find_program(CARGO_EXECUTABLE cargo)
if (CARGO_EXECUTABLE)
set(CARGO_FOUND TRUE)
endif()
option(DAWN_WITH_DNRS "Rust image loader" ${CARGO_FOUND})
if (DAWN_WITH_DNRS AND NOT CARGO_FOUND)
message(FATAL_ERROR "DAWN_WITH_DNRS=ON but cargo was not found")
endif()
# jxrlib ships no pkg-config file, and its headers include one another
# by bare name, so the whole package directory has to go on the include path.
find_path(JXRLIB_INCLUDE_DIR JXRGlue.h PATH_SUFFIXES jxrlib)
find_library(JXRLIB_GLUE_LIBRARY jxrglue)
find_library(JXRLIB_CORE_LIBRARY jpegxr)
@@ -128,7 +136,7 @@ if (DAWN_WITH_JXRLIB AND NOT JXRLIB_FOUND)
endif()
if (APPLE)
option(DAWN_WITH_IMAGEIO "ImageIO fallback" ON)
option(DAWN_WITH_IMAGEIO "ImageIO loader" ON)
option(DAWN_WITH_CGPDF "Core Graphics PDF page rendering" ON)
endif()
@@ -191,7 +199,7 @@ else()
target_include_directories(lcms2_threaded PUBLIC
$<BUILD_INTERFACE:${lcms2_SOURCE_DIR}/plugins/threaded/include>)
# FIXME: LibRaw, GdkPixbuf, Glycin may transitively link to system lcms2.
# FIXME: LibRaw may transitively link to system lcms2.
# What gives? Clear in LINK_INTERFACE_LIBRARIES?
target_link_libraries(dawn-lcms2 INTERFACE
lcms2 lcms2_fast_float lcms2_threaded)
@@ -244,6 +252,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
$<$<COMPILE_LANGUAGE:CXX>:-Wnon-virtual-dtor>)
endif()
if (DAWN_WITH_DNRS)
add_subdirectory(libdnrs)
endif()
add_subdirectory(libdn)
add_subdirectory(dnimaged)
add_subdirectory(dnthumbd)
@@ -304,26 +315,6 @@ else()
"${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml" @ONLY)
install(FILES "${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml"
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
# Only meaningful once dn is in place, hence here rather than in dn/,
# which is processed before the rules above. A staged install cannot run
# the binary it just staged, so packages ship the updater to run later.
string(CONFIGURE [[
if (NOT "$ENV{DESTDIR}" STREQUAL "")
message(STATUS "Staged install: run dn-update-desktop-files on target")
else()
message(STATUS "Updating desktop file media types")
execute_process(
COMMAND "@DN_DESKTOP_UPDATER@"
RESULT_VARIABLE dn_desktop_result)
if (NOT dn_desktop_result EQUAL 0)
message(WARNING "dn-update-desktop-files: ${dn_desktop_result}")
endif()
endif()
]] dn_desktop_hook @ONLY)
if (DN_DESKTOP_UPDATER AND NOT CMAKE_CROSSCOMPILING)
install(CODE "${dn_desktop_hook}")
endif()
endif()
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
+24
View File
@@ -0,0 +1,24 @@
#
# desktop-media-types.cmake: fill in a desktop file's MimeType= from dn
#
# Copyright The Dawn Authors
# SPDX-License-Identifier: MPL-2.0
#
# Usage: cmake [-DEMULATOR=wine] -DDN=dn -DINPUT=in -DOUTPUT=out
# -P desktop-media-types.cmake
#
execute_process(COMMAND ${EMULATOR} "${DN}" --list-supported-media-types
OUTPUT_VARIABLE types OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE result)
if (NOT result EQUAL 0 OR NOT types)
message(FATAL_ERROR "${DN} reported no media types (${result})")
endif()
# Desktop entries separate and terminate list values with semicolons,
# which is also how CMake joins the elements of a list.
string(REPLACE "\n" ";" types "${types}")
file(READ "${INPUT}" contents)
string(REGEX REPLACE "MimeType=[^\n]*" "MimeType=${types};" contents
"${contents}")
file(WRITE "${OUTPUT}" "${contents}")
+2 -2
View File
@@ -22,6 +22,7 @@
#cmakedefine01 DAWN_WITH_LCMS2_THREADED
#cmakedefine01 DAWN_WITH_JPEG_QS
#cmakedefine01 DAWN_WITH_LIBRAW
#cmakedefine01 DAWN_WITH_LIBWMF
#cmakedefine01 DAWN_WITH_LIBRSVG
#cmakedefine01 DAWN_WITH_XCURSOR
#cmakedefine01 DAWN_WITH_LIBHEIF
@@ -29,8 +30,7 @@
#cmakedefine01 DAWN_WITH_OPENJPEG
#cmakedefine01 DAWN_WITH_LIBTIFF
#cmakedefine01 DAWN_WITH_JXRLIB
#cmakedefine01 DAWN_WITH_GLYCIN
#cmakedefine01 DAWN_WITH_GDKPIXBUF
#cmakedefine01 DAWN_WITH_DNRS
#cmakedefine01 DAWN_WITH_IMAGEIO
#cmakedefine01 DAWN_WITH_CGPDF
+23 -22
View File
@@ -146,9 +146,9 @@ if (APPLE)
# The abstract type the vendor formats conform to.
list(APPEND utis public.camera-raw-image)
endif()
if (DAWN_WITH_GDKPIXBUF)
if (DAWN_WITH_DNRS)
# The rest of what it adds, ImageIO knows about as well.
list(APPEND utis
com.apple.quicktime-image
com.microsoft.ico
public.xbitmap-image
)
@@ -230,28 +230,29 @@ else()
RENAME "${DAWN_NAMESPACE}.svg")
install(DIRECTORY "${DN_ICON_BASE}"
DESTINATION "${CMAKE_INSTALL_DATADIR}")
install(FILES
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.desktop"
# MimeType= follows the loader table compiled into dn, so ask it.
# A cross build can only ask through an emulator; without one,
# the checked-in list is installed as it is.
set(desktop "${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.desktop")
if (NOT CMAKE_CROSSCOMPILING OR CMAKE_CROSSCOMPILING_EMULATOR)
set(generated
"${CMAKE_CURRENT_BINARY_DIR}/${DAWN_NAMESPACE}.desktop")
add_custom_command(OUTPUT "${generated}"
COMMAND ${CMAKE_COMMAND}
"-DEMULATOR=${CMAKE_CROSSCOMPILING_EMULATOR}"
-DDN=$<TARGET_FILE:dn>
"-DINPUT=${desktop}" "-DOUTPUT=${generated}"
-P "${PROJECT_SOURCE_DIR}/cmake/desktop-media-types.cmake"
DEPENDS dn "${desktop}"
COMMENT "Filling in desktop file media types"
VERBATIM)
add_custom_target(desktop-media-types ALL DEPENDS "${generated}")
set(desktop "${generated}")
endif()
install(FILES "${desktop}"
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.Browse.desktop"
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
# The desktop file's MimeType= cannot be filled in at build time:
# it depends on gdk-pixbuf loaders installed on the target.
# The parent list runs this after dn itself is installed.
set(DN_DESKTOP_UPDATER
"${CMAKE_CURRENT_BINARY_DIR}/dn-update-desktop-files")
# The parent list runs this script directly, so it needs to be
# executable, which configure_file() does not make it.
configure_file(dn-update-desktop-files.in
"${DN_DESKTOP_UPDATER}" @ONLY
FILE_PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
install(PROGRAMS "${DN_DESKTOP_UPDATER}"
DESTINATION "${CMAKE_INSTALL_BINDIR}")
set(DN_DESKTOP_UPDATER "${DN_DESKTOP_UPDATER}" PARENT_SCOPE)
endif()
endif()
-20
View File
@@ -1,20 +0,0 @@
#!/bin/sh -e
# Point the installed desktop file's MimeType= at what dn can actually open.
#
# The list is only partly fixed at build time: glycin and gdk-pixbuf each
# contribute whatever loaders are installed on this machine. Package managers
# want to run this after unpacking, and again whenever those change.
dn='@CMAKE_INSTALL_FULL_BINDIR@/dn'
desktop='@CMAKE_INSTALL_FULL_DATADIR@/applications/@DAWN_NAMESPACE@.desktop'
types=$("$dn" --list-supported-media-types | tr '\n' ';')
if [ -z "$types" ]
then
echo "$0: $dn reported no media types" >&2
exit 1
fi
tmp=$desktop.$$
trap 'rm -f "$tmp"' EXIT
sed "s|^MimeType=.*|MimeType=$types|" "$desktop" >"$tmp"
mv "$tmp" "$desktop"
+5 -2
View File
@@ -14,10 +14,13 @@ apt install build-essential cmake qt6-base-dev wayland-protocols glslang-tools \
.Optional dependencies
```sh
apt install librsvg2-dev libxcursor-dev libheif-dev libtiff-dev libraw-dev \
libjxl-dev libopenjp2-7-dev libjxr-dev libgdk-pixbuf-2.0-dev # libglycin-2-dev
apt install cargo librsvg2-dev libxcursor-dev libheif-dev libtiff-dev \
libraw-dev libjxl-dev libopenjp2-7-dev libjxr-dev libwmf-dev
```
Cargo builds the in-tree libdnrs library, which requires a recent Rust version.
Build with `-DDAWN_WITH_DNRS=OFF` to skip it.
.libresvg-dev is required but not often packaged, thus you may need to build it
```sh
apt install cargo cargo-c
+2 -3
View File
@@ -36,9 +36,8 @@ Options
no longer be found.
*--list-supported-media-types*::
Output supported media types and exit. This is used by a script to update
the list of MIME types within *dn*'s desktop file when the list
of GdkPixbuf loaders changes.
*--list-supported-extensions*::
Output supported media types or file extensions and exit.
*-V*, *--version*::
Output version information and exit.
+12 -5
View File
@@ -254,11 +254,18 @@ compatible formats (I own a Nikon DSLR).
I don't especially care about niche formats. If it ever becomes desirable,
there is an obscure project called https://snisurset.net/code/abydos/about.html[
Abydos] which gets quite exhaustive in that regard. The current general
fallbacks are Glycin, the older GdkPixbuf, and on macOS the system ImageIO.
Windows Imaging Component was so ugly that I rather added JPEG XR explicitly
instead as the only thing no other loader could do (the Raw Image Extension
on Microsoft Store uses LibRaw).
Abydos] which gets quite exhaustive in that regard.
The general fallback in my previous viewer used to be GdkPixbuf. Today, that
would mostly defer to Glycin, which in turn rather trivially encapsulates
Rust _image-rs_, plus a few effectively duplicate libraries. Given that Glycin
also does its own sandboxing, and came with API problems, I've regretfully
introduced a minimal Rust library directly into the project. You wouldn't
believe how much fluff this thin layer necessitates. Disgusting.
Similarly, Windows Imaging Component was so ugly that I rather added JPEG XR
explicitly instead as the only thing no other loader could do (the Raw Image
Extension on Microsoft Store uses LibRaw).
Ultimately, loaders are a zoo, which is why you can disable or reorder them.
+21 -7
View File
@@ -37,6 +37,10 @@ if (DAWN_WITH_LIBRAW)
target_sources(libdn PRIVATE load-libraw.cpp)
target_link_libraries(libdn PRIVATE PkgConfig::LIBRAW)
endif()
if (DAWN_WITH_LIBWMF)
target_sources(libdn PRIVATE load-libwmf.cpp)
target_link_libraries(libdn PRIVATE PkgConfig::LIBWMF)
endif()
target_sources(libdn PRIVATE load-resvg.cpp)
if (DAWN_WITH_LIBRSVG)
target_sources(libdn PRIVATE load-librsvg.cpp)
@@ -68,13 +72,23 @@ if (DAWN_WITH_JXRLIB)
target_link_libraries(libdn PRIVATE
"${JXRLIB_GLUE_LIBRARY}" "${JXRLIB_CORE_LIBRARY}")
endif()
if (DAWN_WITH_GLYCIN)
target_sources(libdn PRIVATE load-glycin.cpp)
target_link_libraries(libdn PRIVATE PkgConfig::GLYCIN)
endif()
if (DAWN_WITH_GDKPIXBUF)
target_sources(libdn PRIVATE load-gdkpixbuf.cpp)
target_link_libraries(libdn PRIVATE PkgConfig::GDKPIXBUF)
if (DAWN_WITH_DNRS)
target_sources(libdn PRIVATE load-dnrs.cpp)
target_link_libraries(libdn PRIVATE libdnrs)
if (WIN32)
# Rust's MinGW standard library calls these Native API helpers.
target_link_libraries(libdn PRIVATE ntdll)
endif()
# Rust would otherwise export rust_eh_personality, which resvg exports
# as well, and ELF interposition would cross the two runtimes.
set(exclude "${CMAKE_STATIC_LIBRARY_PREFIX}dnrs${CMAKE_STATIC_LIBRARY_SUFFIX}")
set(exclude "LINKER:--exclude-libs,${exclude}")
include(CheckLinkerFlag)
check_linker_flag(CXX "${exclude}" DAWN_HAVE_EXCLUDE_LIBS)
if (DAWN_HAVE_EXCLUDE_LIBS)
target_link_options(libdn PRIVATE "${exclude}")
endif()
endif()
# Target is libdn; SONAME/file is libdn.so (avoid liblibdn.so).
+4 -7
View File
@@ -43,6 +43,7 @@ LoadFn load_jpeg;
LoadFn load_webp;
LoadFn load_tiff_ep;
LoadFn load_libraw;
LoadFn load_libwmf;
LoadFn load_resvg;
LoadFn load_librsvg;
LoadFn load_xcursor;
@@ -51,16 +52,12 @@ LoadFn load_jxl;
LoadFn load_openjpeg;
LoadFn load_tiff;
LoadFn load_jxr;
LoadFn load_glycin;
LoadFn load_gdkpixbuf;
LoadFn load_dnrs;
LoadFn load_imageio;
LoadFn load_cgpdf;
/// MIME types provided by the installed gdk-pixbuf loaders (if built).
std::vector<std::string> gdkpixbuf_media_types();
/// MIME types glycin can load, per its installed loader config (if built).
std::vector<std::string> glycin_media_types();
/// MIME types compiled into the in-tree Rust decoder.
std::vector<std::string> dnrs_media_types();
/// MIME types the system ImageIO can load (if its loader is built).
std::vector<std::string> imageio_media_types();
+11 -7
View File
@@ -1671,16 +1671,20 @@ constexpr Loader kLoaders[] = {
#endif
"JPEG XR", {"image/jxr", "image/vnd.ms-photo"}, {}},
{"Glycin",
#if DAWN_WITH_GLYCIN
&detail::load_glycin, {}, {}, &detail::glycin_media_types},
{"libwmf",
#if DAWN_WITH_LIBWMF
&detail::load_libwmf,
#else
{}, {}, {}, {}},
{},
#endif
"WMF", {"image/wmf", "image/x-wmf"}, {}},
{"GdkPixbuf",
#if DAWN_WITH_GDKPIXBUF
&detail::load_gdkpixbuf, {}, {}, &detail::gdkpixbuf_media_types},
{"Rust",
#if DAWN_WITH_DNRS
&detail::load_dnrs,
"BMP, DDS, farbfeld, GIF, ICO, JPEG, OpenEXR, PNG, PNM, QOI, "
"Radiance HDR, TARGA, TIFF, WebP, XBM, XPM, ...",
{}, &detail::dnrs_media_types},
#else
{}, {}, {}, {}},
#endif
+2 -3
View File
@@ -285,7 +285,7 @@ public:
double gamma, double whitepoint[2], double primaries[6]);
/// Synthesizes a profile from ITU-T H.273 coded values (as carried by
/// AVIF/HEIF nclx, and by glycin). Null for code points we do not model,
/// AVIF/HEIF nclx). Null for code points we do not model,
/// including PQ (16) and HLG (18): both are HDR curves with no ICC v2
/// parametric equivalent, and approximating them would shift tone badly.
/// `matrix_coefficients` and range are deliberately not taken -- they
@@ -356,8 +356,7 @@ struct Loader {
/// The loader table, in the default order it is tried in.
std::span<const Loader> loaders();
/// MIME types this build can load: base codecs, optional libraries, and
/// whatever gdk-pixbuf modules are installed. Order is stable.
/// shared-mime-info types this build can load. The order is stable.
std::vector<std::string> supported_media_types();
void orientation_dimensions(
+305
View File
@@ -0,0 +1,305 @@
//
// load-dnrs.cpp: broad-coverage fallback through the in-tree Rust decoders
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "libdn-loaders.h"
#include "libdn.h"
#include <libdnrs.h>
#include <cstring>
#include <memory>
using namespace std;
namespace dawn
{
namespace
{
struct DecoderDeleter {
void operator()(dnrs_decoder *decoder) const { dnrs_decoder_free(decoder); }
};
struct ErrorDeleter {
void operator()(dnrs_error *error) const { dnrs_error_free(error); }
};
struct FrameGuard {
dnrs_frame frame{};
~FrameGuard() { dnrs_frame_clear(&frame); }
};
using DecoderPtr = unique_ptr<dnrs_decoder, DecoderDeleter>;
using DnrsErrorPtr = unique_ptr<dnrs_error, ErrorDeleter>;
static string
dnrs_prefix(const char *codec = nullptr)
{
string message = "Rust";
if (codec && *codec)
message += string(" (") + codec + ")";
return message + ": ";
}
static string
dnrs_message(dnrs_error *error, const char *codec = nullptr)
{
string message = dnrs_prefix(codec);
const char *detail = error ? dnrs_error_message(error) : nullptr;
message += detail && *detail ? detail : "decoding error";
return message;
}
static bool
valid_frame(const dnrs_frame &frame, size_t bytes_per_pixel)
{
if (!frame.width || !frame.height || frame.width > kMaxDimension ||
frame.height > kMaxDimension)
return false;
if (frame.width > SIZE_MAX / bytes_per_pixel)
return false;
const size_t row = size_t(frame.width) * bytes_per_pixel;
return frame.data && frame.stride >= row &&
frame.height - 1 <= (SIZE_MAX - row) / frame.stride &&
frame.length >= size_t(frame.height - 1) * frame.stride + row;
}
static void
pack_gray(Image &image, const dnrs_frame &frame, bool alpha, bool wide)
{
for (uint32_t y = 0; y < image.height; y++) {
const uint8_t *src = frame.data + size_t(y) * frame.stride;
uint16_t *dst = assume_aligned<uint16_t>(row_bytes(image, y));
for (uint32_t x = 0; x < image.width; x++) {
uint16_t g = 0, a = 65535;
if (wide) {
g = uint16_t(src[0] | uint16_t(src[1]) << 8);
src += 2;
if (alpha) {
a = uint16_t(src[0] | uint16_t(src[1]) << 8);
src += 2;
}
} else {
g = uint16_t(*src++) * 257;
if (alpha)
a = uint16_t(*src++) * 257;
}
dst[0] = dst[1] = dst[2] = g;
dst[3] = a;
dst += 4;
}
}
}
static void
pack_rgb16(Image &image, const dnrs_frame &frame, bool alpha)
{
for (uint32_t y = 0; y < image.height; y++) {
const uint8_t *src = frame.data + size_t(y) * frame.stride;
uint16_t *dst = assume_aligned<uint16_t>(row_bytes(image, y));
for (uint32_t x = 0; x < image.width; x++) {
const uint16_t r = uint16_t(src[0] | uint16_t(src[1]) << 8);
const uint16_t g = uint16_t(src[2] | uint16_t(src[3]) << 8);
const uint16_t b = uint16_t(src[4] | uint16_t(src[5]) << 8);
const uint16_t a =
alpha ? uint16_t(src[6] | uint16_t(src[7]) << 8) : 65535;
dst[0] = b;
dst[1] = g;
dst[2] = r;
dst[3] = a;
src += alpha ? 8 : 6;
dst += 4;
}
}
}
static ImagePtr
load_frame(const dnrs_frame &frame, Error *error)
{
size_t bpp = 0;
switch (frame.format) {
case DNRS_PIXEL_GRAY8:
bpp = 1;
break;
case DNRS_PIXEL_GRAY_ALPHA8:
bpp = 2;
break;
case DNRS_PIXEL_RGB8:
bpp = 3;
break;
case DNRS_PIXEL_RGBA8:
bpp = 4;
break;
case DNRS_PIXEL_GRAY16LE:
bpp = 2;
break;
case DNRS_PIXEL_GRAY_ALPHA16LE:
bpp = 4;
break;
case DNRS_PIXEL_RGB16LE:
bpp = 6;
break;
case DNRS_PIXEL_RGBA16LE:
bpp = 8;
break;
default:
set_error(error, "Rust: unsupported pixel format");
return nullptr;
}
if (!valid_frame(frame, bpp)) {
set_error(error, "Rust: invalid or truncated frame");
return nullptr;
}
ImagePtr image = image_new(frame.width, frame.height);
if (!image) {
set_error(error, "Rust: image allocation failure");
return nullptr;
}
switch (frame.format) {
case DNRS_PIXEL_GRAY8:
pack_gray(*image, frame, false, false);
break;
case DNRS_PIXEL_GRAY_ALPHA8:
pack_gray(*image, frame, true, false);
break;
case DNRS_PIXEL_RGB8:
pack_rgb8_to_bgra16(*image, frame.data, frame.stride);
break;
case DNRS_PIXEL_RGBA8:
pack_rgba8_to_bgra16(*image, frame.data, frame.stride);
break;
case DNRS_PIXEL_GRAY16LE:
pack_gray(*image, frame, false, true);
break;
case DNRS_PIXEL_GRAY_ALPHA16LE:
pack_gray(*image, frame, true, true);
break;
case DNRS_PIXEL_RGB16LE:
pack_rgb16(*image, frame, false);
break;
case DNRS_PIXEL_RGBA16LE:
pack_rgb16(*image, frame, true);
break;
}
image->frame_duration =
int64_t(min<uint64_t>(frame.duration_ms, uint64_t(INT64_MAX)));
return image;
}
static void
copy_blob(vector<uint8_t> &out, dnrs_blob blob)
{
if (blob.data && blob.length)
out.assign(blob.data, blob.data + blob.length);
}
} // namespace
vector<string>
detail::dnrs_media_types()
{
size_t length = 0;
const char *const *types = dnrs_mime_types(&length);
vector<string> result;
result.reserve(length);
for (size_t i = 0; i < length; i++)
if (types[i])
result.emplace_back(types[i]);
return result;
}
ImagePtr
detail::load_dnrs(
span<const uint8_t> data, const OpenContext &ctx, Error *error)
{
dnrs_error *raw_error = nullptr;
DecoderPtr decoder(dnrs_decoder_new(
data.data(), data.size(), ctx.first_frame_only, &raw_error));
DnrsErrorPtr dnrs_error(raw_error);
if (!decoder) {
set_error(error, dnrs_message(dnrs_error.get()));
return nullptr;
}
dnrs_document_info document{};
raw_error = nullptr;
if (!dnrs_decoder_get_info(decoder.get(), &document, &raw_error)) {
dnrs_error.reset(raw_error);
set_error(error, dnrs_message(dnrs_error.get()));
return nullptr;
}
const char *codec = document.codec;
ImagePtr pages, pages_tail;
dnrs_page_info page_info{};
while (true) {
raw_error = nullptr;
if (!dnrs_decoder_next_page(decoder.get(), &page_info, &raw_error)) {
dnrs_error.reset(raw_error);
if (dnrs_error) {
set_error(error, dnrs_message(dnrs_error.get(), codec));
return nullptr;
}
break;
}
if (!page_info.width || !page_info.height ||
page_info.width > kMaxDimension ||
page_info.height > kMaxDimension) {
set_error(error, dnrs_prefix(codec) + "invalid image dimensions");
return nullptr;
}
ImagePtr frames, frames_tail;
while (true) {
FrameGuard owned;
raw_error = nullptr;
if (!dnrs_decoder_next_frame(
decoder.get(), &owned.frame, &raw_error)) {
dnrs_error.reset(raw_error);
if (dnrs_error) {
set_error(error, dnrs_message(dnrs_error.get(), codec));
return nullptr;
}
break;
}
ImagePtr frame = load_frame(owned.frame, error);
if (!frame)
return nullptr;
append_frame(frames, frames_tail, std::move(frame));
if (ctx.first_frame_only)
break;
}
if (!frames) {
set_error(error, dnrs_prefix(codec) + "page has no frames");
return nullptr;
}
frames->orientation = page_info.orientation <= 8
? Orientation(page_info.orientation)
: Orientation::Unknown;
frames->loops = page_info.loop_count;
copy_blob(frames->icc, document.icc);
copy_blob(frames->exif, document.exif);
copy_blob(frames->xmp, document.xmp);
for (size_t i = 0; i < document.text_length; i++) {
const dnrs_text &entry = document.text[i];
if (entry.key && entry.value)
frames->text.emplace(entry.key, entry.value);
}
ensure_working_premul_pages(
*frames, ctx, nullptr, /*input_premul=*/false);
append_page(pages, pages_tail, std::move(frames));
if (ctx.first_frame_only)
break;
}
if (!pages)
set_error(error, dnrs_prefix(codec) + "image has no pages");
return pages;
}
} // namespace dawn
-136
View File
@@ -1,136 +0,0 @@
//
// load-gdkpixbuf.cpp: last-resort image loading via gdk-pixbuf
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
// This is the last-resort loader: gdk-pixbuf has a wide range of loadable
// formats via its module system, of varying quality and trustworthiness,
// so it only gets a chance after everything more specific has already failed.
#include <dawn-config.h>
#include "libdn-loaders.h"
#include "libdn.h"
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <cstring>
using namespace std;
namespace dawn
{
// GdkPixbuf currently only ever produces 8-bit-per-sample RGB(A) buffers,
// with 3 or 4 channels--this isn't expected to ever legitimately fail.
static ImagePtr
load_gdkpixbuf_pixels(GdkPixbuf *pixbuf, Error *error)
{
if (gdk_pixbuf_get_colorspace(pixbuf) != GDK_COLORSPACE_RGB ||
gdk_pixbuf_get_bits_per_sample(pixbuf) != 8) {
set_error(error, "unsupported gdk-pixbuf pixel format");
return nullptr;
}
auto width = uint32_t(gdk_pixbuf_get_width(pixbuf));
auto height = uint32_t(gdk_pixbuf_get_height(pixbuf));
ImagePtr image = image_new(width, height);
if (!image) {
set_error(error, "image allocation failure");
return nullptr;
}
bool has_alpha = gdk_pixbuf_get_has_alpha(pixbuf);
int src_stride = gdk_pixbuf_get_rowstride(pixbuf);
guint length = 0;
const guchar *src = gdk_pixbuf_get_pixels_with_length(pixbuf, &length);
// gdk-pixbuf's in-memory representation always carries straight
// (unassociated) alpha, exactly what ensure_working_premul() below
// expects.
if (has_alpha)
pack_rgba8_to_bgra16(*image, src, size_t(src_stride));
else
pack_rgb8_to_bgra16(*image, src, size_t(src_stride));
return image;
}
static void
load_gdkpixbuf_metadata(Image &image, GdkPixbuf *pixbuf)
{
const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation");
if (orientation && strlen(orientation) == 1) {
int n = *orientation - '0';
if (n >= 1 && n <= 8)
image.orientation = Orientation(n);
}
const char *icc = gdk_pixbuf_get_option(pixbuf, "icc-profile");
if (icc) {
gsize len = 0;
guchar *raw = g_base64_decode(icc, &len);
if (raw) {
image.icc.assign(raw, raw + len);
g_free(raw);
}
}
}
vector<string>
detail::gdkpixbuf_media_types()
{
vector<string> types;
GSList *formats = gdk_pixbuf_get_formats();
for (GSList *iter = formats; iter; iter = iter->next) {
gchar **mime_types =
gdk_pixbuf_format_get_mime_types((GdkPixbufFormat *) iter->data);
for (gchar **p = mime_types; *p; p++)
types.push_back(*p);
g_strfreev(mime_types);
}
g_slist_free(formats);
return types;
}
ImagePtr
detail::load_gdkpixbuf(
span<const uint8_t> data, const OpenContext &ctx, Error *error)
{
// gdk-pixbuf controls the playback of animations itself, and there is
// no reliable method of extracting individual frames from it--treat
// everything gdk-pixbuf can load as a single-frame, single-page image.
GdkPixbufLoader *loader = gdk_pixbuf_loader_new();
GError *gerror = nullptr;
bool ok = gdk_pixbuf_loader_write(
loader, data.data(), gsize(data.size()), &gerror);
if (ok)
ok = gdk_pixbuf_loader_close(loader, &gerror);
else
(void) gdk_pixbuf_loader_close(loader, nullptr);
GdkPixbuf *pixbuf = ok ? gdk_pixbuf_loader_get_pixbuf(loader) : nullptr;
if (!pixbuf) {
set_error(
error, gerror ? gerror->message : "gdk-pixbuf decoding error");
if (gerror)
g_error_free(gerror);
g_object_unref(loader);
return nullptr;
}
g_object_ref(pixbuf);
g_object_unref(loader);
ImagePtr image = load_gdkpixbuf_pixels(pixbuf, error);
if (image) {
load_gdkpixbuf_metadata(*image, pixbuf);
ensure_working_premul(*image, ctx, nullptr, /*input_premul=*/false);
}
g_object_unref(pixbuf);
return image;
}
} // namespace dawn
-293
View File
@@ -1,293 +0,0 @@
//
// load-glycin.cpp: sandboxed image loading via glycin
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
// Like gdk-pixbuf, glycin is a broad-coverage fallback built on out-of-process
// loaders, so it only gets a chance after everything more specific has failed.
// It goes first of the two: the loaders are memory-safe and sandboxed, and it
// gives us real 16-bit output rather than gdk-pixbuf's forced 8-bit RGB(A).
//
// glycin's orientation getter stays at 1 unless transformations are applied,
// so we let it apply them and take the pixels as already oriented. It exposes
// no Exif; key-value metadata comes through Image::text.
//
// How much it will tell us about colour depends on its version. From 2.2 it
// can be told to leave the pixels in the file's own space, and hands over the
// ICC profile to go with them, like every other loader here. Before that the
// CICP is all there is, synthesized into a profile where we can represent it
// (see get_profile_cicp).
#include <dawn-config.h>
#include "libdn-loaders.h"
#include "libdn.h"
#include <glycin.h>
// glycin 2.2 brought the colour API, and no version macro to ask for it by.
// GlyFrameDetails landed in the same release, and its type macro is the only
// thing that release left visible to the preprocessor.
#ifdef GLY_TYPE_FRAME_DETAILS
#define DAWN_GLYCIN_2_2 1
#else
#define DAWN_GLYCIN_2_2 0
#endif
using namespace std;
namespace dawn
{
// The formats we ask glycin for. This is a whitelist, not a filter: glycin
// converts anything else into the nearest listed format, so a short list
// costs conversions, never coverage.
//
// Four is the minimum that avoids all lossy conversions. glycin's other
// formats reduce to these without losing anything:
//
// - channel order (A8R8G8B8, B8G8R8A8, ...) is just a permutation, and
// our packers only read R,G,B[,A] order;
// - premultiplied variants would have to be un-premultiplied for
// ensure_working_premul() anyway, so we take the straight ones;
// - grayscale (G8, G16, G8A8, ...) widens to RGB losslessly;
// - 8-bit widens to our 16-bit working format either way.
//
// The gap is float: R16G16B16_FLOAT and the R32 formats get squashed into
// 16-bit unsigned, which clips values outside [0,1] and loses precision on
// HDR sources (EXR most of all). The working format is fixed-point BGRA16,
// so nothing here could preserve that anyway--this is a libdn-wide limit,
// not one this list imposes. Should the working format ever grow a float
// variant, add those formats here and give them a packer.
constexpr GlyMemoryFormatSelection kAcceptedFormats = GlyMemoryFormatSelection(
GLY_MEMORY_SELECTION_R8G8B8 | GLY_MEMORY_SELECTION_R8G8B8A8 |
GLY_MEMORY_SELECTION_R16G16B16 | GLY_MEMORY_SELECTION_R16G16B16A16);
static ImagePtr
load_glycin_frame(GlyFrame *frame, const OpenContext &ctx, Error *error)
{
uint32_t width = gly_frame_get_width(frame);
uint32_t height = gly_frame_get_height(frame);
if (!width || !height || width > kMaxDimension || height > kMaxDimension) {
set_error(error, "invalid image dimensions");
return nullptr;
}
GBytes *bytes = gly_frame_get_buf_bytes(frame);
if (!bytes) {
set_error(error, "missing frame buffer");
return nullptr;
}
gsize length = 0;
const uint8_t *src = (const uint8_t *) g_bytes_get_data(bytes, &length);
size_t stride = gly_frame_get_stride(frame);
GlyMemoryFormat format = gly_frame_get_memory_format(frame);
// Guard against a loader reporting a buffer smaller than it describes,
// which would otherwise have us read past the end of the mapping.
size_t bpp = 0;
switch (format) {
case GLY_MEMORY_R8G8B8:
bpp = 3;
break;
case GLY_MEMORY_R8G8B8A8:
bpp = 4;
break;
case GLY_MEMORY_R16G16B16:
bpp = 6;
break;
case GLY_MEMORY_R16G16B16A16:
bpp = 8;
break;
default:
set_error(error, "unsupported glycin pixel format");
return nullptr;
}
if (stride < size_t(width) * bpp ||
length < size_t(height - 1) * stride + size_t(width) * bpp) {
set_error(error, "truncated glycin frame buffer");
return nullptr;
}
ImagePtr image = image_new(width, height);
if (!image) {
set_error(error, "image allocation failure");
return nullptr;
}
// None of the accepted formats is premultiplied, which is what
// ensure_working_premul() below expects.
switch (format) {
case GLY_MEMORY_R8G8B8:
pack_rgb8_to_bgra16(*image, src, stride);
break;
case GLY_MEMORY_R8G8B8A8:
pack_rgba8_to_bgra16(*image, src, stride);
break;
case GLY_MEMORY_R16G16B16:
pack_rgb16le_to_bgra16(
*image, assume_aligned<const uint16_t>(src), stride, 16);
break;
case GLY_MEMORY_R16G16B16A16:
pack_rgba16le_to_bgra16(
*image, assume_aligned<const uint16_t>(src), stride, 16);
break;
default:
break;
}
int64_t delay = gly_frame_get_delay(frame);
if (delay > 0)
image->frame_duration = delay / 1000;
// ensure_working_premul() turns a blob into a profile on its own.
shared_ptr<Profile> source;
#if DAWN_GLYCIN_2_2
if (GBytes *icc = gly_frame_get_color_icc_profile(frame)) {
gsize icc_length = 0;
auto *blob = (const uint8_t *) g_bytes_get_data(icc, &icc_length);
image->icc.assign(blob, blob + icc_length);
g_bytes_unref(icc);
}
#endif
// A CICP brings no blob with it, so record the profile we synthesize,
// or the viewer would report no source profile at all.
GlyCicp *cicp =
image->icc.empty() ? gly_frame_get_color_cicp(frame) : nullptr;
if (cicp) {
source = cmm_or_default(ctx)->get_profile_cicp(
cicp->color_primaries, cicp->transfer_characteristics);
if (source) {
image->icc = source->to_bytes();
image->effective_profile = source;
} else
add_warning(ctx,
"glycin: unrepresentable CICP colour space, assuming sRGB");
gly_cicp_free(cicp);
}
ensure_working_premul(*image, ctx, source.get(), /*input_premul=*/false);
return image;
}
// Copies glycin's key-value metadata (PNG tEXt/zTXt/iTXt and friends) onto
// the image. glycin already strips the chunk-type prefixes gdk-pixbuf keeps,
// which matches what load-wuffs.cpp puts in Image::text.
static void
load_glycin_metadata(Image &image, GlyImage *img)
{
GStrv keys = gly_image_get_metadata_keys(img);
for (gchar **k = keys; k && *k; k++) {
gchar *value = gly_image_get_metadata_key_value(img, *k);
if (!value)
continue;
image.text.emplace(*k, value);
g_free(value);
}
g_strfreev(keys);
}
vector<string>
detail::glycin_media_types()
{
vector<string> types;
GStrv mime_types = gly_loader_get_mime_types();
for (gchar **p = mime_types; p && *p; p++)
types.push_back(*p);
g_strfreev(mime_types);
return types;
}
ImagePtr
detail::load_glycin(
span<const uint8_t> data, const OpenContext &ctx, Error *error)
{
// glycin takes ownership of the GBytes, and we must not hand it a view
// of a span whose lifetime it does not control.
GBytes *bytes = g_bytes_new(data.data(), data.size());
GlyLoader *loader = gly_loader_new_for_bytes(bytes);
g_bytes_unref(bytes);
// We have no way to read the untransformed orientation back out, so let
// glycin bake it into the pixels rather than lose it entirely.
gly_loader_set_apply_transformations(loader, TRUE);
gly_loader_set_accepted_memory_formats(loader, kAcceptedFormats);
#if DAWN_GLYCIN_2_2
// dn colour-manages for itself, and would rather have the original.
gly_loader_set_color_convert_icc_srgb(loader, FALSE);
#endif
GError *gerror = nullptr;
GlyImage *img = gly_loader_load(loader, &gerror);
g_object_unref(loader);
if (!img) {
set_error(error, gerror ? gerror->message : "glycin decoding error");
if (gerror)
g_error_free(gerror);
return nullptr;
}
ImagePtr head, tail;
while (true) {
// Without this, glycin restarts animations from the first frame
// instead of telling us that it has run out.
GlyFrameRequest *request = gly_frame_request_new();
gly_frame_request_set_loop_animation(request, FALSE);
GError *ferror = nullptr;
GlyFrame *frame = gly_image_get_specific_frame(img, request, &ferror);
g_object_unref(request);
if (!frame) {
// Running out of frames is normal, and not always reported as
// NO_MORE_FRAMES--some loaders raise a generic error instead.
// Only a failure on the very first frame means the image itself
// is undecodable.
if (!head)
set_error(
error, ferror ? ferror->message : "glycin decoding error");
if (ferror)
g_error_free(ferror);
break;
}
ImagePtr image = load_glycin_frame(frame, ctx, error);
int64_t delay = gly_frame_get_delay(frame);
g_object_unref(frame);
if (!image) {
if (!head) {
g_object_unref(img);
return nullptr;
}
add_warning(ctx, "glycin: dropping undecodable trailing frame");
break;
}
append_frame(head, tail, image);
if (ctx.first_frame_only)
break;
// Stop unless this really is an animation. Asking a still image for
// another frame is how we would learn that it has none, but glycin's
// ISOBMFF loaders (AVIF, HEIF) never answer that question: they block
// forever instead of raising NO_MORE_FRAMES. A zero delay marks a
// still frame in every loader tested, so key off that and never make
// the request. Animations are still probed, and there the
// end-of-frames reply does arrive.
if (delay <= 0)
break;
}
// Metadata belongs to the image, not any one frame: attach it to the head.
if (head)
load_glycin_metadata(*head, img);
g_object_unref(img);
return head;
}
} // namespace dawn
+231
View File
@@ -0,0 +1,231 @@
//
// load-libwmf.cpp: WMF rendering through libwmf's GD device
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "libdn-loaders.h"
#include "libdn.h"
#include <libwmf/api.h>
#include <libwmf/gd.h>
#include <cmath>
#include <cstring>
#include <memory>
using namespace std;
namespace dawn
{
namespace
{
class WmfRenderClosure : public RenderClosure
{
vector<uint8_t> data_;
double width_;
double height_;
public:
WmfRenderClosure(vector<uint8_t> &&data, double width, double height)
: data_(std::move(data)), width_(width), height_(height)
{
}
ImagePtr render(Cmm *cmm, Profile *target, double scale) override;
ImagePtr render_internal(
double scale, Cmm *cmm, Profile *target, Error *error);
};
/// Everything libwmf hands out has to go back, on every exit path.
struct WmfApi {
wmfAPI *api = nullptr;
bool opened = false;
~WmfApi()
{
if (opened)
wmf_mem_close(api);
if (api)
wmf_api_destroy(api);
}
};
} // namespace
static const char *
wmf_error_string(wmf_error_t error)
{
switch (error) {
case wmf_E_None:
return "no error";
case wmf_E_InsMem:
return "out of memory";
case wmf_E_BadFile:
return "unreadable input";
case wmf_E_BadFormat:
return "invalid WMF data";
case wmf_E_EOF:
return "unexpected end of file";
case wmf_E_DeviceError:
return "GD device error";
case wmf_E_Glitch:
return "internal error";
case wmf_E_Assert:
return "internal assertion failure";
case wmf_E_UserExit:
return "rendering cancelled";
}
return "unknown error";
}
static bool
wmf_open_and_scan(WmfApi &wmf, vector<uint8_t> &data, wmfD_Rect *bbox,
uint32_t *width, uint32_t *height, Error *error)
{
if (data.size() > size_t(LONG_MAX)) {
set_error(error, "libwmf: input is too large");
return false;
}
wmfAPI_Options options{};
options.function = wmf_gd_function;
unsigned long flags = WMF_OPT_FUNCTION | WMF_OPT_IGNORE_NONFATAL |
WMF_OPT_NO_ERROR | WMF_OPT_NO_DEBUG;
wmf_error_t status = wmf_api_create(&wmf.api, flags, &options);
if (status != wmf_E_None) {
set_error(error, string("libwmf: ") + wmf_error_string(status));
return false;
}
// The wmf_gd_image device hands out a GD image that libwmf offers no
// way to destroy; only its encoders dispose of one themselves.
wmf_gd_t *device = WMF_GD_GetData(wmf.api);
if (!(device->flags & WMF_GD_SUPPORTS_PNG)) {
set_error(error, "libwmf: built without PNG support");
return false;
}
device->type = wmf_gd_png;
device->flags |= WMF_GD_OUTPUT_MEMORY;
status = wmf_mem_open(wmf.api, data.data(), long(data.size()));
if (status != wmf_E_None) {
set_error(error, string("libwmf: ") + wmf_error_string(status));
return false;
}
wmf.opened = true;
status = wmf_scan(wmf.api, 0, bbox);
unsigned int w = 0, h = 0;
if (status == wmf_E_None)
status = wmf_display_size(wmf.api, &w, &h, 72, 72);
if (status != wmf_E_None) {
set_error(error, string("libwmf: ") + wmf_error_string(status));
return false;
}
if (!w || !h || w > kMaxDimension || h > kMaxDimension) {
set_error(error, "libwmf: invalid image dimensions");
return false;
}
*width = w;
*height = h;
return true;
}
/// libwmf reports no length for what it writes to memory, though PNG is
/// self-delimiting, so measure its own output by walking the chunks.
static size_t
png_length(const char *data)
{
auto png = (const uint8_t *) data;
if (memcmp(png, "\x89PNG\r\n\x1a\n", 8))
return 0;
const uint8_t *chunk = png + 8;
for (bool last = false; !last;) {
size_t length = size_t(chunk[0]) << 24 | size_t(chunk[1]) << 16 |
size_t(chunk[2]) << 8 | chunk[3];
last = !memcmp(chunk + 4, "IEND", 4);
chunk += 12 + length;
}
return size_t(chunk - png);
}
/// Renders at the given dimensions, or at the metafile's own when they are
/// zero, in which case they are reported back.
static ImagePtr
render_wmf(vector<uint8_t> &data, uint32_t *width, uint32_t *height,
const OpenContext &ctx, Error *error)
{
WmfApi wmf;
wmfD_Rect bbox{};
uint32_t base_width = 0, base_height = 0;
if (!wmf_open_and_scan(wmf, data, &bbox, &base_width, &base_height, error))
return nullptr;
if (!*width || !*height) {
*width = base_width;
*height = base_height;
}
wmf_gd_t *device = WMF_GD_GetData(wmf.api);
device->bbox = bbox;
device->width = *width;
device->height = *height;
wmf_error_t status = wmf_play(wmf.api, 0, &bbox);
size_t length =
status == wmf_E_None && device->memory ? png_length(device->memory) : 0;
if (!length) {
set_error(error,
string("libwmf: ") +
(status == wmf_E_None ? "GD device produced no image"
: wmf_error_string(status)));
return nullptr;
}
return detail::load_wuffs(
{(const uint8_t *) device->memory, length}, ctx, error);
}
ImagePtr
WmfRenderClosure::render(Cmm *cmm, Profile *target, double scale)
{
Error ignored;
return render_internal(scale, cmm, target, &ignored);
}
ImagePtr
WmfRenderClosure::render_internal(
double scale, Cmm *cmm, Profile *target, Error *error)
{
double w = ceil(width_ * scale), h = ceil(height_ * scale);
if (w < 1 || h < 1 || w > kMaxDimension || h > kMaxDimension) {
set_error(error, "libwmf: image dimensions overflow");
return nullptr;
}
OpenContext ctx;
if (cmm)
ctx.cmm = cmm->shared_from_this();
if (target)
ctx.screen_profile = shared_ptr<Profile>(shared_ptr<Profile>(), target);
auto width = uint32_t(w), height = uint32_t(h);
return render_wmf(data_, &width, &height, ctx, error);
}
ImagePtr
detail::load_libwmf(
span<const uint8_t> data, const OpenContext &ctx, Error *error)
{
vector<uint8_t> owned(data.begin(), data.end());
uint32_t width = 0, height = 0;
ImagePtr image = render_wmf(owned, &width, &height, ctx, error);
if (image)
image->render =
make_unique<WmfRenderClosure>(std::move(owned), width, height);
return image;
}
} // namespace dawn
+68
View File
@@ -0,0 +1,68 @@
# Cargo builds for the build host, and only an explicit target gets
# a directory of its own. Dawn's Windows builds are MinGW, and cross-compiled.
set(default_target)
if (WIN32)
set(default_target x86_64-pc-windows-gnu)
endif()
set(DAWN_DNRS_RUST_TARGET "${default_target}" CACHE STRING
"Rust target triple for libdnrs; empty builds for the build host")
if (CMAKE_CROSSCOMPILING AND NOT DAWN_DNRS_RUST_TARGET)
message(FATAL_ERROR "Cross-compiling libdnrs needs DAWN_DNRS_RUST_TARGET")
endif()
set(target_dir "${CMAKE_CURRENT_BINARY_DIR}/cargo-target")
set(cargo_args build --release)
set(cargo_env "CARGO_TARGET_DIR=${target_dir}")
set(archive "${target_dir}")
if (DAWN_DNRS_RUST_TARGET)
list(APPEND cargo_args --target "${DAWN_DNRS_RUST_TARGET}")
string(APPEND archive "/${DAWN_DNRS_RUST_TARGET}")
endif()
set(archive "${archive}/release/${CMAKE_STATIC_LIBRARY_PREFIX}dnrs")
set(archive "${archive}${CMAKE_STATIC_LIBRARY_SUFFIX}")
if (APPLE AND CMAKE_OSX_DEPLOYMENT_TARGET)
list(APPEND cargo_env
"MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
if (CMAKE_CROSSCOMPILING)
string(TOUPPER "${DAWN_DNRS_RUST_TARGET}" env_target)
string(REPLACE "-" "_" env_target "${env_target}")
string(TOLOWER "${env_target}" cc_env_target)
# As with resvg: Rust comes with the standard library of its own host
# alone, so any cross build has to make one from rust-src, and that is
# a nightly feature.
list(APPEND cargo_args -Zbuild-std)
list(APPEND cargo_env
RUSTC_BOOTSTRAP=1
"CARGO_TARGET_${env_target}_LINKER=${CMAKE_C_COMPILER}"
"CC_${cc_env_target}=${CMAKE_C_COMPILER}"
"AR_${cc_env_target}=${CMAKE_AR}")
endif()
add_custom_command(
OUTPUT "${archive}"
COMMAND ${CMAKE_COMMAND} -E env ${cargo_env}
${CARGO_EXECUTABLE} ${cargo_args}
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS lib.rs Cargo.toml libdnrs.h CMakeLists.txt
COMMENT "Building libdnrs"
JOB_SERVER_AWARE TRUE
VERBATIM)
add_custom_target(libdnrs-build DEPENDS "${archive}")
add_library(libdnrs STATIC IMPORTED GLOBAL)
set_target_properties(libdnrs PROPERTIES
IMPORTED_LOCATION "${archive}"
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}")
add_dependencies(libdnrs libdnrs-build)
if (BUILD_TESTING AND NOT CMAKE_CROSSCOMPILING)
add_test(NAME dnrs_rust
COMMAND ${CMAKE_COMMAND} -E env ${cargo_env}
${CARGO_EXECUTABLE} test --all-features)
set_tests_properties(dnrs_rust PROPERTIES
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}")
endif()
+34
View File
@@ -0,0 +1,34 @@
[package]
name = "dnrs"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
publish = false
[lib]
path = "lib.rs"
crate-type = ["staticlib"]
[features]
default = ["jpeg2000", "raw"]
jpeg2000 = ["dep:hayro-jpeg2000"]
raw = ["dep:libopenraw"]
[dependencies]
hayro-jpeg2000 = { version = "=0.3.4", features = ["image"], optional = true }
image = { version = "=0.25.10", default-features = false, features = [
"bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm",
"qoi", "tga", "tiff", "webp",
] }
image-extras = { version = "=0.1.1", default-features = false, features = [
"xbm", "xpm",
] }
libopenraw = { version = "=0.4.0-alpha.12", default-features = false,
optional = true }
png = { version = "=0.18.1", default-features = false }
tiff = { version = "=0.11.3", default-features = false }
[profile.release]
lto = true
codegen-units = 1
strip = "debuginfo"
+987
View File
@@ -0,0 +1,987 @@
//
// libdnrs: image-rs and other decoders exposed through a small C ABI
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#![allow(non_camel_case_types)]
use std::ffi::{c_char, CStr, CString};
use std::io::{BufRead, Cursor, Seek};
use std::mem;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr;
use std::sync::OnceLock;
use image::{
AnimationDecoder, DynamicImage, ImageDecoder, ImageFormat, ImageReader,
};
#[cfg(feature = "raw")]
use libopenraw::{metadata::Value, Bitmap};
const MAX_DIMENSION: u32 = 65_535;
const MAX_DECODE_ALLOC: u64 = 512 * 1024 * 1024;
const MAX_TEXT_LENGTH: usize = 2 * 1024 * 1024;
#[repr(C)]
pub struct dnrs_blob {
data: *const u8,
length: usize,
}
#[repr(C)]
pub struct dnrs_text {
key: *const c_char,
value: *const c_char,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub enum dnrs_pixel_format {
Gray8,
GrayAlpha8,
Rgb8,
Rgba8,
Gray16Le,
GrayAlpha16Le,
Rgb16Le,
Rgba16Le,
}
#[repr(C)]
pub struct dnrs_document_info {
codec: *const c_char,
page_count: u32,
icc: dnrs_blob,
exif: dnrs_blob,
xmp: dnrs_blob,
text: *const dnrs_text,
text_length: usize,
}
#[repr(C)]
pub struct dnrs_page_info {
width: u32,
height: u32,
frame_count: u32,
orientation: u32,
loop_count: u64,
}
#[repr(C)]
pub struct dnrs_frame {
width: u32,
height: u32,
stride: usize,
format: dnrs_pixel_format,
data: *mut u8,
length: usize,
duration_ms: u64,
}
pub struct dnrs_error {
message: CString,
}
struct Frame {
width: u32,
height: u32,
stride: usize,
format: dnrs_pixel_format,
pixels: Vec<u8>,
duration_ms: u64,
}
struct Page {
width: u32,
height: u32,
orientation: u32,
loop_count: u64,
frames: Vec<Frame>,
}
#[derive(Default)]
struct Metadata {
icc: Vec<u8>,
exif: Vec<u8>,
xmp: Vec<u8>,
orientation: u32,
text: Vec<(String, String)>,
}
pub struct dnrs_decoder {
codec: CString,
icc: Vec<u8>,
exif: Vec<u8>,
xmp: Vec<u8>,
_text_storage: Vec<(CString, CString)>,
text: Vec<dnrs_text>,
pages: Vec<Page>,
page_index: Option<usize>,
frame_index: usize,
}
fn image_limits() -> image::Limits {
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_DIMENSION);
limits.max_image_height = Some(MAX_DIMENSION);
limits.max_alloc = Some(MAX_DECODE_ALLOC);
limits
}
fn set_decoder_limits(decoder: &mut impl ImageDecoder) -> Result<(), String> {
decoder
.set_limits(image_limits())
.map_err(|error| error.to_string())
}
static MIME_TYPES: &[&CStr] = &[
c"image/bmp",
c"image/gif",
c"image/jpeg",
c"image/png",
c"image/qoi",
c"image/tiff",
c"image/vnd.microsoft.icon",
c"image/vnd.radiance",
c"image/webp",
c"image/x-dds",
c"image/x-exr",
c"image/x-farbfeld",
c"image/x-portable-anymap",
c"image/x-tga",
c"image/x-win-bitmap",
c"image/x-xbitmap",
c"image/x-xpixmap",
#[cfg(feature = "jpeg2000")]
c"image/jp2",
#[cfg(feature = "jpeg2000")]
c"image/x-jp2-codestream",
#[cfg(feature = "raw")]
c"image/x-dcraw",
];
fn set_error(out: *mut *mut dnrs_error, message: impl ToString) {
if out.is_null() {
return;
}
let message = message.to_string().replace('\0', " ");
unsafe {
*out = Box::into_raw(Box::new(dnrs_error {
message: CString::new(message).expect("NULs were removed"),
}));
}
}
fn ffi_result<T>(
error: *mut *mut dnrs_error,
default: T,
f: impl FnOnce() -> Result<T, String>,
) -> T {
if !error.is_null() {
unsafe { *error = ptr::null_mut() };
}
match catch_unwind(AssertUnwindSafe(f)) {
Ok(Ok(value)) => value,
Ok(Err(message)) => {
set_error(error, message);
default
}
Err(_) => {
set_error(error, "decoder panic");
default
}
}
}
fn bytes_per_pixel(format: dnrs_pixel_format) -> usize {
match format {
dnrs_pixel_format::Gray8 => 1,
dnrs_pixel_format::GrayAlpha8 => 2,
dnrs_pixel_format::Rgb8 => 3,
dnrs_pixel_format::Rgba8 => 4,
dnrs_pixel_format::Gray16Le => 2,
dnrs_pixel_format::GrayAlpha16Le => 4,
dnrs_pixel_format::Rgb16Le => 6,
dnrs_pixel_format::Rgba16Le => 8,
}
}
fn make_frame(
width: u32,
height: u32,
format: dnrs_pixel_format,
pixels: Vec<u8>,
duration_ms: u64,
) -> Result<Frame, String> {
if width == 0 || width > MAX_DIMENSION {
return Err("invalid image width".into());
}
if height == 0 || height > MAX_DIMENSION {
return Err("invalid image height".into());
}
let stride = (width as usize)
.checked_mul(bytes_per_pixel(format))
.ok_or("image dimensions overflow")?;
let expected = stride
.checked_mul(height as usize)
.ok_or("image dimensions overflow")?;
if pixels.len() != expected {
return Err("decoder returned a truncated frame".into());
}
Ok(Frame {
width,
height,
stride,
format,
pixels,
duration_ms,
})
}
fn frame_from_dynamic(
image: DynamicImage,
duration_ms: u64,
) -> Result<Frame, String> {
let (width, height) = (image.width(), image.height());
let (format, pixels) = match image {
DynamicImage::ImageLuma8(v) => (dnrs_pixel_format::Gray8, v.into_raw()),
DynamicImage::ImageLumaA8(v) => {
(dnrs_pixel_format::GrayAlpha8, v.into_raw())
}
DynamicImage::ImageRgb8(v) => (dnrs_pixel_format::Rgb8, v.into_raw()),
DynamicImage::ImageRgba8(v) => (dnrs_pixel_format::Rgba8, v.into_raw()),
DynamicImage::ImageLuma16(v) => {
(dnrs_pixel_format::Gray16Le, u16_to_le(v.into_raw()))
}
DynamicImage::ImageLumaA16(v) => {
(dnrs_pixel_format::GrayAlpha16Le, u16_to_le(v.into_raw()))
}
DynamicImage::ImageRgb16(v) => {
(dnrs_pixel_format::Rgb16Le, u16_to_le(v.into_raw()))
}
DynamicImage::ImageRgba16(v) => {
(dnrs_pixel_format::Rgba16Le, u16_to_le(v.into_raw()))
}
other => (
dnrs_pixel_format::Rgba16Le,
u16_to_le(other.to_rgba16().into_raw()),
),
};
make_frame(width, height, format, pixels, duration_ms)
}
fn u16_to_le(samples: Vec<u16>) -> Vec<u8> {
let mut bytes = Vec::with_capacity(samples.len() * 2);
for sample in samples {
bytes.extend_from_slice(&sample.to_le_bytes());
}
bytes
}
fn animation_frames<I>(
frames: I,
first_frame_only: bool,
) -> Result<Vec<Frame>, String>
where
I: IntoIterator<Item = image::ImageResult<image::Frame>>,
{
let mut decoded = Vec::new();
let mut total = 0_u64;
for frame in
frames
.into_iter()
.take(if first_frame_only { 1 } else { usize::MAX })
{
let frame = frame.map_err(|e| e.to_string())?;
let (numer, denom) = frame.delay().numer_denom_ms();
let duration = if denom == 0 {
0
} else {
(u64::from(numer) + u64::from(denom) / 2) / u64::from(denom)
};
let frame = frame_from_dynamic(
DynamicImage::ImageRgba8(frame.into_buffer()),
duration,
)?;
push_frame(&mut decoded, &mut total, frame)?;
}
Ok(decoded)
}
fn push_frame(
frames: &mut Vec<Frame>,
total: &mut u64,
frame: Frame,
) -> Result<(), String> {
*total = total
.checked_add(frame.pixels.len() as u64)
.ok_or_else(|| "decoded image size overflow".to_string())?;
if *total > MAX_DECODE_ALLOC {
return Err("decoded image exceeds allocation limit".into());
}
frames.push(frame);
Ok(())
}
fn collect_metadata(
decoder: &mut impl ImageDecoder,
) -> Result<Metadata, String> {
Ok(Metadata {
icc: decoder
.icc_profile()
.map_err(|error| error.to_string())?
.unwrap_or_default(),
exif: decoder
.exif_metadata()
.map_err(|error| error.to_string())?
.unwrap_or_default(),
xmp: decoder
.xmp_metadata()
.map_err(|error| error.to_string())?
.unwrap_or_default(),
orientation: u32::from(
decoder
.orientation()
.map_err(|error| error.to_string())?
.to_exif(),
),
text: Vec::new(),
})
}
fn decode_frame<D: ImageDecoder>(
mut decoder: D,
) -> Result<(Frame, Metadata), String> {
set_decoder_limits(&mut decoder)?;
let metadata = collect_metadata(&mut decoder)?;
let image =
DynamicImage::from_decoder(decoder).map_err(|e| e.to_string())?;
Ok((frame_from_dynamic(image, 0)?, metadata))
}
fn decode_still<R: BufRead + Seek>(
mut reader: ImageReader<R>,
) -> Result<(Frame, Metadata), String> {
reader.limits(image_limits());
decode_frame(reader.into_decoder().map_err(|e| e.to_string())?)
}
fn decoder_from_still<D: ImageDecoder>(
codec: &str,
decoder: D,
) -> Result<dnrs_decoder, String> {
let (frame, metadata) = decode_frame(decoder)?;
decoder_from_frames(codec.into(), vec![frame], 0, metadata)
}
fn collect_png_text(data: &[u8]) -> Result<Vec<(String, String)>, String> {
let mut decoder = png::Decoder::new(Cursor::new(data));
decoder.set_limits(png::Limits {
bytes: usize::try_from(MAX_DECODE_ALLOC).unwrap_or(usize::MAX),
});
let mut reader = decoder.read_info().map_err(|error| error.to_string())?;
let info = reader.info();
if info.width == 0
|| info.height == 0
|| info.width > MAX_DIMENSION
|| info.height > MAX_DIMENSION
{
return Err("invalid image dimensions".into());
}
reader.finish().map_err(|error| error.to_string())?;
let info = reader.info();
let mut text = Vec::new();
for chunk in &info.uncompressed_latin1_text {
text.push((chunk.keyword.clone(), chunk.text.clone()));
}
for chunk in &info.compressed_latin1_text {
let mut chunk = chunk.clone();
chunk
.decompress_text_with_limit(MAX_TEXT_LENGTH)
.map_err(|error| error.to_string())?;
text.push((
chunk.keyword.clone(),
chunk.get_text().map_err(|error| error.to_string())?,
));
}
for chunk in &info.utf8_text {
if chunk.keyword == "XML:com.adobe.xmp" {
continue;
}
let mut chunk = chunk.clone();
chunk
.decompress_text_with_limit(MAX_TEXT_LENGTH)
.map_err(|error| error.to_string())?;
text.push((
chunk.keyword.clone(),
chunk.get_text().map_err(|error| error.to_string())?,
));
}
Ok(text)
}
fn tiff_select_page(page: &mut [u8], offset: u64) -> Result<(), String> {
if page.len() < 16 {
return Err("invalid TIFF header".into());
}
match page.get(0..4) {
Some([b'I', b'I', 42, 0]) => {
let offset = u32::try_from(offset)
.map_err(|_| "invalid TIFF page offset")?;
page[4..8].copy_from_slice(&offset.to_le_bytes());
}
Some([b'M', b'M', 0, 42]) => {
let offset = u32::try_from(offset)
.map_err(|_| "invalid TIFF page offset")?;
page[4..8].copy_from_slice(&offset.to_be_bytes());
}
Some([b'I', b'I', 43, 0]) => {
page[8..16].copy_from_slice(&offset.to_le_bytes())
}
Some([b'M', b'M', 0, 43]) => {
page[8..16].copy_from_slice(&offset.to_be_bytes())
}
_ => return Err("invalid TIFF header".into()),
}
Ok(())
}
fn decode_tiff_pages(
data: &[u8],
first_frame_only: bool,
) -> Result<Vec<(Frame, Metadata)>, String> {
let mut scanner = tiff::decoder::Decoder::new(Cursor::new(data))
.map_err(|error| error.to_string())?;
// image-rs only ever decodes the first IFD, so retarget the header of
// one working copy at each page in turn.
let mut page = data.to_vec();
let mut pages = Vec::new();
let mut total = 0_u64;
loop {
let (width, height) =
scanner.dimensions().map_err(|error| error.to_string())?;
if width == 0
|| height == 0
|| width > MAX_DIMENSION
|| height > MAX_DIMENSION
{
return Err("invalid image dimensions".into());
}
let offset = scanner
.ifd_pointer()
.ok_or_else(|| "missing TIFF page offset".to_string())?
.0;
tiff_select_page(&mut page, offset)?;
let decoded = decode_still(ImageReader::with_format(
Cursor::new(page.as_slice()),
ImageFormat::Tiff,
))?;
total = total
.checked_add(decoded.0.pixels.len() as u64)
.ok_or_else(|| "decoded image size overflow".to_string())?;
if total > MAX_DECODE_ALLOC {
return Err("decoded image exceeds allocation limit".into());
}
pages.push(decoded);
if first_frame_only || !scanner.more_images() {
break;
}
scanner.next_image().map_err(|error| error.to_string())?;
}
Ok(pages)
}
fn decode_animation<I>(
frames: I,
loops: image::metadata::LoopCount,
metadata: Metadata,
first_frame_only: bool,
) -> Result<(Vec<Frame>, Metadata, u64), String>
where
I: IntoIterator<Item = image::ImageResult<image::Frame>>,
{
let loops = match loops {
image::metadata::LoopCount::Infinite => 0,
image::metadata::LoopCount::Finite(n) => u64::from(n.get()),
};
Ok((animation_frames(frames, first_frame_only)?, metadata, loops))
}
fn decode_still_frames(
data: &[u8],
format: ImageFormat,
) -> Result<(Vec<Frame>, Metadata, u64), String> {
let reader = ImageReader::with_format(Cursor::new(data), format);
let (frame, metadata) = decode_still(reader)?;
Ok((vec![frame], metadata, 0))
}
fn decode_image_rs(
data: &[u8],
format: ImageFormat,
first_frame_only: bool,
) -> Result<dnrs_decoder, String> {
let codec = format!("image-rs/{format:?}");
let (frames, mut metadata, loops) = match format {
ImageFormat::Gif => {
let mut decoder =
image::codecs::gif::GifDecoder::new(Cursor::new(data))
.map_err(|e| e.to_string())?;
set_decoder_limits(&mut decoder)?;
let metadata = collect_metadata(&mut decoder)?;
let loops = decoder.loop_count();
decode_animation(
decoder.into_frames(),
loops,
metadata,
first_frame_only,
)?
}
ImageFormat::Png => {
let mut decoder =
image::codecs::png::PngDecoder::new(Cursor::new(data))
.map_err(|e| e.to_string())?;
set_decoder_limits(&mut decoder)?;
let metadata = collect_metadata(&mut decoder)?;
if decoder.is_apng().map_err(|e| e.to_string())? {
let apng = decoder.apng().map_err(|e| e.to_string())?;
let loops = apng.loop_count();
decode_animation(
apng.into_frames(),
loops,
metadata,
first_frame_only,
)?
} else {
decode_still_frames(data, format)?
}
}
ImageFormat::WebP => {
let mut decoder =
image::codecs::webp::WebPDecoder::new(Cursor::new(data))
.map_err(|e| e.to_string())?;
set_decoder_limits(&mut decoder)?;
let metadata = collect_metadata(&mut decoder)?;
if decoder.has_animation() {
let loops = decoder.loop_count();
decode_animation(
decoder.into_frames(),
loops,
metadata,
first_frame_only,
)?
} else {
decode_still_frames(data, format)?
}
}
ImageFormat::Tiff => {
return decoder_from_pages(
codec,
decode_tiff_pages(data, first_frame_only)?,
)
}
_ => decode_still_frames(data, format)?,
};
if format == ImageFormat::Png {
metadata.text = collect_png_text(data)?;
}
decoder_from_frames(codec, frames, loops, metadata)
}
#[cfg(feature = "jpeg2000")]
fn decode_jpeg2000(data: &[u8]) -> Result<dnrs_decoder, String> {
let decoder =
hayro_jpeg2000::integration::Jp2Decoder::new(Cursor::new(data))
.map_err(|e| e.to_string())?;
decoder_from_still("hayro-jpeg2000", decoder)
}
#[cfg(feature = "raw")]
fn frame_from_rendered(rendered: &impl Bitmap) -> Result<Frame, String> {
let (format, pixels) = if let Some(data) = rendered.data16() {
(dnrs_pixel_format::Rgb16Le, u16_to_le(data.to_vec()))
} else if let Some(data) = rendered.data8() {
(dnrs_pixel_format::Rgb8, data.to_vec())
} else {
return Err("libopenraw returned no pixels".into());
};
make_frame(rendered.width(), rendered.height(), format, pixels, 0)
}
#[cfg(feature = "raw")]
fn decode_raw(
rawfile: &libopenraw::RawFileHandle,
) -> Result<dnrs_decoder, String> {
// libopenraw has no allocation-limit API. Reject implausible dimensions
// from the container metadata before asking it to unpack the raw plane.
for key in [
"Exif.Image.ImageWidth",
"Exif.Image.ImageLength",
"Exif.Photo.PixelXDimension",
"Exif.Photo.PixelYDimension",
] {
if let Some(dimension) = rawfile
.metadata_value(key)
.and_then(|value| value.integer())
{
if dimension == 0 || dimension > MAX_DIMENSION {
return Err("invalid raw image dimensions".into());
}
}
}
let xmp = rawfile
.metadata_value("Exif.Image.ApplicationNotes")
.and_then(|value| match value {
Value::Bytes(bytes) => Some(bytes),
_ => None,
})
.unwrap_or_default();
let raw = rawfile.raw_data(false).map_err(|e| e.to_string())?;
if raw.width() == 0
|| raw.height() == 0
|| raw.width() > MAX_DIMENSION
|| raw.height() > MAX_DIMENSION
{
return Err("invalid raw image dimensions".into());
}
// Developing camera samples is format-specific processing rather than
// display policy. libopenraw produces tagged sRGB values here; libdn
// still owns conversion from that source space to the display target.
let rendered = raw
.rendered_image(&libopenraw::RenderingOptions::default())
.map_err(|e| e.to_string())?;
let metadata = Metadata {
xmp,
..Metadata::default()
};
let frames = vec![frame_from_rendered(&rendered)?];
let mut decoder =
decoder_from_frames("libopenraw".into(), frames, 0, metadata)?;
decoder.pages[0].orientation = rawfile.orientation();
Ok(decoder)
}
fn decode_image(
data: Vec<u8>,
first_frame_only: bool,
) -> Result<dnrs_decoder, String> {
image_extras::register();
if let Ok(format) = image::guess_format(&data) {
return decode_image_rs(&data, format, first_frame_only);
}
#[cfg(feature = "jpeg2000")]
if data.starts_with(&[0xff, 0x4f, 0xff, 0x51])
|| data.starts_with(&[0, 0, 0, 12, b'j', b'P', b' ', b' '])
{
return decode_jpeg2000(&data);
}
if let Ok(decoder) =
image::codecs::tga::TgaDecoder::new(Cursor::new(data.as_slice()))
{
return decoder_from_still("image-rs/Tga", decoder);
}
// XPM registers a signature with image, while XBM is C source with no
// magic number and has to be tried explicitly.
if let Ok(reader) =
ImageReader::new(Cursor::new(data.as_slice())).with_guessed_format()
{
if let Ok((frame, metadata)) = decode_still(reader) {
return decoder_from_frames(
"image-extras".into(),
vec![frame],
0,
metadata,
);
}
}
if let Ok(decoder) =
image_extras::xbm::XbmDecoder::new(Cursor::new(data.as_slice()))
{
return decoder_from_still("image-extras/XBM", decoder);
}
#[cfg(feature = "raw")]
if let Ok(rawfile) = libopenraw::rawfile_from_memory(data, None) {
return decode_raw(&rawfile);
}
Err("unrecognised image data".into())
}
fn decoder_from_pages(
codec: String,
decoded: Vec<(Frame, Metadata)>,
) -> Result<dnrs_decoder, String> {
let mut first = None;
let mut pages = Vec::new();
for (frame, metadata) in decoded {
pages.push(Page {
width: frame.width,
height: frame.height,
orientation: metadata.orientation,
loop_count: 0,
frames: vec![frame],
});
first.get_or_insert(metadata);
}
let first = first.ok_or("the image has no pages")?;
make_decoder(codec, pages, first)
}
fn decoder_from_frames(
codec: String,
frames: Vec<Frame>,
loop_count: u64,
metadata: Metadata,
) -> Result<dnrs_decoder, String> {
let first = frames
.first()
.ok_or_else(|| "the image has no frames".to_string())?;
let page = Page {
width: first.width,
height: first.height,
orientation: metadata.orientation,
loop_count,
frames,
};
make_decoder(codec, vec![page], metadata)
}
fn make_decoder(
codec: String,
pages: Vec<Page>,
metadata: Metadata,
) -> Result<dnrs_decoder, String> {
let text_storage = metadata
.text
.into_iter()
.map(|(key, value)| {
let key = CString::new(key.replace('\0', " "))
.map_err(|_| "invalid metadata key")?;
let value = CString::new(value.replace('\0', " "))
.map_err(|_| "invalid metadata value")?;
Ok((key, value))
})
.collect::<Result<Vec<_>, String>>()?;
let text = text_storage
.iter()
.map(|(key, value)| dnrs_text {
key: key.as_ptr(),
value: value.as_ptr(),
})
.collect();
Ok(dnrs_decoder {
codec: CString::new(codec).map_err(|_| "invalid codec name")?,
icc: metadata.icc,
exif: metadata.exif,
xmp: metadata.xmp,
_text_storage: text_storage,
text,
pages,
page_index: None,
frame_index: 0,
})
}
/// # Safety
///
/// `length`, when non-null, must point to writable memory.
#[no_mangle]
pub unsafe extern "C" fn dnrs_mime_types(
length: *mut usize,
) -> *const *const c_char {
static TYPES: OnceLock<Vec<usize>> = OnceLock::new();
let types = TYPES.get_or_init(|| {
MIME_TYPES.iter().map(|s| s.as_ptr() as usize).collect()
});
if !length.is_null() {
unsafe { *length = types.len() };
}
types.as_ptr().cast()
}
/// # Safety
///
/// `data` must point to `length` readable bytes, unless `length` is zero.
/// `error`, when non-null, must point to writable memory.
#[no_mangle]
pub unsafe extern "C" fn dnrs_decoder_new(
data: *const u8,
length: usize,
first_frame_only: bool,
error: *mut *mut dnrs_error,
) -> *mut dnrs_decoder {
ffi_result(error, ptr::null_mut(), || {
if data.is_null() && length != 0 {
return Err("null input pointer".into());
}
if length > isize::MAX as usize {
return Err("input is too large".into());
}
let input = if length == 0 {
Vec::new()
} else {
unsafe { std::slice::from_raw_parts(data, length) }.to_vec()
};
decode_image(input, first_frame_only)
.map(|decoder| Box::into_raw(Box::new(decoder)))
})
}
/// # Safety
///
/// All non-null arguments must point to live objects of their declared type.
#[no_mangle]
pub unsafe extern "C" fn dnrs_decoder_get_info(
decoder: *const dnrs_decoder,
info: *mut dnrs_document_info,
error: *mut *mut dnrs_error,
) -> bool {
ffi_result(error, false, || {
let decoder = unsafe { decoder.as_ref() }.ok_or("null decoder")?;
let info = unsafe { info.as_mut() }.ok_or("null document info")?;
*info = dnrs_document_info {
codec: decoder.codec.as_ptr(),
page_count: u32::try_from(decoder.pages.len()).unwrap_or(u32::MAX),
icc: dnrs_blob {
data: decoder.icc.as_ptr(),
length: decoder.icc.len(),
},
exif: dnrs_blob {
data: decoder.exif.as_ptr(),
length: decoder.exif.len(),
},
xmp: dnrs_blob {
data: decoder.xmp.as_ptr(),
length: decoder.xmp.len(),
},
text: decoder.text.as_ptr(),
text_length: decoder.text.len(),
};
Ok(true)
})
}
/// # Safety
///
/// All non-null arguments must point to live objects of their declared type.
#[no_mangle]
pub unsafe extern "C" fn dnrs_decoder_next_page(
decoder: *mut dnrs_decoder,
info: *mut dnrs_page_info,
error: *mut *mut dnrs_error,
) -> bool {
ffi_result(error, false, || {
let decoder = unsafe { decoder.as_mut() }.ok_or("null decoder")?;
let next = decoder.page_index.map_or(0, |n| n + 1);
let Some(page) = decoder.pages.get(next) else {
return Ok(false);
};
let info = unsafe { info.as_mut() }.ok_or("null page info")?;
*info = dnrs_page_info {
width: page.width,
height: page.height,
frame_count: u32::try_from(page.frames.len()).unwrap_or(u32::MAX),
orientation: page.orientation,
loop_count: page.loop_count,
};
decoder.page_index = Some(next);
decoder.frame_index = 0;
Ok(true)
})
}
/// # Safety
///
/// All non-null arguments must point to live objects of their declared type.
#[no_mangle]
pub unsafe extern "C" fn dnrs_decoder_next_frame(
decoder: *mut dnrs_decoder,
out: *mut dnrs_frame,
error: *mut *mut dnrs_error,
) -> bool {
ffi_result(error, false, || {
let decoder = unsafe { decoder.as_mut() }.ok_or("null decoder")?;
let index = decoder.frame_index;
let page = decoder
.page_index
.and_then(|n| decoder.pages.get_mut(n))
.ok_or("next_frame called before next_page")?;
let Some(frame) = page.frames.get_mut(index) else {
return Ok(false);
};
let out = unsafe { out.as_mut() }.ok_or("null frame")?;
// Iteration only ever moves forward, so give up our copy.
let pixels = mem::take(&mut frame.pixels).into_boxed_slice();
let length = pixels.len();
let data = Box::into_raw(pixels).cast::<u8>();
*out = dnrs_frame {
width: frame.width,
height: frame.height,
stride: frame.stride,
format: frame.format,
data,
length,
duration_ms: frame.duration_ms,
};
decoder.frame_index += 1;
Ok(true)
})
}
/// # Safety
///
/// `error`, when non-null, must point to a live `dnrs_error`.
#[no_mangle]
pub unsafe extern "C" fn dnrs_error_message(
error: *const dnrs_error,
) -> *const c_char {
unsafe { error.as_ref() }.map_or(ptr::null(), |e| e.message.as_ptr())
}
/// # Safety
///
/// `frame`, when non-null, must point to a frame returned by libdnrs.
#[no_mangle]
pub unsafe extern "C" fn dnrs_frame_clear(frame: *mut dnrs_frame) {
if let Some(frame) = unsafe { frame.as_mut() } {
if !frame.data.is_null() {
unsafe {
drop(Box::from_raw(ptr::slice_from_raw_parts_mut(
frame.data,
frame.length,
)));
}
}
frame.data = ptr::null_mut();
frame.length = 0;
}
}
/// # Safety
///
/// `decoder`, when non-null, must have been returned by libdnrs and not freed.
#[no_mangle]
pub unsafe extern "C" fn dnrs_decoder_free(decoder: *mut dnrs_decoder) {
if !decoder.is_null() {
unsafe { drop(Box::from_raw(decoder)) };
}
}
/// # Safety
///
/// `error`, when non-null, must have been returned by libdnrs and not freed.
#[no_mangle]
pub unsafe extern "C" fn dnrs_error_free(error: *mut dnrs_error) {
if !error.is_null() {
unsafe { drop(Box::from_raw(error)) };
}
}
#[cfg(test)]
mod tests;
+86
View File
@@ -0,0 +1,86 @@
//
// libdnrs.h: image-rs and other decoders exposed through a small C ABI
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct dnrs_decoder;
struct dnrs_error;
struct dnrs_blob {
const uint8_t *data;
size_t length;
};
struct dnrs_text {
const char *key;
const char *value;
};
enum dnrs_pixel_format {
DNRS_PIXEL_GRAY8,
DNRS_PIXEL_GRAY_ALPHA8,
DNRS_PIXEL_RGB8,
DNRS_PIXEL_RGBA8,
DNRS_PIXEL_GRAY16LE,
DNRS_PIXEL_GRAY_ALPHA16LE,
DNRS_PIXEL_RGB16LE,
DNRS_PIXEL_RGBA16LE,
};
struct dnrs_document_info {
const char *codec;
uint32_t page_count;
struct dnrs_blob icc;
struct dnrs_blob exif;
struct dnrs_blob xmp;
const struct dnrs_text *text;
size_t text_length;
};
struct dnrs_page_info {
uint32_t width;
uint32_t height;
uint32_t frame_count;
uint32_t orientation;
uint64_t loop_count;
};
struct dnrs_frame {
uint32_t width;
uint32_t height;
size_t stride;
enum dnrs_pixel_format format;
uint8_t *data;
size_t length;
uint64_t duration_ms;
};
const char *const *dnrs_mime_types(size_t *length);
struct dnrs_decoder *dnrs_decoder_new(const uint8_t *data, size_t length,
bool first_frame_only, struct dnrs_error **error);
bool dnrs_decoder_get_info(const struct dnrs_decoder *decoder,
struct dnrs_document_info *info, struct dnrs_error **error);
bool dnrs_decoder_next_page(struct dnrs_decoder *decoder,
struct dnrs_page_info *page, struct dnrs_error **error);
bool dnrs_decoder_next_frame(struct dnrs_decoder *decoder,
struct dnrs_frame *frame, struct dnrs_error **error);
const char *dnrs_error_message(const struct dnrs_error *error);
void dnrs_frame_clear(struct dnrs_frame *frame);
void dnrs_decoder_free(struct dnrs_decoder *decoder);
void dnrs_error_free(struct dnrs_error *error);
#ifdef __cplusplus
}
#endif
+3
View File
@@ -0,0 +1,3 @@
hard_tabs = true
max_width = 80
tab_spaces = 4
+485
View File
@@ -0,0 +1,485 @@
use super::*;
use std::ffi::CStr;
struct Decoder(*mut dnrs_decoder);
impl Drop for Decoder {
fn drop(&mut self) {
unsafe { dnrs_decoder_free(self.0) };
}
}
fn error_message(error: *mut dnrs_error) -> String {
assert!(!error.is_null());
let message = unsafe {
CStr::from_ptr(dnrs_error_message(error))
.to_string_lossy()
.into_owned()
};
unsafe { dnrs_error_free(error) };
message
}
fn open(data: &[u8], first_frame_only: bool) -> Decoder {
let mut error = ptr::null_mut();
let decoder = unsafe {
dnrs_decoder_new(
data.as_ptr(),
data.len(),
first_frame_only,
&mut error,
)
};
if decoder.is_null() {
panic!("{}", error_message(error));
}
assert!(error.is_null());
Decoder(decoder)
}
fn document_info(decoder: &Decoder) -> dnrs_document_info {
let mut info = unsafe { std::mem::zeroed() };
let mut error = ptr::null_mut();
assert!(unsafe { dnrs_decoder_get_info(decoder.0, &mut info, &mut error) });
assert!(error.is_null());
info
}
fn next_page(decoder: &Decoder) -> dnrs_page_info {
let mut info = unsafe { std::mem::zeroed() };
let mut error = ptr::null_mut();
assert!(unsafe {
dnrs_decoder_next_page(decoder.0, &mut info, &mut error)
});
assert!(error.is_null());
info
}
fn next_frame(decoder: &Decoder) -> dnrs_frame {
let mut frame = unsafe { std::mem::zeroed() };
let mut error = ptr::null_mut();
assert!(unsafe {
dnrs_decoder_next_frame(decoder.0, &mut frame, &mut error)
});
assert!(error.is_null());
frame
}
fn png(
color: png::ColorType,
depth: png::BitDepth,
pixels: &[u8],
text: bool,
exif: bool,
) -> Vec<u8> {
let mut data = Vec::new();
{
let mut info = png::Info::with_size(2, 1);
info.color_type = color;
info.bit_depth = depth;
if exif {
info.exif_metadata = Some(
b"II\x2a\0\x08\0\0\0\x01\0\x12\x01\x03\0\x01\0\0\0\x06\0\0\0\0\0\0\0"
.as_slice()
.into(),
);
}
let encoder = png::Encoder::with_info(&mut data, info).unwrap();
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(pixels).unwrap();
if text {
let chunk = png::text_metadata::TEXtChunk::new("prompt", "hello");
writer.write_text_chunk(&chunk).unwrap();
}
writer.finish().unwrap();
}
data
}
#[test]
fn abi_rejects_bad_calls_and_input() {
let mut error = ptr::null_mut();
let decoder =
unsafe { dnrs_decoder_new(ptr::null(), 1, false, &mut error) };
assert!(decoder.is_null());
assert_eq!(error_message(error), "null input pointer");
let junk = b"this is not an image";
error = ptr::null_mut();
let decoder = unsafe {
dnrs_decoder_new(junk.as_ptr(), junk.len(), false, &mut error)
};
assert!(decoder.is_null());
assert_eq!(error_message(error), "unrecognised image data");
let oversized = br#"#define huge_width 65536
#define huge_height 1
static unsigned char huge_bits[] = { 0x00 };
"#;
error = ptr::null_mut();
let decoder = unsafe {
dnrs_decoder_new(oversized.as_ptr(), oversized.len(), false, &mut error)
};
assert!(decoder.is_null());
assert!(!error.is_null());
unsafe { dnrs_error_free(error) };
let data = png(
png::ColorType::Rgba,
png::BitDepth::Eight,
&[1, 2, 3, 4, 5, 6, 7, 8],
false,
false,
);
let decoder = open(&data, false);
let mut frame = unsafe { std::mem::zeroed() };
error = ptr::null_mut();
assert!(!unsafe {
dnrs_decoder_next_frame(decoder.0, &mut frame, &mut error)
});
assert_eq!(error_message(error), "next_frame called before next_page");
}
#[test]
fn png_rgba8_metadata_and_iteration() {
let data = png(
png::ColorType::Rgba,
png::BitDepth::Eight,
&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80],
true,
true,
);
let decoder = open(&data, false);
let document = document_info(&decoder);
assert_eq!(document.page_count, 1);
assert!(!document.exif.data.is_null());
assert!(document.exif.length >= 26);
assert_eq!(document.text_length, 1);
let text = unsafe { &*document.text };
assert_eq!(unsafe { CStr::from_ptr(text.key) }.to_bytes(), b"prompt");
assert_eq!(unsafe { CStr::from_ptr(text.value) }.to_bytes(), b"hello");
let page = next_page(&decoder);
assert_eq!((page.width, page.height, page.frame_count), (2, 1, 1));
assert_eq!(page.orientation, 6);
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Rgba8 as u32);
assert_eq!(frame.stride, 8);
assert_eq!(frame.length, 8);
assert_eq!(
unsafe { std::slice::from_raw_parts(frame.data, frame.length) },
&[0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]
);
unsafe { dnrs_frame_clear(&mut frame) };
assert!(frame.data.is_null());
let mut error = ptr::null_mut();
assert!(!unsafe {
dnrs_decoder_next_frame(decoder.0, &mut frame, &mut error)
});
assert!(error.is_null());
}
#[test]
fn png_rgba16_is_little_endian() {
let data = png(
png::ColorType::Rgba,
png::BitDepth::Sixteen,
&[
0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07, 0x08,
],
false,
false,
);
let decoder = open(&data, false);
next_page(&decoder);
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Rgba16Le as u32);
assert_eq!(
unsafe { std::slice::from_raw_parts(frame.data, frame.length) },
&[
0x34, 0x12, 0x78, 0x56, 0xbc, 0x9a, 0xf0, 0xde, 0x02, 0x01, 0x04,
0x03, 0x06, 0x05, 0x08, 0x07,
]
);
unsafe { dnrs_frame_clear(&mut frame) };
}
#[test]
fn gif_animation_and_first_frame_only() {
use image::codecs::gif::{GifEncoder, Repeat};
use image::{Delay, Frame, RgbaImage};
let mut data = Vec::new();
{
let mut encoder = GifEncoder::new(&mut data);
encoder.set_repeat(Repeat::Finite(3)).unwrap();
for (pixel, delay) in [([255, 0, 0, 255], 20), ([0, 0, 255, 255], 30)] {
let image = RgbaImage::from_raw(1, 1, pixel.to_vec()).unwrap();
encoder
.encode_frame(Frame::from_parts(
image,
0,
0,
Delay::from_numer_denom_ms(delay, 1),
))
.unwrap();
}
}
let decoder = open(&data, false);
let page = next_page(&decoder);
assert_eq!(page.frame_count, 2);
assert_eq!(page.loop_count, 3);
let mut first = next_frame(&decoder);
let mut second = next_frame(&decoder);
assert_eq!(first.duration_ms, 20);
assert_eq!(second.duration_ms, 30);
unsafe {
dnrs_frame_clear(&mut first);
dnrs_frame_clear(&mut second);
}
let decoder = open(&data, true);
assert_eq!(next_page(&decoder).frame_count, 1);
}
#[test]
fn tiff_pages_are_separate() {
use tiff::encoder::{colortype, TiffEncoder};
let mut cursor = Cursor::new(Vec::new());
{
let mut encoder = TiffEncoder::new(&mut cursor).unwrap();
encoder
.write_image::<colortype::RGB8>(1, 1, &[255, 0, 0])
.unwrap();
encoder
.write_image::<colortype::RGB8>(1, 1, &[0, 0, 255])
.unwrap();
}
let decoder = open(&cursor.into_inner(), false);
assert_eq!(document_info(&decoder).page_count, 2);
for expected in [[255, 0, 0], [0, 0, 255]] {
assert_eq!(next_page(&decoder).frame_count, 1);
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Rgb8 as u32);
assert_eq!(
unsafe { std::slice::from_raw_parts(frame.data, frame.length) },
&expected
);
unsafe { dnrs_frame_clear(&mut frame) };
}
}
#[test]
fn image_extras_xbm_and_xpm_are_sniffed() {
let xbm = br#"#define dot_width 1
#define dot_height 1
static unsigned char dot_bits[] = { 0x01 };
"#;
let decoder = open(xbm, false);
assert_eq!(next_page(&decoder).width, 1);
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Gray8 as u32);
unsafe { dnrs_frame_clear(&mut frame) };
let xpm = br#"/* XPM */
static char *dot[] = {
"1 1 1 1",
"a c #ff0000",
"a"
};
"#;
let decoder = open(xpm, false);
assert_eq!(next_page(&decoder).width, 1);
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Rgba16Le as u32);
assert_eq!(
unsafe { std::slice::from_raw_parts(frame.data, frame.length) },
&[255, 255, 0, 0, 0, 0, 255, 255]
);
unsafe { dnrs_frame_clear(&mut frame) };
}
fn verify_encoded_image(data: &[u8]) {
let decoder = open(data, false);
assert_eq!(document_info(&decoder).page_count, 1);
let page = next_page(&decoder);
assert_eq!((page.width, page.height, page.frame_count), (1, 1, 1));
let mut frame = next_frame(&decoder);
assert!(!frame.data.is_null());
assert!(frame.length > 0);
unsafe { dnrs_frame_clear(&mut frame) };
}
#[test]
fn image_rs_still_formats_are_decodable() {
use image::{ExtendedColorType, ImageEncoder};
let rgb = [0x12, 0x34, 0x56];
let rgba = [0x12, 0x34, 0x56, 0x78];
let rgba16 = [0x1234_u16, 0x5678, 0x9abc, 0xdef0]
.into_iter()
.flat_map(u16::to_ne_bytes)
.collect::<Vec<_>>();
let rgb32f = [0.25_f32, 0.5, 0.75]
.into_iter()
.flat_map(f32::to_ne_bytes)
.collect::<Vec<_>>();
let rgba32f = [0.25_f32, 0.5, 0.75, 1.0]
.into_iter()
.flat_map(f32::to_ne_bytes)
.collect::<Vec<_>>();
let mut data = Vec::new();
image::codecs::bmp::BmpEncoder::new(&mut data)
.write_image(&rgb, 1, 1, ExtendedColorType::Rgb8)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::farbfeld::FarbfeldEncoder::new(&mut data)
.write_image(&rgba16, 1, 1, ExtendedColorType::Rgba16)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::hdr::HdrEncoder::new(&mut data)
.write_image(&rgb32f, 1, 1, ExtendedColorType::Rgb32F)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::ico::IcoEncoder::new(&mut data)
.write_image(&rgba, 1, 1, ExtendedColorType::Rgba8)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut data, 100)
.write_image(&rgb, 1, 1, ExtendedColorType::Rgb8)
.unwrap();
verify_encoded_image(&data);
let mut exr = Cursor::new(Vec::new());
image::codecs::openexr::OpenExrEncoder::new(&mut exr)
.write_image(&rgba32f, 1, 1, ExtendedColorType::Rgba32F)
.unwrap();
verify_encoded_image(exr.get_ref());
data.clear();
image::codecs::pnm::PnmEncoder::new(&mut data)
.write_image(&rgb, 1, 1, ExtendedColorType::Rgb8)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::qoi::QoiEncoder::new(&mut data)
.write_image(&rgba, 1, 1, ExtendedColorType::Rgba8)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::tga::TgaEncoder::new(&mut data)
.write_image(&rgb, 1, 1, ExtendedColorType::Rgb8)
.unwrap();
verify_encoded_image(&data);
data.clear();
image::codecs::webp::WebPEncoder::new_lossless(&mut data)
.write_image(&rgba, 1, 1, ExtendedColorType::Rgba8)
.unwrap();
verify_encoded_image(&data);
}
#[test]
fn dds_dxt1_is_decodable() {
fn push_u32(data: &mut Vec<u8>, value: u32) {
data.extend_from_slice(&value.to_le_bytes());
}
let mut data = b"DDS ".to_vec();
push_u32(&mut data, 124);
push_u32(&mut data, 0x1007);
push_u32(&mut data, 4);
push_u32(&mut data, 4);
push_u32(&mut data, 8);
push_u32(&mut data, 0);
push_u32(&mut data, 0);
for _ in 0..11 {
push_u32(&mut data, 0);
}
push_u32(&mut data, 32);
push_u32(&mut data, 4);
data.extend_from_slice(b"DXT1");
for _ in 0..5 {
push_u32(&mut data, 0);
}
push_u32(&mut data, 0x1000);
for _ in 0..4 {
push_u32(&mut data, 0);
}
data.extend_from_slice(&[0x00, 0xf8, 0, 0, 0, 0, 0, 0]);
let decoder = open(&data, false);
let page = next_page(&decoder);
assert_eq!((page.width, page.height), (4, 4));
let mut frame = next_frame(&decoder);
assert_eq!(frame.format as u32, dnrs_pixel_format::Rgb8 as u32);
assert_eq!(
unsafe { std::slice::from_raw_parts(frame.data, 3) },
&[255, 0, 0]
);
unsafe { dnrs_frame_clear(&mut frame) };
}
#[cfg(feature = "jpeg2000")]
#[test]
fn jpeg2000_is_decodable() {
// A 1x1 red JP2 encoded by OpenJPEG 2.5.4.
let data = [
0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a,
0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70, 0x6a, 0x70, 0x32, 0x20,
0x00, 0x00, 0x00, 0x00, 0x6a, 0x70, 0x32, 0x20, 0x00, 0x00, 0x00, 0x2d,
0x6a, 0x70, 0x32, 0x68, 0x00, 0x00, 0x00, 0x16, 0x69, 0x68, 0x64, 0x72,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x03, 0x0f, 0x07,
0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x63, 0x6f, 0x6c, 0x72, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x93, 0x6a, 0x70, 0x32,
0x63, 0xff, 0x4f, 0xff, 0x51, 0x00, 0x2f, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x0f, 0x01, 0x01, 0x0f, 0x01,
0x01, 0x0f, 0x01, 0x01, 0xff, 0x52, 0x00, 0x0c, 0x00, 0x00, 0x00, 0x01,
0x01, 0x00, 0x04, 0x04, 0x00, 0x01, 0xff, 0x5c, 0x00, 0x04, 0x40, 0x80,
0xff, 0x64, 0x00, 0x25, 0x00, 0x01, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65,
0x64, 0x20, 0x62, 0x79, 0x20, 0x4f, 0x70, 0x65, 0x6e, 0x4a, 0x50, 0x45,
0x47, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x20, 0x32, 0x2e,
0x35, 0x2e, 0x34, 0xff, 0x90, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
0x1b, 0x00, 0x01, 0xff, 0x93, 0xcf, 0xfc, 0x30, 0x08, 0x07, 0xb7, 0x80,
0xdf, 0xf8, 0x90, 0x10, 0x01, 0x3f, 0xff, 0xd9,
];
verify_encoded_image(&data);
}
#[test]
fn every_png_truncation_is_an_error() {
let data = png(
png::ColorType::Rgba,
png::BitDepth::Eight,
&[1, 2, 3, 4, 5, 6, 7, 8],
false,
false,
);
for length in 0..data.len() {
let mut error = ptr::null_mut();
let decoder = unsafe {
dnrs_decoder_new(data.as_ptr(), length, false, &mut error)
};
assert!(decoder.is_null(), "accepted PNG prefix of {length} bytes");
assert!(!error.is_null());
unsafe { dnrs_error_free(error) };
}
}
+1 -3
View File
@@ -172,7 +172,7 @@ cd "$builddir/ucrt64"
dbsync
fetch $pkg-qt6-base $pkg-vulkan-loader $pkg-vulkan-headers $pkg-libwebp \
$pkg-libjpeg-turbo $pkg-libheif $pkg-libjxl $pkg-openjpeg2 $pkg-libraw \
$pkg-jxrlib $pkg-glycin $pkg-zlib $pkg-shared-mime-info $pkg-gcc-libs \
$pkg-jxrlib $pkg-libwmf $pkg-zlib $pkg-shared-mime-info $pkg-gcc-libs \
#$pkg-perl $pkg-perl-win32-api
verify
extract
@@ -181,8 +181,6 @@ configure "$@"
cd -
toolchain=submodules/liberty/cmake/toolchains/MinGW-w64-x64.cmake
# Glycin pulls in GTK 4, and with it the GdkPixbuf that it replaces.
cmake -B "$builddir" -DCMAKE_TOOLCHAIN_FILE=$toolchain \
-DDAWN_WITH_GDKPIXBUF=OFF \
-DCMAKE_AUTOMOC_EXECUTABLE=$builddir/ucrt64/automoc \
-DCMAKE_AUTORCC_EXECUTABLE=$builddir/ucrt64/autorcc "$@"
+9 -24
View File
@@ -118,6 +118,15 @@ modules:
url: https://www.libraw.org/data/LibRaw-0.22.2.tar.gz
sha256: de86b035655accff8d4010f1a221fdf50d353cb7b1422ba26f14a0db92612cfa
# Only the library and its GD image device are retained by the application.
- name: libwmf
config-opts:
- --disable-static
sources:
- type: archive
url: https://github.com/caolanm/libwmf/archive/refs/tags/v0.2.15.tar.gz
sha256: 379ae041dd53ab35f55c9b73a78a31f1ce530e925023c31607b05f2199d867e6
# Dawn looks for Debian's flat include/jxrlib, upstream nests by component.
- name: jxrlib
buildsystem: simple
@@ -129,27 +138,3 @@ modules:
url: https://github.com/mircomir/jxrlib.git
tag: '1.4.2'
commit: 568cc2c4e09cf04412bc53a3e03e678ecd1d4693
# Only glycin-image-rs: HEIF, JPEG XL, SVG and raw have their own loaders.
# Unlike resvg, the release tarball does not vendor its crates.
- name: glycin
buildsystem: meson
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
build-args:
- --share=network
env:
CARGO_HOME: /run/build/glycin/cargo
config-opts:
- -Dglycin-loaders=true
- -Dloaders=glycin-image-rs
- -Dlibglycin=true
- -Dlibglycin-gtk4=false
- -Dglycin-thumbnailer=false
- -Dintrospection=false
- -Dvapi=false
- -Dtests=false
sources:
- type: archive
url: https://download.gnome.org/sources/glycin/2.1/glycin-2.1.5.tar.xz
sha256: 6c09757ee906330a60b6705753aa56bca007ad219b95e6e3537510d41bc341c8
+7 -3
View File
@@ -2,6 +2,8 @@ app-id: name.janouch.Dawn
runtime: org.freedesktop.Platform
runtime-version: '26.08'
sdk: org.freedesktop.Sdk
sdk-extensions:
- org.freedesktop.Sdk.Extension.rust-stable
base: name.janouch.Dawn.BaseApp
base-version: stable
command: dn
@@ -32,8 +34,7 @@ finish-args:
cleanup-commands:
- find /app/bin -mindepth 1 ! -name 'dn' ! -name 'dn-*'
-exec rm -rf {} +
- find /app/libexec -mindepth 1 -maxdepth 1 ! -name glycin-loaders
-exec rm -rf {} +
- rm -rf /app/libexec
- rm -f /app/lib/libcolorhug.so* /app/lib/libgusb.so* /app/lib/libraw_r.so*
- rm -rf /app/plugins/generic /app/plugins/imageformats
@@ -55,12 +56,15 @@ modules:
buildsystem: cmake-ninja
builddir: true
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
build-args:
- --share=network
env:
CARGO_HOME: /run/build/dawn/cargo
QT_QPA_PLATFORM: offscreen
config-opts:
- -DCMAKE_BUILD_TYPE=Release
- -DBUILD_TESTING=OFF
- -DDAWN_WITH_GDKPIXBUF=OFF
- -DFETCHCONTENT_SOURCE_DIR_LCMS2=/run/build/dawn/lcms2
- -DFETCHCONTENT_SOURCE_DIR_JPEGQS=/run/build/dawn/jpegqs
sources:
+7 -1
View File
@@ -71,6 +71,12 @@ set_tests_properties(channels PROPERTIES
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)
if (DAWN_WITH_DNRS)
add_executable(test-dnrs test-dnrs.cpp)
target_link_libraries(test-dnrs PRIVATE dawn::libdn)
add_test(NAME dnrs COMMAND test-dnrs)
endif()
if (APPLE AND DAWN_WITH_IMAGEIO)
add_executable(test-imageio test-imageio.cpp)
target_link_libraries(test-imageio PRIVATE dawn::libdn)
@@ -136,7 +142,7 @@ add_executable(test-shm test-shm.cpp)
target_link_libraries(test-shm PRIVATE dawn::libdn)
add_test(NAME shm COMMAND test-shm)
# The main desktop file is rewritten by dn-update-desktop-files on install.
# The main desktop file has its MimeType= rewritten during the build.
find_program(DAWN_DESKTOP_FILE_VALIDATE desktop-file-validate)
if(DAWN_DESKTOP_FILE_VALIDATE)
foreach(desktop
+66
View File
@@ -0,0 +1,66 @@
//
// test-dnrs.cpp: verify Dawn's libdnrs fallback
//
// Copyright The Dawn Authors
// SPDX-License-Identifier: MPL-2.0
//
#include "libdn/libdn-loaders.h"
#include "libdn/libdn.h"
#include <cstdint>
#include <span>
#include <string_view>
int
main()
{
const uint8_t pnm[] = "P6\n1 1\n255\n\x12\x34\x56";
dawn::OpenContext context;
context.first_frame_only = false;
dawn::Error error;
dawn::ImagePtr image = dawn::detail::load_dnrs(
std::span(pnm, sizeof pnm - 1), context, &error);
if (!image || error)
return 1;
if (image->width != 1 || image->height != 1 || image->frame_next ||
image->page_next)
return 2;
const uint16_t *pixel = dawn::row_u16(*image, 0);
if (pixel[0] != 0x5656 || pixel[1] != 0x3434 || pixel[2] != 0x1212 ||
pixel[3] != 0xffff)
return 3;
image =
dawn::open_from_data(std::span(pnm, sizeof pnm - 1), context, &error);
if (!image || error || !image->loader ||
std::string_view(image->loader) != "Wuffs")
return 4;
const uint8_t xbm[] = "#define dot_width 1\n"
"#define dot_height 1\n"
"static unsigned char dot_bits[] = { 0x01 };\n";
image =
dawn::open_from_data(std::span(xbm, sizeof xbm - 1), context, &error);
if (!image || error || !image->loader ||
std::string_view(image->loader) == "")
return 5;
const uint8_t pnm16[] = "P6\n1 1\n65535\n\x12\x34\x56\x78\x9a\xbc";
image = dawn::detail::load_dnrs(
std::span(pnm16, sizeof pnm16 - 1), context, &error);
if (!image || error)
return 6;
pixel = dawn::row_u16(*image, 0);
if (pixel[0] != 0x9abc || pixel[1] != 0x5678 || pixel[2] != 0x1234 ||
pixel[3] != 0xffff)
return 7;
const uint8_t junk[] = "not an image";
error = {};
image = dawn::detail::load_dnrs(
std::span(junk, sizeof junk - 1), context, &error);
if (image || !error)
return 8;
return 0;
}