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
|
- name: Install dependencies
|
||||||
run: |
|
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
|
shared-mime-info vulkan-loader jpeg-turbo webp zlib
|
||||||
|
|
||||||
- name: Build package
|
- name: Build package
|
||||||
|
|||||||
@@ -2,5 +2,6 @@
|
|||||||
/.flatpak/
|
/.flatpak/
|
||||||
/build/
|
/build/
|
||||||
/build-*/
|
/build-*/
|
||||||
|
/libdnrs/Cargo.lock
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.cache/
|
.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(LIBJXL "JPEG XL support" libjxl)
|
||||||
dawn_optional_pkg(OPENJPEG "JPEG 2000 support" libopenjp2)
|
dawn_optional_pkg(OPENJPEG "JPEG 2000 support" libopenjp2)
|
||||||
dawn_optional_pkg(LIBTIFF "TIFF support" libtiff-4)
|
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(LIBRAW "LibRaw support" libraw)
|
||||||
|
dawn_optional_pkg(LIBWMF "WMF support" libwmf)
|
||||||
|
|
||||||
# jxrlib ships no pkg-config file, and its headers include one another by
|
find_program(CARGO_EXECUTABLE cargo)
|
||||||
# bare name, so the whole package directory has to go on the include path.
|
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_path(JXRLIB_INCLUDE_DIR JXRGlue.h PATH_SUFFIXES jxrlib)
|
||||||
find_library(JXRLIB_GLUE_LIBRARY jxrglue)
|
find_library(JXRLIB_GLUE_LIBRARY jxrglue)
|
||||||
find_library(JXRLIB_CORE_LIBRARY jpegxr)
|
find_library(JXRLIB_CORE_LIBRARY jpegxr)
|
||||||
@@ -128,7 +136,7 @@ if (DAWN_WITH_JXRLIB AND NOT JXRLIB_FOUND)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (APPLE)
|
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)
|
option(DAWN_WITH_CGPDF "Core Graphics PDF page rendering" ON)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
@@ -191,7 +199,7 @@ else()
|
|||||||
target_include_directories(lcms2_threaded PUBLIC
|
target_include_directories(lcms2_threaded PUBLIC
|
||||||
$<BUILD_INTERFACE:${lcms2_SOURCE_DIR}/plugins/threaded/include>)
|
$<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?
|
# What gives? Clear in LINK_INTERFACE_LIBRARIES?
|
||||||
target_link_libraries(dawn-lcms2 INTERFACE
|
target_link_libraries(dawn-lcms2 INTERFACE
|
||||||
lcms2 lcms2_fast_float lcms2_threaded)
|
lcms2 lcms2_fast_float lcms2_threaded)
|
||||||
@@ -244,6 +252,9 @@ if (CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|||||||
$<$<COMPILE_LANGUAGE:CXX>:-Wnon-virtual-dtor>)
|
$<$<COMPILE_LANGUAGE:CXX>:-Wnon-virtual-dtor>)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if (DAWN_WITH_DNRS)
|
||||||
|
add_subdirectory(libdnrs)
|
||||||
|
endif()
|
||||||
add_subdirectory(libdn)
|
add_subdirectory(libdn)
|
||||||
add_subdirectory(dnimaged)
|
add_subdirectory(dnimaged)
|
||||||
add_subdirectory(dnthumbd)
|
add_subdirectory(dnthumbd)
|
||||||
@@ -304,26 +315,6 @@ else()
|
|||||||
"${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml" @ONLY)
|
"${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml" @ONLY)
|
||||||
install(FILES "${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml"
|
install(FILES "${PROJECT_BINARY_DIR}/${DAWN_NAMESPACE}.metainfo.xml"
|
||||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo")
|
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()
|
endif()
|
||||||
|
|
||||||
set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE")
|
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_LCMS2_THREADED
|
||||||
#cmakedefine01 DAWN_WITH_JPEG_QS
|
#cmakedefine01 DAWN_WITH_JPEG_QS
|
||||||
#cmakedefine01 DAWN_WITH_LIBRAW
|
#cmakedefine01 DAWN_WITH_LIBRAW
|
||||||
|
#cmakedefine01 DAWN_WITH_LIBWMF
|
||||||
#cmakedefine01 DAWN_WITH_LIBRSVG
|
#cmakedefine01 DAWN_WITH_LIBRSVG
|
||||||
#cmakedefine01 DAWN_WITH_XCURSOR
|
#cmakedefine01 DAWN_WITH_XCURSOR
|
||||||
#cmakedefine01 DAWN_WITH_LIBHEIF
|
#cmakedefine01 DAWN_WITH_LIBHEIF
|
||||||
@@ -29,8 +30,7 @@
|
|||||||
#cmakedefine01 DAWN_WITH_OPENJPEG
|
#cmakedefine01 DAWN_WITH_OPENJPEG
|
||||||
#cmakedefine01 DAWN_WITH_LIBTIFF
|
#cmakedefine01 DAWN_WITH_LIBTIFF
|
||||||
#cmakedefine01 DAWN_WITH_JXRLIB
|
#cmakedefine01 DAWN_WITH_JXRLIB
|
||||||
#cmakedefine01 DAWN_WITH_GLYCIN
|
#cmakedefine01 DAWN_WITH_DNRS
|
||||||
#cmakedefine01 DAWN_WITH_GDKPIXBUF
|
|
||||||
#cmakedefine01 DAWN_WITH_IMAGEIO
|
#cmakedefine01 DAWN_WITH_IMAGEIO
|
||||||
#cmakedefine01 DAWN_WITH_CGPDF
|
#cmakedefine01 DAWN_WITH_CGPDF
|
||||||
|
|
||||||
|
|||||||
+23
-22
@@ -146,9 +146,9 @@ if (APPLE)
|
|||||||
# The abstract type the vendor formats conform to.
|
# The abstract type the vendor formats conform to.
|
||||||
list(APPEND utis public.camera-raw-image)
|
list(APPEND utis public.camera-raw-image)
|
||||||
endif()
|
endif()
|
||||||
if (DAWN_WITH_GDKPIXBUF)
|
if (DAWN_WITH_DNRS)
|
||||||
|
# The rest of what it adds, ImageIO knows about as well.
|
||||||
list(APPEND utis
|
list(APPEND utis
|
||||||
com.apple.quicktime-image
|
|
||||||
com.microsoft.ico
|
com.microsoft.ico
|
||||||
public.xbitmap-image
|
public.xbitmap-image
|
||||||
)
|
)
|
||||||
@@ -230,28 +230,29 @@ else()
|
|||||||
RENAME "${DAWN_NAMESPACE}.svg")
|
RENAME "${DAWN_NAMESPACE}.svg")
|
||||||
install(DIRECTORY "${DN_ICON_BASE}"
|
install(DIRECTORY "${DN_ICON_BASE}"
|
||||||
DESTINATION "${CMAKE_INSTALL_DATADIR}")
|
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"
|
"${CMAKE_CURRENT_SOURCE_DIR}/${DAWN_NAMESPACE}.Browse.desktop"
|
||||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications")
|
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()
|
||||||
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
|
.Optional dependencies
|
||||||
```sh
|
```sh
|
||||||
apt install librsvg2-dev libxcursor-dev libheif-dev libtiff-dev libraw-dev \
|
apt install cargo librsvg2-dev libxcursor-dev libheif-dev libtiff-dev \
|
||||||
libjxl-dev libopenjp2-7-dev libjxr-dev libgdk-pixbuf-2.0-dev # libglycin-2-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
|
.libresvg-dev is required but not often packaged, thus you may need to build it
|
||||||
```sh
|
```sh
|
||||||
apt install cargo cargo-c
|
apt install cargo cargo-c
|
||||||
|
|||||||
+2
-3
@@ -36,9 +36,8 @@ Options
|
|||||||
no longer be found.
|
no longer be found.
|
||||||
|
|
||||||
*--list-supported-media-types*::
|
*--list-supported-media-types*::
|
||||||
Output supported media types and exit. This is used by a script to update
|
*--list-supported-extensions*::
|
||||||
the list of MIME types within *dn*'s desktop file when the list
|
Output supported media types or file extensions and exit.
|
||||||
of GdkPixbuf loaders changes.
|
|
||||||
|
|
||||||
*-V*, *--version*::
|
*-V*, *--version*::
|
||||||
Output version information and exit.
|
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,
|
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[
|
there is an obscure project called https://snisurset.net/code/abydos/about.html[
|
||||||
Abydos] which gets quite exhaustive in that regard. The current general
|
Abydos] which gets quite exhaustive in that regard.
|
||||||
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
|
The general fallback in my previous viewer used to be GdkPixbuf. Today, that
|
||||||
instead as the only thing no other loader could do (the Raw Image Extension
|
would mostly defer to Glycin, which in turn rather trivially encapsulates
|
||||||
on Microsoft Store uses LibRaw).
|
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.
|
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_sources(libdn PRIVATE load-libraw.cpp)
|
||||||
target_link_libraries(libdn PRIVATE PkgConfig::LIBRAW)
|
target_link_libraries(libdn PRIVATE PkgConfig::LIBRAW)
|
||||||
endif()
|
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)
|
target_sources(libdn PRIVATE load-resvg.cpp)
|
||||||
if (DAWN_WITH_LIBRSVG)
|
if (DAWN_WITH_LIBRSVG)
|
||||||
target_sources(libdn PRIVATE load-librsvg.cpp)
|
target_sources(libdn PRIVATE load-librsvg.cpp)
|
||||||
@@ -68,13 +72,23 @@ if (DAWN_WITH_JXRLIB)
|
|||||||
target_link_libraries(libdn PRIVATE
|
target_link_libraries(libdn PRIVATE
|
||||||
"${JXRLIB_GLUE_LIBRARY}" "${JXRLIB_CORE_LIBRARY}")
|
"${JXRLIB_GLUE_LIBRARY}" "${JXRLIB_CORE_LIBRARY}")
|
||||||
endif()
|
endif()
|
||||||
if (DAWN_WITH_GLYCIN)
|
if (DAWN_WITH_DNRS)
|
||||||
target_sources(libdn PRIVATE load-glycin.cpp)
|
target_sources(libdn PRIVATE load-dnrs.cpp)
|
||||||
target_link_libraries(libdn PRIVATE PkgConfig::GLYCIN)
|
target_link_libraries(libdn PRIVATE libdnrs)
|
||||||
endif()
|
if (WIN32)
|
||||||
if (DAWN_WITH_GDKPIXBUF)
|
# Rust's MinGW standard library calls these Native API helpers.
|
||||||
target_sources(libdn PRIVATE load-gdkpixbuf.cpp)
|
target_link_libraries(libdn PRIVATE ntdll)
|
||||||
target_link_libraries(libdn PRIVATE PkgConfig::GDKPIXBUF)
|
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()
|
endif()
|
||||||
|
|
||||||
# Target is libdn; SONAME/file is libdn.so (avoid liblibdn.so).
|
# Target is libdn; SONAME/file is libdn.so (avoid liblibdn.so).
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ LoadFn load_jpeg;
|
|||||||
LoadFn load_webp;
|
LoadFn load_webp;
|
||||||
LoadFn load_tiff_ep;
|
LoadFn load_tiff_ep;
|
||||||
LoadFn load_libraw;
|
LoadFn load_libraw;
|
||||||
|
LoadFn load_libwmf;
|
||||||
LoadFn load_resvg;
|
LoadFn load_resvg;
|
||||||
LoadFn load_librsvg;
|
LoadFn load_librsvg;
|
||||||
LoadFn load_xcursor;
|
LoadFn load_xcursor;
|
||||||
@@ -51,16 +52,12 @@ LoadFn load_jxl;
|
|||||||
LoadFn load_openjpeg;
|
LoadFn load_openjpeg;
|
||||||
LoadFn load_tiff;
|
LoadFn load_tiff;
|
||||||
LoadFn load_jxr;
|
LoadFn load_jxr;
|
||||||
LoadFn load_glycin;
|
LoadFn load_dnrs;
|
||||||
LoadFn load_gdkpixbuf;
|
|
||||||
LoadFn load_imageio;
|
LoadFn load_imageio;
|
||||||
LoadFn load_cgpdf;
|
LoadFn load_cgpdf;
|
||||||
|
|
||||||
/// MIME types provided by the installed gdk-pixbuf loaders (if built).
|
/// MIME types compiled into the in-tree Rust decoder.
|
||||||
std::vector<std::string> gdkpixbuf_media_types();
|
std::vector<std::string> dnrs_media_types();
|
||||||
|
|
||||||
/// MIME types glycin can load, per its installed loader config (if built).
|
|
||||||
std::vector<std::string> glycin_media_types();
|
|
||||||
|
|
||||||
/// MIME types the system ImageIO can load (if its loader is built).
|
/// MIME types the system ImageIO can load (if its loader is built).
|
||||||
std::vector<std::string> imageio_media_types();
|
std::vector<std::string> imageio_media_types();
|
||||||
|
|||||||
+11
-7
@@ -1671,16 +1671,20 @@ constexpr Loader kLoaders[] = {
|
|||||||
#endif
|
#endif
|
||||||
"JPEG XR", {"image/jxr", "image/vnd.ms-photo"}, {}},
|
"JPEG XR", {"image/jxr", "image/vnd.ms-photo"}, {}},
|
||||||
|
|
||||||
{"Glycin",
|
{"libwmf",
|
||||||
#if DAWN_WITH_GLYCIN
|
#if DAWN_WITH_LIBWMF
|
||||||
&detail::load_glycin, {}, {}, &detail::glycin_media_types},
|
&detail::load_libwmf,
|
||||||
#else
|
#else
|
||||||
{}, {}, {}, {}},
|
{},
|
||||||
#endif
|
#endif
|
||||||
|
"WMF", {"image/wmf", "image/x-wmf"}, {}},
|
||||||
|
|
||||||
{"GdkPixbuf",
|
{"Rust",
|
||||||
#if DAWN_WITH_GDKPIXBUF
|
#if DAWN_WITH_DNRS
|
||||||
&detail::load_gdkpixbuf, {}, {}, &detail::gdkpixbuf_media_types},
|
&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
|
#else
|
||||||
{}, {}, {}, {}},
|
{}, {}, {}, {}},
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+2
-3
@@ -285,7 +285,7 @@ public:
|
|||||||
double gamma, double whitepoint[2], double primaries[6]);
|
double gamma, double whitepoint[2], double primaries[6]);
|
||||||
|
|
||||||
/// Synthesizes a profile from ITU-T H.273 coded values (as carried by
|
/// 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
|
/// including PQ (16) and HLG (18): both are HDR curves with no ICC v2
|
||||||
/// parametric equivalent, and approximating them would shift tone badly.
|
/// parametric equivalent, and approximating them would shift tone badly.
|
||||||
/// `matrix_coefficients` and range are deliberately not taken -- they
|
/// `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.
|
/// The loader table, in the default order it is tried in.
|
||||||
std::span<const Loader> loaders();
|
std::span<const Loader> loaders();
|
||||||
|
|
||||||
/// MIME types this build can load: base codecs, optional libraries, and
|
/// shared-mime-info types this build can load. The order is stable.
|
||||||
/// whatever gdk-pixbuf modules are installed. Order is stable.
|
|
||||||
std::vector<std::string> supported_media_types();
|
std::vector<std::string> supported_media_types();
|
||||||
|
|
||||||
void orientation_dimensions(
|
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
|
dbsync
|
||||||
fetch $pkg-qt6-base $pkg-vulkan-loader $pkg-vulkan-headers $pkg-libwebp \
|
fetch $pkg-qt6-base $pkg-vulkan-loader $pkg-vulkan-headers $pkg-libwebp \
|
||||||
$pkg-libjpeg-turbo $pkg-libheif $pkg-libjxl $pkg-openjpeg2 $pkg-libraw \
|
$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
|
#$pkg-perl $pkg-perl-win32-api
|
||||||
verify
|
verify
|
||||||
extract
|
extract
|
||||||
@@ -181,8 +181,6 @@ configure "$@"
|
|||||||
|
|
||||||
cd -
|
cd -
|
||||||
toolchain=submodules/liberty/cmake/toolchains/MinGW-w64-x64.cmake
|
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 \
|
cmake -B "$builddir" -DCMAKE_TOOLCHAIN_FILE=$toolchain \
|
||||||
-DDAWN_WITH_GDKPIXBUF=OFF \
|
|
||||||
-DCMAKE_AUTOMOC_EXECUTABLE=$builddir/ucrt64/automoc \
|
-DCMAKE_AUTOMOC_EXECUTABLE=$builddir/ucrt64/automoc \
|
||||||
-DCMAKE_AUTORCC_EXECUTABLE=$builddir/ucrt64/autorcc "$@"
|
-DCMAKE_AUTORCC_EXECUTABLE=$builddir/ucrt64/autorcc "$@"
|
||||||
|
|||||||
@@ -118,6 +118,15 @@ modules:
|
|||||||
url: https://www.libraw.org/data/LibRaw-0.22.2.tar.gz
|
url: https://www.libraw.org/data/LibRaw-0.22.2.tar.gz
|
||||||
sha256: de86b035655accff8d4010f1a221fdf50d353cb7b1422ba26f14a0db92612cfa
|
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.
|
# Dawn looks for Debian's flat include/jxrlib, upstream nests by component.
|
||||||
- name: jxrlib
|
- name: jxrlib
|
||||||
buildsystem: simple
|
buildsystem: simple
|
||||||
@@ -129,27 +138,3 @@ modules:
|
|||||||
url: https://github.com/mircomir/jxrlib.git
|
url: https://github.com/mircomir/jxrlib.git
|
||||||
tag: '1.4.2'
|
tag: '1.4.2'
|
||||||
commit: 568cc2c4e09cf04412bc53a3e03e678ecd1d4693
|
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: org.freedesktop.Platform
|
||||||
runtime-version: '26.08'
|
runtime-version: '26.08'
|
||||||
sdk: org.freedesktop.Sdk
|
sdk: org.freedesktop.Sdk
|
||||||
|
sdk-extensions:
|
||||||
|
- org.freedesktop.Sdk.Extension.rust-stable
|
||||||
base: name.janouch.Dawn.BaseApp
|
base: name.janouch.Dawn.BaseApp
|
||||||
base-version: stable
|
base-version: stable
|
||||||
command: dn
|
command: dn
|
||||||
@@ -32,8 +34,7 @@ finish-args:
|
|||||||
cleanup-commands:
|
cleanup-commands:
|
||||||
- find /app/bin -mindepth 1 ! -name 'dn' ! -name 'dn-*'
|
- find /app/bin -mindepth 1 ! -name 'dn' ! -name 'dn-*'
|
||||||
-exec rm -rf {} +
|
-exec rm -rf {} +
|
||||||
- find /app/libexec -mindepth 1 -maxdepth 1 ! -name glycin-loaders
|
- rm -rf /app/libexec
|
||||||
-exec rm -rf {} +
|
|
||||||
|
|
||||||
- rm -f /app/lib/libcolorhug.so* /app/lib/libgusb.so* /app/lib/libraw_r.so*
|
- rm -f /app/lib/libcolorhug.so* /app/lib/libgusb.so* /app/lib/libraw_r.so*
|
||||||
- rm -rf /app/plugins/generic /app/plugins/imageformats
|
- rm -rf /app/plugins/generic /app/plugins/imageformats
|
||||||
@@ -55,12 +56,15 @@ modules:
|
|||||||
buildsystem: cmake-ninja
|
buildsystem: cmake-ninja
|
||||||
builddir: true
|
builddir: true
|
||||||
build-options:
|
build-options:
|
||||||
|
append-path: /usr/lib/sdk/rust-stable/bin
|
||||||
|
build-args:
|
||||||
|
- --share=network
|
||||||
env:
|
env:
|
||||||
|
CARGO_HOME: /run/build/dawn/cargo
|
||||||
QT_QPA_PLATFORM: offscreen
|
QT_QPA_PLATFORM: offscreen
|
||||||
config-opts:
|
config-opts:
|
||||||
- -DCMAKE_BUILD_TYPE=Release
|
- -DCMAKE_BUILD_TYPE=Release
|
||||||
- -DBUILD_TESTING=OFF
|
- -DBUILD_TESTING=OFF
|
||||||
- -DDAWN_WITH_GDKPIXBUF=OFF
|
|
||||||
- -DFETCHCONTENT_SOURCE_DIR_LCMS2=/run/build/dawn/lcms2
|
- -DFETCHCONTENT_SOURCE_DIR_LCMS2=/run/build/dawn/lcms2
|
||||||
- -DFETCHCONTENT_SOURCE_DIR_JPEGQS=/run/build/dawn/jpegqs
|
- -DFETCHCONTENT_SOURCE_DIR_JPEGQS=/run/build/dawn/jpegqs
|
||||||
sources:
|
sources:
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ set_tests_properties(channels PROPERTIES
|
|||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
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)
|
if (APPLE AND DAWN_WITH_IMAGEIO)
|
||||||
add_executable(test-imageio test-imageio.cpp)
|
add_executable(test-imageio test-imageio.cpp)
|
||||||
target_link_libraries(test-imageio PRIVATE dawn::libdn)
|
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)
|
target_link_libraries(test-shm PRIVATE dawn::libdn)
|
||||||
add_test(NAME shm COMMAND test-shm)
|
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)
|
find_program(DAWN_DESKTOP_FILE_VALIDATE desktop-file-validate)
|
||||||
if(DAWN_DESKTOP_FILE_VALIDATE)
|
if(DAWN_DESKTOP_FILE_VALIDATE)
|
||||||
foreach(desktop
|
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