Replace Glycin and GdkPixbuf
From one kind of pain to another kind of pain.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
/.flatpak/
|
||||
/build/
|
||||
/build-*/
|
||||
/libdnrs/Cargo.lock
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
+17
-26
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
hard_tabs = true
|
||||
max_width = 80
|
||||
tab_spaces = 4
|
||||
@@ -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
@@ -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 "$@"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user