├── .clang-format ├── .github └── workflows │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .gitmodules ├── .readthedocs.yaml ├── CMakeLists.txt ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs ├── conf.py ├── index.rst └── requirements.txt ├── example ├── example.py └── lena.png ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src ├── wuffs-aux-image-wrapper.h ├── wuffs-aux-json-wrapper.h ├── wuffs-aux-utils.h └── wuffs-bindings.cpp └── test ├── images ├── 1QcSHQRnh493V4dIh4eXh1h4kJUI.th ├── bricks-color.etc2.pkm ├── hippopotamus.nie ├── lena.bmp ├── lena.gif ├── lena.jpeg ├── lena.nie ├── lena.png ├── lena.qoi ├── lena.tga ├── lena.wbmp ├── lena.webp └── lena_exif.png ├── json ├── invalid1.json ├── invalid2.json ├── invalid3.json ├── non-string-map-key.json ├── simple.json └── valid1.json ├── requirements.txt ├── test_aux_image_decoder.py └── test_aux_json_decoder.py /.clang-format: -------------------------------------------------------------------------------- 1 | # Generated from CLion C/C++ Code Style settings 2 | BasedOnStyle: Google 3 | SortIncludes: true -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | 21 | - name: Build wheel 22 | run: python3 -m pip wheel . 23 | 24 | - name: Install wheel 25 | run: python3 -m pip install *.whl 26 | 27 | - name: Install test requirements 28 | run: python3 -m pip install -r test/requirements.txt 29 | 30 | - name: Run tests 31 | run: python3 -m pytest test/ 32 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: macOS CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: macos-13 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | 21 | - name: Build wheel 22 | run: python3 -m pip wheel . 23 | 24 | - name: Install wheel 25 | run: python3 -m pip install *.whl 26 | 27 | - name: Install test requirements 28 | run: python3 -m pip install -r test/requirements.txt 29 | 30 | - name: Run tests 31 | run: python3 -m pytest test/ 32 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: windows-2019 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | with: 19 | submodules: true 20 | 21 | - name: Build wheel 22 | run: python3 -m pip wheel . 23 | 24 | - name: Install wheel 25 | run: python3 -m pip install (get-item .\*.whl) 26 | 27 | - name: Install test requirements 28 | run: python3 -m pip install -r test/requirements.txt 29 | 30 | - name: Run tests 31 | run: python3 -m pytest test/ 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | .cache/ 4 | cmake-build-debug/ 5 | cmake-build-release/ 6 | dist/ 7 | build/ 8 | _build/ 9 | pywuffs.egg-info/ 10 | __pycache__/ 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libs/wuffs"] 2 | path = libs/wuffs 3 | url = https://github.com/google/wuffs.git 4 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | submodules: 4 | include: all 5 | 6 | build: 7 | os: ubuntu-22.04 8 | tools: 9 | python: "3.11" 10 | 11 | sphinx: 12 | configuration: docs/conf.py 13 | 14 | python: 15 | install: 16 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project(pywuffs) 4 | 5 | set(CMAKE_CXX_STANDARD 14) 6 | 7 | if(NOT CMAKE_BUILD_TYPE) 8 | set(CMAKE_BUILD_TYPE Release) 9 | endif() 10 | 11 | if(UNIX) 12 | set(CMAKE_CXX_FLAGS 13 | ${CMAKE_CXX_FLAGS} 14 | "-std=c++14 -Wall -Wextra -s -fvisibility=hidden -Wl,--strip-all") 15 | set(CMAKE_CXX_FLAGS_DEBUG "-g") 16 | set(CMAKE_CXX_FLAGS_RELEASE "-O3") 17 | elseif(MSVC) 18 | set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} "/arch:AVX") 19 | endif() 20 | 21 | find_package(pybind11 REQUIRED) 22 | 23 | pybind11_add_module(pywuffs src/wuffs-bindings.cpp) 24 | target_include_directories(pywuffs PRIVATE libs/wuffs/release/c/) 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Georgiy Manuilov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include src/wuffs-aux-image-wrapper.h src/wuffs-aux-json-wrapper.h src/wuffs-aux-utils.h libs/wuffs/release/c/wuffs-unsupported-snapshot.c 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pywuffs: Python bindings for Wuffs the Library 2 | 3 | [![Linux CI](https://github.com/dev0x13/pywuffs/actions/workflows/linux.yml/badge.svg)](https://github.com/dev0x13/pywuffs/actions/workflows/linux.yml) 4 | [![Windows CI](https://github.com/dev0x13/pywuffs/actions/workflows/windows.yml/badge.svg)](https://github.com/dev0x13/pywuffs/actions/workflows/windows.yml) 5 | [![macOS CI](https://github.com/dev0x13/pywuffs/actions/workflows/macos.yml/badge.svg)](https://github.com/dev0x13/pywuffs/actions/workflows/macos.yml) 6 | [![Downloads](https://static.pepy.tech/badge/pywuffs)](https://pepy.tech/project/pywuffs) 7 | 8 | This project is intended to enable using [Wuffs the Library](https://github.com/google/wuffs) from Python code. For now, 9 | it only provides bindings for image and JSON decoding parts of 10 | the [Auxiliary C++ API](https://github.com/google/wuffs/blob/main/doc/note/auxiliary-code.md) as being of the most 11 | interest since it provides for "ridiculously fast" decoding of images of some types. 12 | 13 | Current version of Wuffs library used in this project is **unsupported snapshot** taken from 14 | [this](https://github.com/google/wuffs/releases/tag/v0.4.0-alpha.9) tag. The primary 15 | rationale for using the snapshot version instead of a stable release is that it provides JPEG decoder. 16 | 17 | ## Installation 18 | 19 | ### Using pip 20 | 21 | ```bash 22 | python3 -m pip install pywuffs 23 | ``` 24 | 25 | ### Using CMake 26 | 27 | CMake build support is mostly intended for development purposes, so the process might be 28 | not so smooth. 29 | 30 | Building the Python module using CMake requires `pybind11` library to be installed in the 31 | system, for example in Ubuntu it can be installed like this: 32 | 33 | ```bash 34 | sudo apt install pybind11-dev 35 | ``` 36 | 37 | #### Linux 38 | 39 | ```bash 40 | mkdir _build && cd _build 41 | cmake .. 42 | make 43 | ``` 44 | 45 | #### Windows 46 | 47 | ```shell 48 | mkdir _build && cd _build 49 | cmake -A x64 .. 50 | cmake --build . 51 | ``` 52 | 53 | ## Usage example 54 | 55 | The example below demonstrates how to decode a PNG image and its EXIF metadata: 56 | 57 | ```python 58 | from pywuffs import ImageDecoderType, PixelFormat 59 | from pywuffs.aux import ( 60 | ImageDecoder, 61 | ImageDecoderConfig, 62 | ImageDecoderFlags 63 | ) 64 | 65 | config = ImageDecoderConfig() 66 | 67 | # All decoders are enabled by default 68 | config.enabled_decoders = [ImageDecoderType.PNG] 69 | 70 | # No metadata is reported by default 71 | config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF] 72 | 73 | # Pixel format is PixelFormat.BGRA_PREMUL by default 74 | config.pixel_format = PixelFormat.BGR 75 | 76 | decoder = ImageDecoder(config) 77 | 78 | decoding_result = decoder.decode("lena.png") 79 | 80 | # Decoded image data in BGR format 81 | image_data = decoding_result.pixbuf 82 | 83 | # Shape of the decoded image 84 | image_shape = decoding_result.pixbuf.shape 85 | 86 | # Parsed EXIF metadata 87 | meta_minfo = decoding_result.reported_metadata[0].minfo 88 | meta_bytes = decoding_result.reported_metadata[0].data.tobytes() 89 | ``` 90 | 91 | ## API reference 92 | 93 | API documentation is available at https://pywuffs.readthedocs.io. 94 | 95 | ## Implementation goals 96 | 97 | 1. Bindings are supposed to be as close as possible to the original C and C++ Wuffs API. The differences are only 98 | justified when it's hardly possible to transfer the API entries to Python as is. 99 | 2. Bindings are not supposed to add much overhead. Because of that some parts of the API are not as convenient as they 100 | expected to be. 101 | 102 | ## Roadmap 103 | 104 | 1. Bindings for other parts of `wuffs_aux` API (CBOR decoding). 105 | 2. Bindings for the C API of Wuffs the Library. 106 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | project = 'pywuffs' 2 | copyright = '2023, pywuffs authors' 3 | 4 | master_doc = 'index' 5 | 6 | extensions = ['sphinx.ext.autodoc', 'sphinx_epytext', 'sphinx.ext.coverage', 'sphinx.ext.napoleon', 7 | 'sphinx.ext.autosummary'] 8 | 9 | exclude_patterns = [] 10 | 11 | html_theme = 'sphinx_rtd_theme' 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | pywuffs: Python bindings for Wuffs the Library 2 | ============================================== 3 | 4 | .. automodule:: pywuffs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | .. automodule:: pywuffs.aux 10 | :members: 11 | :undoc-members: 12 | :show-inheritance: 13 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx_epytext 2 | sphinx_rtd_theme 3 | pywuffs -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | from pywuffs import ImageDecoderType, PixelFormat 2 | from pywuffs.aux import ( 3 | ImageDecoder, 4 | ImageDecoderConfig, 5 | ImageDecoderFlags 6 | ) 7 | 8 | config = ImageDecoderConfig() 9 | config.enabled_decoders = [ImageDecoderType.PNG] 10 | config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF] 11 | config.pixel_format = PixelFormat.BGR 12 | 13 | decoder = ImageDecoder(config) 14 | 15 | decoding_result = decoder.decode("lena.png") 16 | 17 | print(decoding_result.pixbuf.shape) 18 | print(decoding_result.pixbuf) 19 | -------------------------------------------------------------------------------- /example/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/example/lena.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "pybind11"] 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools 2 | pybind11 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from setuptools import setup 3 | from pybind11.setup_helpers import Pybind11Extension 4 | from setuptools.command.build_ext import build_ext 5 | from pathlib import Path 6 | 7 | long_description = (Path(__file__).parent / "README.md").read_text() 8 | 9 | UNIX_BUILD_ARGS = ["-O3", "-g0", "-s", "--std=c++14", 10 | "-fvisibility=hidden", "-flto", "-fno-fat-lto-objects"] 11 | BUILD_ARGS = defaultdict(lambda: UNIX_BUILD_ARGS) 12 | BUILD_ARGS["msvc"] = ["/O3", "/DNDEBUG", "/arch:AVX"] 13 | BUILD_ARGS["unix"] = UNIX_BUILD_ARGS 14 | 15 | UNIX_LINK_ARGS = ["-flto", "-fno-fat-lto-objects"] 16 | GCC_STRIP_FLAG = "-Wl,--strip-all" 17 | LINK_ARGS = defaultdict(lambda: UNIX_LINK_ARGS) 18 | LINK_ARGS["msvc"] = [] 19 | LINK_ARGS["unix"] = UNIX_LINK_ARGS 20 | 21 | 22 | class CustomBuildExt(build_ext): 23 | def build_extensions(self): 24 | compiler = self.compiler.compiler_type 25 | build_args = BUILD_ARGS[compiler] 26 | link_args = LINK_ARGS[compiler] 27 | for ext in self.extensions: 28 | ext.extra_link_args = link_args 29 | ext.extra_compile_args = build_args 30 | if hasattr(self.compiler, "compiler") and self.compiler.compiler[0].endswith("gcc"): 31 | ext.extra_link_args.append(GCC_STRIP_FLAG) 32 | build_ext.build_extensions(self) 33 | 34 | 35 | ext_modules = [ 36 | Pybind11Extension( 37 | "pywuffs", 38 | ["src/wuffs-bindings.cpp"], 39 | include_dirs=["libs/wuffs/release/c"] 40 | ), 41 | ] 42 | 43 | setup(name="pywuffs", 44 | version="2.0.1", 45 | description="Python bindings for Wuffs the Library", 46 | author="Georgiy Manuilov", 47 | url="https://github.com/dev0x13/pywuffs", 48 | cmdclass={"build_ext": CustomBuildExt}, 49 | long_description=long_description, 50 | long_description_content_type="text/markdown", 51 | ext_modules=ext_modules) 52 | -------------------------------------------------------------------------------- /src/wuffs-aux-image-wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "wuffs-aux-utils.h" 11 | 12 | // This API wraps the wuffs_aux API for image decoding. The wrapper is needed 13 | // since the wuffs_aux API uses the callback-based approach which doesn't play 14 | // well with Python-C++ interop. 15 | // Although the API is different from wuffs_aux, it's supposed to be as thin as 16 | // possible including minor aspect like the default non-configured behavior. 17 | 18 | namespace wuffs_aux_wrap { 19 | 20 | enum class ImageDecoderFlags : uint64_t { 21 | #define IDFE(flag) \ 22 | REPORT_METADATA_##flag = \ 23 | wuffs_aux::DecodeImageArgFlags::REPORT_METADATA_##flag 24 | IDFE(BGCL), 25 | IDFE(CHRM), 26 | IDFE(EXIF), 27 | IDFE(GAMA), 28 | IDFE(ICCP), 29 | IDFE(KVP), 30 | IDFE(MTIM), 31 | IDFE(OFS2), 32 | IDFE(PHYD), 33 | IDFE(SRGB), 34 | IDFE(XMP) 35 | #undef IDF_ENTRY 36 | }; 37 | 38 | enum class ImageDecoderQuirks : uint32_t { 39 | IGNORE_CHECKSUM = WUFFS_BASE__QUIRK_IGNORE_CHECKSUM, 40 | GIF_DELAY_NUM_DECODED_FRAMES = WUFFS_GIF__QUIRK_DELAY_NUM_DECODED_FRAMES, 41 | GIF_FIRST_FRAME_LOCAL_PALETTE_MEANS_BLACK_BACKGROUND = 42 | WUFFS_GIF__QUIRK_FIRST_FRAME_LOCAL_PALETTE_MEANS_BLACK_BACKGROUND, 43 | GIF_QUIRK_HONOR_BACKGROUND_COLOR = WUFFS_GIF__QUIRK_HONOR_BACKGROUND_COLOR, 44 | GIF_IGNORE_TOO_MUCH_PIXEL_DATA = WUFFS_GIF__QUIRK_IGNORE_TOO_MUCH_PIXEL_DATA, 45 | GIF_IMAGE_BOUNDS_ARE_STRICT = WUFFS_GIF__QUIRK_IMAGE_BOUNDS_ARE_STRICT, 46 | GIF_REJECT_EMPTY_FRAME = WUFFS_GIF__QUIRK_REJECT_EMPTY_FRAME, 47 | GIF_REJECT_EMPTY_PALETTE = WUFFS_GIF__QUIRK_REJECT_EMPTY_PALETTE, 48 | QUALITY = WUFFS_BASE__QUIRK_QUALITY 49 | }; 50 | 51 | const uint64_t kLowerQuality = WUFFS_BASE__QUIRK_QUALITY__VALUE__LOWER_QUALITY; 52 | const uint64_t kHigherQuality = 53 | WUFFS_BASE__QUIRK_QUALITY__VALUE__HIGHER_QUALITY; 54 | 55 | enum class ImageDecoderType : uint32_t { 56 | #define IDTE(dt) dt = WUFFS_BASE__FOURCC__##dt 57 | IDTE(BMP), 58 | IDTE(GIF), 59 | IDTE(NIE), 60 | IDTE(PNG), 61 | IDTE(TGA), 62 | IDTE(WBMP), 63 | IDTE(JPEG), 64 | IDTE(WEBP), 65 | IDTE(QOI), 66 | IDTE(ETC2), 67 | IDTE(TH) 68 | #undef IDTE 69 | }; 70 | 71 | enum class PixelFormat : uint32_t { 72 | #define PFE(pf) pf = WUFFS_BASE__PIXEL_FORMAT__##pf 73 | PFE(A), 74 | PFE(Y), 75 | PFE(Y_16LE), 76 | PFE(Y_16BE), 77 | PFE(YA_NONPREMUL), 78 | PFE(YA_PREMUL), 79 | PFE(YCBCR), 80 | PFE(YCBCRA_NONPREMUL), 81 | PFE(YCBCRK), 82 | PFE(YCOCG), 83 | PFE(YCOCGA_NONPREMUL), 84 | PFE(YCOCGK), 85 | PFE(INDEXED__BGRA_NONPREMUL), 86 | PFE(INDEXED__BGRA_PREMUL), 87 | PFE(INDEXED__BGRA_BINARY), 88 | PFE(BGR_565), 89 | PFE(BGR), 90 | PFE(BGRA_NONPREMUL), 91 | PFE(BGRA_NONPREMUL_4X16LE), 92 | PFE(BGRA_PREMUL), 93 | PFE(BGRA_PREMUL_4X16LE), 94 | PFE(BGRA_BINARY), 95 | PFE(BGRX), 96 | PFE(RGB), 97 | PFE(RGBA_NONPREMUL), 98 | PFE(RGBA_NONPREMUL_4X16LE), 99 | PFE(RGBA_PREMUL), 100 | PFE(RGBA_PREMUL_4X16LE), 101 | PFE(RGBA_BINARY), 102 | PFE(RGBX), 103 | PFE(CMY), 104 | PFE(CMYK) 105 | #undef PFE 106 | }; 107 | 108 | enum class PixelBlend : uint32_t { 109 | SRC = WUFFS_BASE__PIXEL_BLEND__SRC, 110 | SRC_OVER = WUFFS_BASE__PIXEL_BLEND__SRC_OVER 111 | }; 112 | 113 | enum class PixelSubsampling : uint32_t { 114 | NONE = WUFFS_BASE__PIXEL_SUBSAMPLING__NONE, 115 | #define PSE(ps) K##ps = WUFFS_BASE__PIXEL_SUBSAMPLING__##ps 116 | PSE(444), 117 | PSE(440), 118 | PSE(422), 119 | PSE(420), 120 | PSE(411), 121 | PSE(410) 122 | #undef PFE 123 | }; 124 | 125 | // This struct hosts wuffs_aux::DecodeImage arguments in more user- and 126 | // Python- friendly fashion 127 | struct ImageDecoderConfig { 128 | std::vector flags; 129 | PixelBlend pixel_blend = static_cast( 130 | wuffs_aux::DecodeImageArgPixelBlend::DefaultValue().repr); 131 | std::map quirks; 132 | uint32_t background_color = 133 | wuffs_aux::DecodeImageArgBackgroundColor::DefaultValue().repr; 134 | uint32_t max_incl_dimension = 135 | wuffs_aux::DecodeImageArgMaxInclDimension::DefaultValue().repr; 136 | uint64_t max_incl_metadata_length = 137 | wuffs_aux::DecodeImageArgMaxInclMetadataLength::DefaultValue().repr; 138 | std::vector enabled_decoders = { 139 | ImageDecoderType::BMP, ImageDecoderType::GIF, ImageDecoderType::NIE, 140 | ImageDecoderType::PNG, ImageDecoderType::TGA, ImageDecoderType::WBMP, 141 | ImageDecoderType::JPEG, ImageDecoderType::WEBP, ImageDecoderType::QOI, 142 | ImageDecoderType::ETC2, ImageDecoderType::TH}; 143 | uint32_t pixel_format = wuffs_base__make_pixel_format( 144 | static_cast(PixelFormat::BGRA_PREMUL)) 145 | .repr; 146 | }; 147 | 148 | // This struct represents the wuffs_aux::DecodeImageCallbacks::HandleMetadata 149 | // input 150 | struct MetadataEntry { 151 | wuffs_base__more_information minfo{}; 152 | std::vector data; 153 | 154 | MetadataEntry(const wuffs_base__more_information& minfo, 155 | std::vector&& data) 156 | : minfo(minfo), data(std::move(data)) {} 157 | 158 | MetadataEntry() : minfo(wuffs_base__empty_more_information()) {} 159 | 160 | MetadataEntry(MetadataEntry&& other) noexcept { 161 | minfo = other.minfo; 162 | std::swap(data, other.data); 163 | } 164 | 165 | MetadataEntry& operator=(MetadataEntry&& other) noexcept { 166 | if (this != &other) { 167 | minfo = other.minfo; 168 | std::swap(data, other.data); 169 | } 170 | return *this; 171 | } 172 | 173 | MetadataEntry(const wuffs_aux_wrap::MetadataEntry& other) = delete; 174 | MetadataEntry& operator=(const wuffs_aux_wrap::MetadataEntry& other) = delete; 175 | }; 176 | 177 | struct ImageDecodingResult { 178 | wuffs_base__pixel_config pixcfg = wuffs_base__null_pixel_config(); 179 | std::vector pixbuf; 180 | std::vector reported_metadata; 181 | std::string error_message; 182 | 183 | ImageDecodingResult() = default; 184 | 185 | ImageDecodingResult(ImageDecodingResult&& other) noexcept { 186 | std::swap(pixcfg, other.pixcfg); 187 | std::swap(pixbuf, other.pixbuf); 188 | std::swap(reported_metadata, other.reported_metadata); 189 | std::swap(error_message, other.error_message); 190 | } 191 | 192 | ImageDecodingResult& operator=(ImageDecodingResult&& other) noexcept { 193 | if (this != &other) { 194 | std::swap(pixcfg, other.pixcfg); 195 | std::swap(pixbuf, other.pixbuf); 196 | std::swap(reported_metadata, other.reported_metadata); 197 | std::swap(error_message, other.error_message); 198 | } 199 | return *this; 200 | } 201 | 202 | ImageDecodingResult(ImageDecodingResult& other) = delete; 203 | ImageDecodingResult& operator=(ImageDecodingResult& other) = delete; 204 | }; 205 | 206 | struct ImageDecoderError { 207 | static const std::string MaxInclDimensionExceeded; 208 | static const std::string MaxInclMetadataLengthExceeded; 209 | static const std::string OutOfMemory; 210 | static const std::string UnexpectedEndOfFile; 211 | static const std::string UnsupportedImageFormat; 212 | static const std::string UnsupportedMetadata; 213 | static const std::string UnsupportedPixelBlend; 214 | static const std::string UnsupportedPixelConfiguration; 215 | static const std::string UnsupportedPixelFormat; 216 | static const std::string FailedToOpenFile; 217 | }; 218 | 219 | const std::string ImageDecoderError::MaxInclDimensionExceeded = 220 | wuffs_aux::DecodeImage_MaxInclDimensionExceeded; 221 | const std::string ImageDecoderError::MaxInclMetadataLengthExceeded = 222 | wuffs_aux::DecodeImage_MaxInclMetadataLengthExceeded; 223 | const std::string ImageDecoderError::OutOfMemory = 224 | wuffs_aux::DecodeImage_OutOfMemory; 225 | const std::string ImageDecoderError::UnexpectedEndOfFile = 226 | wuffs_aux::DecodeImage_UnexpectedEndOfFile; 227 | const std::string ImageDecoderError::UnsupportedImageFormat = 228 | wuffs_aux::DecodeImage_UnsupportedImageFormat; 229 | const std::string ImageDecoderError::UnsupportedMetadata = 230 | wuffs_aux::DecodeImage_UnsupportedMetadata; 231 | const std::string ImageDecoderError::UnsupportedPixelBlend = 232 | wuffs_aux::DecodeImage_UnsupportedPixelBlend; 233 | const std::string ImageDecoderError::UnsupportedPixelConfiguration = 234 | wuffs_aux::DecodeImage_UnsupportedPixelConfiguration; 235 | const std::string ImageDecoderError::UnsupportedPixelFormat = 236 | wuffs_aux::DecodeImage_UnsupportedPixelFormat; 237 | const std::string ImageDecoderError::FailedToOpenFile = 238 | "wuffs_aux_wrap::ImageDecoder::Decode: failed to open file"; 239 | 240 | class ImageDecoder : public wuffs_aux::DecodeImageCallbacks { 241 | public: 242 | explicit ImageDecoder(const ImageDecoderConfig& config) 243 | : quirks_vector_(utils::ConvertQuirks(config.quirks)), 244 | enabled_decoders_( 245 | {config.enabled_decoders.begin(), config.enabled_decoders.end()}), 246 | pixel_format_(wuffs_base__make_pixel_format(config.pixel_format)), 247 | quirks_(wuffs_aux::DecodeImageArgQuirks(quirks_vector_.data(), 248 | quirks_vector_.size())), 249 | flags_(GetFlagsBitmask(config.flags)), 250 | pixel_blend_(wuffs_aux::DecodeImageArgPixelBlend( 251 | static_cast(config.pixel_blend))), 252 | background_color_( 253 | wuffs_aux::DecodeImageArgBackgroundColor(config.background_color)), 254 | max_incl_dimension_(wuffs_aux::DecodeImageArgMaxInclDimension( 255 | config.max_incl_dimension)), 256 | max_incl_metadata_length_( 257 | wuffs_aux::DecodeImageArgMaxInclMetadataLength( 258 | config.max_incl_metadata_length)) {} 259 | 260 | /* DecodeImageCallbacks methods implementation */ 261 | 262 | wuffs_base__image_decoder::unique_ptr SelectDecoder( 263 | uint32_t fourcc, wuffs_base__slice_u8 prefix_data, 264 | bool prefix_closed) override { 265 | if (enabled_decoders_.count(static_cast(fourcc)) == 0) { 266 | return {nullptr}; 267 | } 268 | return wuffs_aux::DecodeImageCallbacks::SelectDecoder(fourcc, prefix_data, 269 | prefix_closed); 270 | } 271 | 272 | std::string HandleMetadata(const wuffs_base__more_information& minfo, 273 | wuffs_base__slice_u8 raw) override { 274 | decoding_result_.reported_metadata.emplace_back( 275 | minfo, std::vector{raw.ptr, raw.ptr + raw.len}); 276 | return ""; 277 | } 278 | 279 | wuffs_base__pixel_format SelectPixfmt( 280 | const wuffs_base__image_config&) override { 281 | return pixel_format_; 282 | } 283 | 284 | // This implementation is essentially the same as the default one except that 285 | // it uses the "decoding_result_" field for allocating output buffer 286 | AllocPixbufResult AllocPixbuf(const wuffs_base__image_config& image_config, 287 | bool allow_uninitialized_memory) override { 288 | uint32_t w = image_config.pixcfg.width(); 289 | uint32_t h = image_config.pixcfg.height(); 290 | if ((w == 0) || (h == 0)) { 291 | return {""}; 292 | } 293 | uint64_t len = image_config.pixcfg.pixbuf_len(); 294 | if (len == 0 || SIZE_MAX < len) { 295 | return {wuffs_aux::DecodeImage_UnsupportedPixelConfiguration}; 296 | } 297 | decoding_result_.pixbuf.resize(len); 298 | if (!allow_uninitialized_memory) { 299 | std::memset(decoding_result_.pixbuf.data(), 0, 300 | decoding_result_.pixbuf.size()); 301 | } 302 | wuffs_base__pixel_buffer pixbuf; 303 | wuffs_base__status status = pixbuf.set_from_slice( 304 | &image_config.pixcfg, 305 | wuffs_base__make_slice_u8(decoding_result_.pixbuf.data(), 306 | decoding_result_.pixbuf.size())); 307 | if (!status.is_ok()) { 308 | decoding_result_.pixbuf = {}; 309 | return {status.message()}; 310 | } 311 | return {wuffs_aux::MemOwner(nullptr, &free), pixbuf}; 312 | } 313 | 314 | /* End of DecodeImageCallbacks methods implementation */ 315 | 316 | ImageDecodingResult Decode(const uint8_t* data, size_t size) { 317 | wuffs_aux::sync_io::MemoryInput input(data, size); 318 | return DecodeInternal(input); 319 | } 320 | 321 | ImageDecodingResult Decode(const std::string& path_to_file) { 322 | FILE* f = fopen(path_to_file.c_str(), "rb"); 323 | if (!f) { 324 | ImageDecodingResult result; 325 | result.error_message = ImageDecoderError::FailedToOpenFile; 326 | return result; 327 | } 328 | wuffs_aux::sync_io::FileInput input(f); 329 | ImageDecodingResult result = DecodeInternal(input); 330 | fclose(f); 331 | return result; 332 | } 333 | 334 | private: 335 | static uint64_t GetFlagsBitmask(const std::vector& flags) { 336 | uint64_t bitmask = 0; 337 | for (const auto f : flags) { 338 | bitmask |= static_cast(f); 339 | } 340 | return bitmask; 341 | } 342 | 343 | ImageDecodingResult DecodeInternal(wuffs_aux::sync_io::Input& input) { 344 | wuffs_aux::DecodeImageResult decode_image_result = wuffs_aux::DecodeImage( 345 | *this, input, quirks_, flags_, pixel_blend_, background_color_, 346 | max_incl_dimension_, max_incl_metadata_length_); 347 | decoding_result_.error_message = 348 | std::move(decode_image_result.error_message); 349 | if (!decode_image_result.pixbuf.pixcfg.is_valid()) { 350 | decoding_result_.pixbuf = {}; 351 | decoding_result_.pixcfg = wuffs_base__null_pixel_config(); 352 | } else { 353 | decoding_result_.pixcfg = decode_image_result.pixbuf.pixcfg; 354 | } 355 | return std::move(decoding_result_); 356 | } 357 | 358 | private: 359 | ImageDecodingResult decoding_result_; 360 | std::vector quirks_vector_; 361 | std::unordered_set enabled_decoders_; 362 | wuffs_base__pixel_format pixel_format_; 363 | wuffs_aux::DecodeImageArgQuirks quirks_; 364 | wuffs_aux::DecodeImageArgFlags flags_; 365 | wuffs_aux::DecodeImageArgPixelBlend pixel_blend_; 366 | wuffs_aux::DecodeImageArgBackgroundColor background_color_; 367 | wuffs_aux::DecodeImageArgMaxInclDimension max_incl_dimension_; 368 | wuffs_aux::DecodeImageArgMaxInclMetadataLength max_incl_metadata_length_; 369 | }; 370 | 371 | } // namespace wuffs_aux_wrap 372 | -------------------------------------------------------------------------------- /src/wuffs-aux-json-wrapper.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "wuffs-aux-utils.h" 13 | 14 | // This API wraps the wuffs_aux API for JSON decoding. The wrapper is needed 15 | // since the wuffs_aux API uses the callback-based approach which doesn't play 16 | // well with Python-C++ interop. 17 | // Although the API is different from wuffs_aux, it's supposed to be as thin as 18 | // possible including minor aspect like the default non-configured behavior. 19 | 20 | namespace wuffs_aux_wrap { 21 | 22 | enum class JsonDecoderQuirks : uint32_t { 23 | #define JDQE(quirk) quirk = WUFFS_JSON__QUIRK_##quirk 24 | JDQE(ALLOW_ASCII_CONTROL_CODES), 25 | JDQE(ALLOW_BACKSLASH_A), 26 | JDQE(ALLOW_BACKSLASH_CAPITAL_U), 27 | JDQE(ALLOW_BACKSLASH_E), 28 | JDQE(ALLOW_BACKSLASH_NEW_LINE), 29 | JDQE(ALLOW_BACKSLASH_QUESTION_MARK), 30 | JDQE(ALLOW_BACKSLASH_SINGLE_QUOTE), 31 | JDQE(ALLOW_BACKSLASH_V), 32 | JDQE(ALLOW_BACKSLASH_X_AS_CODE_POINTS), 33 | JDQE(ALLOW_BACKSLASH_ZERO), 34 | JDQE(ALLOW_COMMENT_BLOCK), 35 | JDQE(ALLOW_COMMENT_LINE), 36 | JDQE(ALLOW_EXTRA_COMMA), 37 | JDQE(ALLOW_INF_NAN_NUMBERS), 38 | JDQE(ALLOW_LEADING_ASCII_RECORD_SEPARATOR), 39 | JDQE(ALLOW_LEADING_UNICODE_BYTE_ORDER_MARK), 40 | JDQE(ALLOW_TRAILING_FILLER), 41 | JDQE(EXPECT_TRAILING_NEW_LINE_OR_EOF), 42 | JDQE(JSON_POINTER_ALLOW_TILDE_N_TILDE_R_TILDE_T), 43 | JDQE(REPLACE_INVALID_UNICODE) 44 | #undef JDQE 45 | }; 46 | 47 | // This struct hosts wuffs_aux::DecodeJson arguments in more user- and 48 | // Python- friendly fashion 49 | struct JsonDecoderConfig { 50 | std::map quirks; 51 | std::string json_pointer; 52 | }; 53 | 54 | struct JsonDecodingResult { 55 | pybind11::object parsed; 56 | std::string error_message; 57 | uint64_t cursor_position = 0; 58 | 59 | JsonDecodingResult() = default; 60 | 61 | JsonDecodingResult(JsonDecodingResult&& other) noexcept { 62 | std::swap(parsed, other.parsed); 63 | std::swap(error_message, other.error_message); 64 | std::swap(cursor_position, other.cursor_position); 65 | } 66 | 67 | JsonDecodingResult& operator=(JsonDecodingResult&& other) noexcept { 68 | if (this != &other) { 69 | std::swap(parsed, other.parsed); 70 | std::swap(error_message, other.error_message); 71 | std::swap(cursor_position, other.cursor_position); 72 | } 73 | return *this; 74 | } 75 | 76 | JsonDecodingResult(JsonDecodingResult& other) = delete; 77 | JsonDecodingResult& operator=(JsonDecodingResult& other) = delete; 78 | }; 79 | 80 | struct JsonDecoderError { 81 | static const std::string BadJsonPointer; 82 | static const std::string NoMatch; 83 | static const std::string DuplicateMapKey; 84 | static const std::string NonStringMapKey; 85 | static const std::string NonContainerStackEntry; 86 | static const std::string BadDepth; 87 | static const std::string FailedToOpenFile; 88 | static const std::string BadC0ControlCode; 89 | static const std::string BadUtf8; 90 | static const std::string BadBackslashEscape; 91 | static const std::string BadInput; 92 | static const std::string BadNewLineInAString; 93 | static const std::string BadQuirkCombination; 94 | static const std::string UnsupportedNumberLength; 95 | static const std::string UnsupportedRecursionDepth; 96 | }; 97 | 98 | const std::string JsonDecoderError::BadJsonPointer = 99 | wuffs_aux::DecodeJson_BadJsonPointer; 100 | const std::string JsonDecoderError::NoMatch = wuffs_aux::DecodeJson_NoMatch; 101 | const std::string JsonDecoderError::DuplicateMapKey = 102 | "wuffs_aux_wrap::JsonDecoder::Decode: duplicate map key: key="; 103 | const std::string JsonDecoderError::NonStringMapKey = 104 | "wuffs_aux_wrap::JsonDecoder::Decode: non-string map key"; 105 | const std::string JsonDecoderError::NonContainerStackEntry = 106 | "wuffs_aux_wrap::JsonDecoder::Decode: non-container stack entry"; 107 | const std::string JsonDecoderError::BadDepth = 108 | "wuffs_aux_wrap::JsonDecoder::Decode: bad depth"; 109 | const std::string JsonDecoderError::FailedToOpenFile = 110 | "wuffs_aux_wrap::JsonDecoder::Decode: failed to open file"; 111 | // + 1 is for stripping leading '#' 112 | const std::string JsonDecoderError::BadC0ControlCode = 113 | wuffs_json__error__bad_c0_control_code + 1; 114 | const std::string JsonDecoderError::BadUtf8 = wuffs_json__error__bad_utf_8 + 1; 115 | const std::string JsonDecoderError::BadBackslashEscape = 116 | wuffs_json__error__bad_backslash_escape + 1; 117 | const std::string JsonDecoderError::BadInput = wuffs_json__error__bad_input + 1; 118 | const std::string JsonDecoderError::BadNewLineInAString = 119 | wuffs_json__error__bad_new_line_in_a_string + 1; 120 | const std::string JsonDecoderError::BadQuirkCombination = 121 | wuffs_json__error__bad_quirk_combination + 1; 122 | const std::string JsonDecoderError::UnsupportedNumberLength = 123 | wuffs_json__error__unsupported_number_length + 1; 124 | const std::string JsonDecoderError::UnsupportedRecursionDepth = 125 | wuffs_json__error__unsupported_recursion_depth + 1; 126 | 127 | class JsonDecoder : public wuffs_aux::DecodeJsonCallbacks { 128 | public: 129 | struct Entry { 130 | Entry(pybind11::object&& jvalue_arg) 131 | : jvalue(std::move(jvalue_arg)), has_map_key(false), map_key() {} 132 | 133 | pybind11::object jvalue; 134 | bool has_map_key; 135 | std::string map_key; 136 | 137 | bool IsList() { return pybind11::isinstance(jvalue); } 138 | 139 | bool IsDict() { return pybind11::isinstance(jvalue); } 140 | }; 141 | 142 | explicit JsonDecoder(const JsonDecoderConfig& config) 143 | : quirks_vector_(utils::ConvertQuirks(config.quirks)), 144 | quirks_(wuffs_aux::DecodeJsonArgQuirks(quirks_vector_.data(), 145 | quirks_vector_.size())), 146 | json_pointer_(config.json_pointer) {} 147 | 148 | /* DecodeJsonCallbacks methods implementation */ 149 | 150 | std::string Append(pybind11::object&& jvalue) { 151 | pybind11::dict a; 152 | if (stack_.empty()) { 153 | stack_.emplace_back(std::move(jvalue)); 154 | return ""; 155 | } 156 | Entry& top = stack_.back(); 157 | if (top.IsList()) { 158 | top.jvalue.cast().append(std::move(jvalue)); 159 | return ""; 160 | } else if (top.IsDict()) { 161 | const pybind11::dict& jmap = top.jvalue.cast(); 162 | if (top.has_map_key) { 163 | top.has_map_key = false; 164 | if (jmap.contains(top.map_key)) { 165 | return JsonDecoderError::DuplicateMapKey + top.map_key; 166 | } 167 | jmap[top.map_key.c_str()] = jvalue; 168 | return ""; 169 | } else if (pybind11::isinstance(jvalue)) { 170 | top.has_map_key = true; 171 | top.map_key = jvalue.cast(); 172 | return ""; 173 | } 174 | return "main: internal error: non-string map key"; 175 | } else { 176 | return "main: internal error: non-container stack entry"; 177 | } 178 | } 179 | 180 | std::string AppendNull() override { return Append(pybind11::none()); } 181 | 182 | std::string AppendBool(bool val) override { 183 | return Append(pybind11::bool_(val)); 184 | } 185 | 186 | std::string AppendI64(int64_t val) override { 187 | return Append(pybind11::int_(val)); 188 | } 189 | 190 | std::string AppendF64(double val) override { 191 | return Append(pybind11::float_(val)); 192 | } 193 | 194 | std::string AppendTextString(std::string&& val) override { 195 | return Append(pybind11::str(val)); 196 | } 197 | 198 | std::string Push(uint32_t flags) override { 199 | if (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_LIST) { 200 | stack_.emplace_back(pybind11::list()); 201 | return ""; 202 | } else if (flags & WUFFS_BASE__TOKEN__VBD__STRUCTURE__TO_DICT) { 203 | stack_.emplace_back(pybind11::dict()); 204 | return ""; 205 | } 206 | return "main: internal error: bad push"; 207 | } 208 | 209 | std::string Pop(uint32_t) override { 210 | if (stack_.empty()) { 211 | return "main: internal error: bad pop"; 212 | } 213 | pybind11::object jvalue = std::move(stack_.back().jvalue); 214 | stack_.pop_back(); 215 | return Append(std::move(jvalue)); 216 | } 217 | 218 | /* End of DecodeJsonCallbacks methods implementation */ 219 | 220 | JsonDecodingResult Decode(const uint8_t* data, size_t size) { 221 | wuffs_aux::sync_io::MemoryInput input(data, size); 222 | return DecodeInternal(input); 223 | } 224 | 225 | JsonDecodingResult Decode(const std::string& path_to_file) { 226 | FILE* f = fopen(path_to_file.c_str(), "rb"); 227 | if (!f) { 228 | JsonDecodingResult result; 229 | result.error_message = JsonDecoderError::FailedToOpenFile; 230 | result.parsed = pybind11::none(); 231 | return result; 232 | } 233 | wuffs_aux::sync_io::FileInput input(f); 234 | JsonDecodingResult result = DecodeInternal(input); 235 | fclose(f); 236 | return result; 237 | } 238 | 239 | private: 240 | JsonDecodingResult DecodeInternal(wuffs_aux::sync_io::Input& input) { 241 | wuffs_aux::DecodeJsonResult decode_json_result = 242 | wuffs_aux::DecodeJson(*this, input, quirks_, json_pointer_); 243 | JsonDecodingResult decoding_result; 244 | decoding_result.error_message = std::move(decode_json_result.error_message); 245 | decoding_result.cursor_position = decode_json_result.cursor_position; 246 | if (stack_.size() != 1) { 247 | decoding_result.error_message = JsonDecoderError::BadDepth; 248 | } 249 | decoding_result.parsed = decoding_result.error_message.empty() 250 | ? std::move(stack_[0].jvalue) 251 | : pybind11::none(); 252 | stack_.clear(); 253 | return decoding_result; 254 | } 255 | 256 | private: 257 | std::vector quirks_vector_; 258 | wuffs_aux::DecodeJsonArgQuirks quirks_; 259 | wuffs_aux::DecodeJsonArgJsonPointer json_pointer_; 260 | std::vector stack_; 261 | }; 262 | 263 | } // namespace wuffs_aux_wrap 264 | -------------------------------------------------------------------------------- /src/wuffs-aux-utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace utils { 8 | 9 | template 10 | std::vector ConvertQuirks( 11 | const std::map& quirks_map) { 12 | std::vector quirks_vector; 13 | quirks_vector.reserve(quirks_map.size()); 14 | for (const auto& quirk : quirks_map) { 15 | quirks_vector.emplace_back(static_cast(quirk.first), 16 | quirk.second); 17 | } 18 | return quirks_vector; 19 | } 20 | 21 | } // namespace utils -------------------------------------------------------------------------------- /src/wuffs-bindings.cpp: -------------------------------------------------------------------------------- 1 | #define WUFFS_IMPLEMENTATION 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | #include "wuffs-aux-image-wrapper.h" 9 | #include "wuffs-aux-json-wrapper.h" 10 | 11 | namespace py = pybind11; 12 | 13 | PYBIND11_MODULE(pywuffs, m) { 14 | m.doc() = "Python bindings for Wuffs the Library."; 15 | 16 | /* 17 | * Base Wuffs API 18 | */ 19 | 20 | // clang-format off 21 | py::class_( 22 | m, "wuffs_base__more_information", 23 | "Holds additional fields. The flavor field follows the base38 namespace " 24 | "convention (https://github.com/google/wuffs/blob/main/doc/note/base38-and-fourcc.md). " 25 | "The other fields' semantics depends on the flavor.") 26 | // clang-format on 27 | .def_readonly("flavor", &wuffs_base__more_information::flavor) 28 | .def_readonly("w", &wuffs_base__more_information::w) 29 | .def_readonly("x", &wuffs_base__more_information::x) 30 | .def_readonly("y", &wuffs_base__more_information::y) 31 | .def_readonly("z", &wuffs_base__more_information::z) 32 | .def("io_redirect__fourcc", 33 | &wuffs_base__more_information::io_redirect__fourcc) 34 | .def("io_seek__position", 35 | &wuffs_base__more_information::io_seek__position) 36 | .def("metadata__fourcc", &wuffs_base__more_information::metadata__fourcc) 37 | .def("metadata_parsed__chrm", 38 | &wuffs_base__more_information::metadata_parsed__chrm) 39 | .def("metadata_parsed__gama", 40 | &wuffs_base__more_information::metadata_parsed__gama) 41 | .def("metadata_parsed__srgb", 42 | &wuffs_base__more_information::metadata_parsed__srgb); 43 | 44 | py::class_( 45 | m, "wuffs_base__pixel_config", 46 | "Holds information about decoded pixel buffer.") 47 | .def("is_valid", &wuffs_base__pixel_config::is_valid) 48 | .def("pixel_format", 49 | [](wuffs_base__pixel_config& self) -> wuffs_aux_wrap::PixelFormat { 50 | return static_cast( 51 | self.pixel_format().repr); 52 | }) 53 | .def("pixel_subsampling", 54 | [](wuffs_base__pixel_config& self) 55 | -> wuffs_aux_wrap::PixelSubsampling { 56 | return static_cast( 57 | self.pixel_subsampling().repr); 58 | }) 59 | .def("width", &wuffs_base__pixel_config::width) 60 | .def("height", &wuffs_base__pixel_config::height) 61 | .def("pixbuf_len", &wuffs_base__pixel_config::pixbuf_len); 62 | 63 | py::enum_( 64 | m, "ImageDecoderQuirks", 65 | "See https://github.com/google/wuffs/blob/main/doc/note/quirks.md.") 66 | .value("IGNORE_CHECKSUM", 67 | wuffs_aux_wrap::ImageDecoderQuirks::IGNORE_CHECKSUM, 68 | "Favor faster decodes over rejecting invalid checksums.") 69 | .value("GIF_DELAY_NUM_DECODED_FRAMES", 70 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_DELAY_NUM_DECODED_FRAMES) 71 | .value("GIF_FIRST_FRAME_LOCAL_PALETTE_MEANS_BLACK_BACKGROUND", 72 | wuffs_aux_wrap::ImageDecoderQuirks:: 73 | GIF_FIRST_FRAME_LOCAL_PALETTE_MEANS_BLACK_BACKGROUND) 74 | .value( 75 | "GIF_QUIRK_HONOR_BACKGROUND_COLOR", 76 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_QUIRK_HONOR_BACKGROUND_COLOR) 77 | .value("GIF_IGNORE_TOO_MUCH_PIXEL_DATA", 78 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_IGNORE_TOO_MUCH_PIXEL_DATA) 79 | .value("GIF_IMAGE_BOUNDS_ARE_STRICT", 80 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_IMAGE_BOUNDS_ARE_STRICT) 81 | .value("GIF_REJECT_EMPTY_FRAME", 82 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_REJECT_EMPTY_FRAME) 83 | .value("GIF_REJECT_EMPTY_PALETTE", 84 | wuffs_aux_wrap::ImageDecoderQuirks::GIF_REJECT_EMPTY_PALETTE) 85 | .value("QUALITY", wuffs_aux_wrap::ImageDecoderQuirks::QUALITY, 86 | "Configures decoders (for a lossy format, where there is some " 87 | "leeway in \"a/the correct decoding\") to use lower than, equal " 88 | "to or higher than the default quality setting."); 89 | 90 | py::enum_(m, "ImageDecoderType") 91 | .value("BMP", wuffs_aux_wrap::ImageDecoderType::BMP) 92 | .value("GIF", wuffs_aux_wrap::ImageDecoderType::GIF) 93 | .value("NIE", wuffs_aux_wrap::ImageDecoderType::NIE) 94 | .value("PNG", wuffs_aux_wrap::ImageDecoderType::PNG) 95 | .value("TGA", wuffs_aux_wrap::ImageDecoderType::TGA) 96 | .value("WBMP", wuffs_aux_wrap::ImageDecoderType::WBMP) 97 | .value("JPEG", wuffs_aux_wrap::ImageDecoderType::JPEG) 98 | .value("WEBP", wuffs_aux_wrap::ImageDecoderType::WEBP) 99 | .value("QOI", wuffs_aux_wrap::ImageDecoderType::QOI) 100 | .value("ETC2", wuffs_aux_wrap::ImageDecoderType::ETC2) 101 | .value("TH", wuffs_aux_wrap::ImageDecoderType::TH); 102 | 103 | m.attr("LowerQuality") = wuffs_aux_wrap::kLowerQuality; 104 | m.attr("HigherQuality") = wuffs_aux_wrap::kHigherQuality; 105 | 106 | // clang-format off 107 | #define PYPF(pf) .value(#pf, wuffs_aux_wrap::PixelFormat::pf) 108 | py::enum_( 109 | m, "PixelFormat", "Common 8-bit-depth pixel formats. This list is not " 110 | "exhaustive; not all valid wuffs_base__pixel_format values are present.") 111 | PYPF(A) 112 | PYPF(Y) 113 | PYPF(Y_16LE) 114 | PYPF(Y_16BE) 115 | PYPF(YA_NONPREMUL) 116 | PYPF(YA_PREMUL) 117 | PYPF(YCBCR) 118 | PYPF(YCBCRA_NONPREMUL) 119 | PYPF(YCBCRK) 120 | PYPF(YCOCG) 121 | PYPF(YCOCGA_NONPREMUL) 122 | PYPF(YCOCGK) 123 | PYPF(INDEXED__BGRA_NONPREMUL) 124 | PYPF(INDEXED__BGRA_PREMUL) 125 | PYPF(INDEXED__BGRA_BINARY) 126 | PYPF(BGR_565) 127 | PYPF(BGR) 128 | PYPF(BGRA_NONPREMUL) 129 | PYPF(BGRA_NONPREMUL_4X16LE) 130 | PYPF(BGRA_PREMUL) 131 | PYPF(BGRA_PREMUL_4X16LE) 132 | PYPF(BGRA_BINARY) 133 | PYPF(BGRX) 134 | PYPF(RGB) 135 | PYPF(RGBA_NONPREMUL) 136 | PYPF(RGBA_NONPREMUL_4X16LE) 137 | PYPF(RGBA_PREMUL) 138 | PYPF(RGBA_PREMUL_4X16LE) 139 | PYPF(RGBA_BINARY) 140 | PYPF(RGBX) 141 | PYPF(CMY) 142 | PYPF(CMYK); 143 | #undef PYPF 144 | // clang-format on 145 | 146 | py::enum_( 147 | m, "PixelBlend", 148 | "Encodes how to blend source and destination pixels, accounting for " 149 | "transparency. It encompasses the Porter-Duff compositing operators as " 150 | "well as the other blending modes defined by PDF.") 151 | .value("SRC", wuffs_aux_wrap::PixelBlend::SRC) 152 | .value("SRC_OVER", wuffs_aux_wrap::PixelBlend::SRC_OVER); 153 | 154 | py::enum_( 155 | m, "PixelSubsampling", 156 | "wuffs_base__pixel_subsampling encodes whether sample values cover one " 157 | "pixel or cover multiple pixels.\n" 158 | "See " 159 | "https://github.com/google/wuffs/blob/main/doc/note/" 160 | "pixel-subsampling.md\n") 161 | .value("NONE", wuffs_aux_wrap::PixelSubsampling::NONE) 162 | .value("K444", wuffs_aux_wrap::PixelSubsampling::K444) 163 | .value("K440", wuffs_aux_wrap::PixelSubsampling::K440) 164 | .value("K422", wuffs_aux_wrap::PixelSubsampling::K422) 165 | .value("K420", wuffs_aux_wrap::PixelSubsampling::K420) 166 | .value("K411", wuffs_aux_wrap::PixelSubsampling::K411) 167 | .value("K410", wuffs_aux_wrap::PixelSubsampling::K410); 168 | 169 | /* 170 | * Aux Wuffs API (DecodeImage) 171 | */ 172 | 173 | py::module aux_m = m.def_submodule("aux", "Simplified \"auxiliary\" API."); 174 | 175 | py::enum_( 176 | aux_m, "ImageDecoderFlags", 177 | "Flags to defining image decoder behavior (e.g. metadata reporting).") 178 | .value("REPORT_METADATA_BGCL", 179 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_BGCL, 180 | "Background Color.") 181 | .value("REPORT_METADATA_CHRM", 182 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_CHRM, 183 | "Primary Chromaticities and White Point.") 184 | .value("REPORT_METADATA_EXIF", 185 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_EXIF, 186 | "Exchangeable Image File Format.") 187 | .value("REPORT_METADATA_GAMA", 188 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_GAMA, 189 | "Gamma Correction.") 190 | .value("REPORT_METADATA_ICCP", 191 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_ICCP, 192 | "International Color Consortium Profile.") 193 | .value("REPORT_METADATA_KVP", 194 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_KVP, 195 | "Key-Value Pair. For PNG files, this includes iTXt, tEXt and zTXt " 196 | "chunks. In the HandleMetadata callback, the raw argument " 197 | "contains UTF-8 strings.") 198 | .value("REPORT_METADATA_MTIM", 199 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_MTIM, 200 | "Modification Time.") 201 | .value("REPORT_METADATA_OFS2", 202 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_OFS2, 203 | "Offset (2-Dimensional).") 204 | .value("REPORT_METADATA_PHYD", 205 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_PHYD, 206 | "Physical Dimensions.") 207 | .value("REPORT_METADATA_SRGB", 208 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_SRGB, 209 | "Standard Red Green Blue (Rendering Intent).") 210 | .value("REPORT_METADATA_XMP", 211 | wuffs_aux_wrap::ImageDecoderFlags::REPORT_METADATA_XMP, 212 | "Extensible Metadata Platform."); 213 | 214 | py::class_(aux_m, "MetadataEntry", 215 | "Holds parsed metadata piece.") 216 | .def_readonly("minfo", &wuffs_aux_wrap::MetadataEntry::minfo, 217 | "wuffs_base__more_information: Info on parsed metadata.") 218 | .def_property_readonly( 219 | "data", 220 | [](wuffs_aux_wrap::MetadataEntry& self) { 221 | return pybind11::array_t(pybind11::buffer_info( 222 | self.data.data(), sizeof(uint8_t), 223 | pybind11::format_descriptor::value, 1, 224 | {self.data.size()}, {1})); 225 | }, 226 | "np.array: Parsed metadata (1D uint8 Numpy array)."); 227 | 228 | py::class_(aux_m, "ImageDecoderConfig", 229 | "Image decoder configuration.") 230 | .def(py::init<>()) 231 | .def_readwrite("flags", &wuffs_aux_wrap::ImageDecoderConfig::flags, 232 | "list: list of ImageDecoderFlags, empty by default.") 233 | .def_readwrite("pixel_blend", 234 | &wuffs_aux_wrap::ImageDecoderConfig::pixel_blend, 235 | "PixelBlend: pixel blend mode, default is PixelBlend.SRC.") 236 | .def_readwrite( 237 | "quirks", &wuffs_aux_wrap::ImageDecoderConfig::quirks, 238 | "dict: dict of ImageDecoderQuirks: pairs, empty by " 239 | "default (PNG decoder will always have " 240 | "ImageDecoderQuirks.IGNORE_CHECKSUM quirk enabled implicitly).") 241 | .def_readwrite( 242 | "background_color", 243 | &wuffs_aux_wrap::ImageDecoderConfig::background_color, 244 | "int: The background_color is used to fill the pixel buffer after " 245 | "callbacks.AllocPixbuf returns, if it is valid in the " 246 | "wuffs_base__color_u32_argb_premul__is_valid sense. The default " 247 | "value, 0x0000_0001, is not valid since its Blue channel value " 248 | "(0x01) is greater than its Alpha channel value (0x00). A valid " 249 | "background_color will typically be overwritten when pixel_blend is " 250 | "PixelBlend.SRC, but might still be visible on partial " 251 | "(not total) success or when pixel_blend is PixelBlend.SRC_OVER and " 252 | "the decoded image is not fully opaque.") 253 | .def_readwrite( 254 | "max_incl_dimension", 255 | &wuffs_aux_wrap::ImageDecoderConfig::max_incl_dimension, 256 | "int: Decoding fails (with " 257 | "ImageDecoderErrors.MaxInclDimensionExceeded) if " 258 | "the image's width or height is greater than max_incl_dimension.") 259 | .def_readwrite( 260 | "max_incl_metadata_length", 261 | &wuffs_aux_wrap::ImageDecoderConfig::max_incl_metadata_length, 262 | "int: Decoding fails (with " 263 | "ImageDecoderErrors.MaxInclDimensionExceeded) if " 264 | "any opted-in (via flags bits) metadata is longer than " 265 | "max_incl_metadata_length.") 266 | .def_readwrite("enabled_decoders", 267 | &wuffs_aux_wrap::ImageDecoderConfig::enabled_decoders, 268 | "list: list of ImageDecoderType.") 269 | .def_readwrite( 270 | "pixel_format", &wuffs_aux_wrap::ImageDecoderConfig::pixel_format, 271 | "PixelFormat: Destination pixel format, default is " 272 | "PixelFormat.BGRA_PREMUL which is 4 bytes per pixel (8 " 273 | "bits per channel × 4 channels). Currently supported formats are:" 274 | "- PixelFormat.BGR_565\n" 275 | "- PixelFormat.BGR\n" 276 | "- PixelFormat.BGRA_NONPREMUL\n" 277 | "- PixelFormat.BGRA_NONPREMUL_4X16LE\n" 278 | "- PixelFormat.BGRA_PREMUL\n" 279 | "- PixelFormat.RGBA_NONPREMUL\n" 280 | "- PixelFormat.RGBA_PREMUL"); 281 | 282 | py::class_(aux_m, "ImageDecoderError") 283 | .def_readonly_static( 284 | "MaxInclDimensionExceeded", 285 | &wuffs_aux_wrap::ImageDecoderError::MaxInclDimensionExceeded) 286 | .def_readonly_static( 287 | "MaxInclMetadataLengthExceeded", 288 | &wuffs_aux_wrap::ImageDecoderError::MaxInclMetadataLengthExceeded) 289 | .def_readonly_static("OutOfMemory", 290 | &wuffs_aux_wrap::ImageDecoderError::OutOfMemory) 291 | .def_readonly_static( 292 | "UnexpectedEndOfFile", 293 | &wuffs_aux_wrap::ImageDecoderError::UnexpectedEndOfFile) 294 | .def_readonly_static( 295 | "UnsupportedImageFormat", 296 | &wuffs_aux_wrap::ImageDecoderError::UnsupportedImageFormat) 297 | .def_readonly_static( 298 | "UnsupportedMetadata", 299 | &wuffs_aux_wrap::ImageDecoderError::UnsupportedMetadata) 300 | .def_readonly_static( 301 | "UnsupportedPixelBlend", 302 | &wuffs_aux_wrap::ImageDecoderError::UnsupportedPixelBlend) 303 | .def_readonly_static( 304 | "MaxInclDimensionExceeded", 305 | &wuffs_aux_wrap::ImageDecoderError::MaxInclDimensionExceeded) 306 | .def_readonly_static( 307 | "UnsupportedPixelConfiguration", 308 | &wuffs_aux_wrap::ImageDecoderError::UnsupportedPixelConfiguration) 309 | .def_readonly_static( 310 | "UnsupportedPixelFormat", 311 | &wuffs_aux_wrap::ImageDecoderError::UnsupportedPixelFormat) 312 | .def_readonly_static( 313 | "FailedToOpenFile", 314 | &wuffs_aux_wrap::ImageDecoderError::FailedToOpenFile); 315 | 316 | py::class_( 317 | aux_m, "ImageDecodingResult", 318 | "Image decoding result. The fields depend on whether decoding " 319 | "succeeded:\n" 320 | " - On total success, the error_message is empty and pixbuf is not " 321 | "empty.\n" 322 | " - On partial success (e.g. the input file was truncated but we are " 323 | "still able to decode some of the pixels), error_message is non-empty " 324 | "but pixbuf is not empty. It is up to the caller whether to accept " 325 | "or reject partial success.\n" 326 | " - On failure, the error_message is non-empty and pixbuf is " 327 | "empty.") 328 | .def_property_readonly( 329 | "pixbuf", 330 | [](wuffs_aux_wrap::ImageDecodingResult& self) 331 | -> pybind11::array_t { 332 | const auto height = self.pixcfg.height(); 333 | const auto width = self.pixcfg.width(); 334 | 335 | if (width == 0 || height == 0) { 336 | return {}; 337 | } 338 | 339 | constexpr size_t kNumDimensions = 3; 340 | const auto channels = self.pixcfg.pixbuf_len() / (width * height); 341 | const std::array shape = {height, width, 342 | channels}; 343 | const std::array strides = { 344 | width * channels, channels, 1}; 345 | 346 | return pybind11::array_t(pybind11::buffer_info( 347 | self.pixbuf.data(), sizeof(uint8_t), 348 | pybind11::format_descriptor::value, kNumDimensions, 349 | shape, strides)); 350 | }, 351 | "np.array: decoded pixel buffer (uint8 Numpy array of [H, " 352 | "W, C] shape).") 353 | .def_readonly("pixcfg", &wuffs_aux_wrap::ImageDecodingResult::pixcfg, 354 | "wuffs_base__pixel_config: decoded pixel buffer config.") 355 | .def_readonly("reported_metadata", 356 | &wuffs_aux_wrap::ImageDecodingResult::reported_metadata, 357 | "list: a list object containing reported data " 358 | "(only filled if any metadata was decoded and the " 359 | "corresponding ImageDecoderFlag flag was set).") 360 | .def_readonly("error_message", 361 | &wuffs_aux_wrap::ImageDecodingResult::error_message, 362 | "str: error message, empty on success, one of " 363 | "ImageDecoderError on error."); 364 | 365 | py::class_(aux_m, "ImageDecoder", 366 | "Image decoder class.") 367 | .def(py::init(), 368 | "Sole constructor. Please note that the class is not thread-safe.\n\n" 369 | "Args:" 370 | "\n config (ImageDecoderConfig): image decoder config.") 371 | .def( 372 | "decode", 373 | [](wuffs_aux_wrap::ImageDecoder& image_decoder, 374 | const py::bytes& data) -> wuffs_aux_wrap::ImageDecodingResult { 375 | py::buffer_info data_view(py::buffer(data).request()); 376 | pybind11::gil_scoped_release release_gil; 377 | return image_decoder.Decode( 378 | reinterpret_cast(data_view.ptr), data_view.size); 379 | }, 380 | "Decodes image using given byte buffer.\n\n" 381 | "Args:" 382 | "\n data (bytes): a byte buffer holding encoded image." 383 | "\nReturns:" 384 | "\n ImageDecodingResult: image decoding result.") 385 | .def( 386 | "decode", 387 | [](wuffs_aux_wrap::ImageDecoder& image_decoder, 388 | const std::string& path_to_file) 389 | -> wuffs_aux_wrap::ImageDecodingResult { 390 | pybind11::gil_scoped_release release_gil; 391 | return image_decoder.Decode(path_to_file); 392 | }, 393 | "Decodes image using given file path.\n\n" 394 | "Args:" 395 | "\n path_to_file (str): path to an image file." 396 | "\nReturns:" 397 | "\n ImageDecodingResult: image decoding result."); 398 | 399 | /* 400 | * Aux Wuffs API (DecodeJson) 401 | */ 402 | 403 | py::enum_( 404 | m, "JsonDecoderQuirks", 405 | "See " 406 | "https://github.com/google/wuffs/blob/main/std/json/decode_quirks.wuffs.") 407 | // clang-format off 408 | #define JDQE(quirk) .value(#quirk, wuffs_aux_wrap::JsonDecoderQuirks::quirk) 409 | JDQE(ALLOW_ASCII_CONTROL_CODES) 410 | JDQE(ALLOW_BACKSLASH_A) 411 | JDQE(ALLOW_BACKSLASH_CAPITAL_U) 412 | JDQE(ALLOW_BACKSLASH_E) 413 | JDQE(ALLOW_BACKSLASH_NEW_LINE) 414 | JDQE(ALLOW_BACKSLASH_QUESTION_MARK) 415 | JDQE(ALLOW_BACKSLASH_SINGLE_QUOTE) 416 | JDQE(ALLOW_BACKSLASH_V) 417 | JDQE(ALLOW_BACKSLASH_X_AS_CODE_POINTS) 418 | JDQE(ALLOW_BACKSLASH_ZERO) 419 | JDQE(ALLOW_COMMENT_BLOCK) 420 | JDQE(ALLOW_COMMENT_LINE) 421 | JDQE(ALLOW_EXTRA_COMMA) 422 | JDQE(ALLOW_INF_NAN_NUMBERS) 423 | JDQE(ALLOW_LEADING_ASCII_RECORD_SEPARATOR) 424 | JDQE(ALLOW_LEADING_UNICODE_BYTE_ORDER_MARK) 425 | JDQE(ALLOW_TRAILING_FILLER) 426 | JDQE(EXPECT_TRAILING_NEW_LINE_OR_EOF) 427 | JDQE(JSON_POINTER_ALLOW_TILDE_N_TILDE_R_TILDE_T) 428 | JDQE(REPLACE_INVALID_UNICODE) 429 | #undef JDQE 430 | // clang-format on 431 | ; 432 | 433 | py::class_(aux_m, "JsonDecoderConfig", 434 | "JSON decoder configuration.") 435 | .def(py::init<>()) 436 | .def_readwrite("quirks", &wuffs_aux_wrap::JsonDecoderConfig::quirks, 437 | "list: list of JsonDecoderQuirks, empty by default.") 438 | .def_readwrite("json_pointer", 439 | &wuffs_aux_wrap::JsonDecoderConfig::json_pointer, 440 | "str: JSON pointer."); 441 | 442 | py::class_(aux_m, "JsonDecoderError") 443 | // clang-format off 444 | #define JDEE(error) .def_readonly_static(#error, &wuffs_aux_wrap::JsonDecoderError::error) 445 | JDEE(BadJsonPointer) 446 | JDEE(NoMatch) 447 | JDEE(DuplicateMapKey) 448 | JDEE(NonStringMapKey) 449 | JDEE(NonContainerStackEntry) 450 | JDEE(BadDepth) 451 | JDEE(FailedToOpenFile) 452 | JDEE(BadC0ControlCode) 453 | JDEE(BadUtf8) 454 | JDEE(BadBackslashEscape) 455 | JDEE(BadInput) 456 | JDEE(BadNewLineInAString) 457 | JDEE(BadQuirkCombination) 458 | JDEE(UnsupportedNumberLength) 459 | JDEE(UnsupportedRecursionDepth) 460 | #undef JDEE 461 | // clang-format on 462 | ; 463 | 464 | py::class_( 465 | aux_m, "JsonDecodingResult", 466 | "JSON decoding result. The fields depend on whether decoding " 467 | "succeeded:\n" 468 | " - On total success, the error_message is empty and parsed is not " 469 | "empty.\n" 470 | " - On failure, the error_message is non-empty and parsed is empty.") 471 | .def_readonly("cursor_position", 472 | &wuffs_aux_wrap::JsonDecodingResult::cursor_position, 473 | "int: cursor position.") 474 | .def_readonly("parsed", &wuffs_aux_wrap::JsonDecodingResult::parsed, 475 | "obj: parsed JSON data.") 476 | .def_readonly("error_message", 477 | &wuffs_aux_wrap::JsonDecodingResult::error_message, 478 | "str: error message, empty on success, one of " 479 | "JsonDecoderError on error."); 480 | 481 | py::class_(aux_m, "JsonDecoder", 482 | "JSON decoder class.") 483 | .def(py::init(), 484 | "Sole constructor.\n\n" 485 | "Args:" 486 | "\n config (JsonDecoderConfig): JSON decoder config.") 487 | .def( 488 | "decode", 489 | [](wuffs_aux_wrap::JsonDecoder& json_decoder, 490 | const py::bytes& data) -> wuffs_aux_wrap::JsonDecodingResult { 491 | py::buffer_info data_view(py::buffer(data).request()); 492 | return json_decoder.Decode( 493 | reinterpret_cast(data_view.ptr), data_view.size); 494 | }, 495 | "Decodes JSON using given byte buffer.\n\n" 496 | "Args:" 497 | "\n data (bytes): a byte buffer holding JSON string." 498 | "\nReturns:" 499 | "\n JsonDecodingResult: JSON decoding result.") 500 | .def( 501 | "decode", 502 | [](wuffs_aux_wrap::JsonDecoder& json_decoder, 503 | const std::string& path_to_file) 504 | -> wuffs_aux_wrap::JsonDecodingResult { 505 | return json_decoder.Decode(path_to_file); 506 | }, 507 | "Decodes JSON using given file path.\n\n" 508 | "Args:" 509 | "\n path_to_file (str): path to a JSON file." 510 | "\nReturns:" 511 | "\n JsonDecodingResult: JSON decoding result."); 512 | } 513 | -------------------------------------------------------------------------------- /test/images/1QcSHQRnh493V4dIh4eXh1h4kJUI.th: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/1QcSHQRnh493V4dIh4eXh1h4kJUI.th -------------------------------------------------------------------------------- /test/images/bricks-color.etc2.pkm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/bricks-color.etc2.pkm -------------------------------------------------------------------------------- /test/images/hippopotamus.nie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/hippopotamus.nie -------------------------------------------------------------------------------- /test/images/lena.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.bmp -------------------------------------------------------------------------------- /test/images/lena.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.gif -------------------------------------------------------------------------------- /test/images/lena.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.jpeg -------------------------------------------------------------------------------- /test/images/lena.nie: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.nie -------------------------------------------------------------------------------- /test/images/lena.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.png -------------------------------------------------------------------------------- /test/images/lena.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.qoi -------------------------------------------------------------------------------- /test/images/lena.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.tga -------------------------------------------------------------------------------- /test/images/lena.wbmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.wbmp -------------------------------------------------------------------------------- /test/images/lena.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena.webp -------------------------------------------------------------------------------- /test/images/lena_exif.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dev0x13/pywuffs/2e62bc54bc962922fcb9da06d8cdc068e559122d/test/images/lena_exif.png -------------------------------------------------------------------------------- /test/json/invalid1.json: -------------------------------------------------------------------------------- 1 | '123' 2 | -------------------------------------------------------------------------------- /test/json/invalid2.json: -------------------------------------------------------------------------------- 1 | {"name": "Joe", "age": null, } 2 | -------------------------------------------------------------------------------- /test/json/invalid3.json: -------------------------------------------------------------------------------- 1 | { 2 | "1234": "123", 3 | 123 4 | } 5 | -------------------------------------------------------------------------------- /test/json/non-string-map-key.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | 2: "value" 4 | } 5 | -------------------------------------------------------------------------------- /test/json/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "value", 3 | "key1": 0, 4 | "key2": 1.0, 5 | "key3": ["123", 123, {"test": 1}, []], 6 | "key4": {"123": 123, "456": []} 7 | } 8 | -------------------------------------------------------------------------------- /test/json/valid1.json: -------------------------------------------------------------------------------- 1 | "123" 2 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | numpy -------------------------------------------------------------------------------- /test/test_aux_image_decoder.py: -------------------------------------------------------------------------------- 1 | import os 2 | from struct import unpack 3 | import pytest 4 | import numpy as np 5 | 6 | from pywuffs.aux import * 7 | from pywuffs import * 8 | 9 | IMAGES_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "images") 10 | TEST_IMAGES = [ 11 | (ImageDecoderType.PNG, os.path.join(IMAGES_PATH, "lena.png")), 12 | (ImageDecoderType.BMP, os.path.join(IMAGES_PATH, "lena.bmp")), 13 | (ImageDecoderType.TGA, os.path.join(IMAGES_PATH, "lena.tga")), 14 | (ImageDecoderType.NIE, os.path.join(IMAGES_PATH, "hippopotamus.nie")), 15 | (ImageDecoderType.GIF, os.path.join(IMAGES_PATH, "lena.gif")), 16 | (ImageDecoderType.WBMP, os.path.join(IMAGES_PATH, "lena.wbmp")), 17 | (ImageDecoderType.JPEG, os.path.join(IMAGES_PATH, "lena.jpeg")), 18 | (ImageDecoderType.WEBP, os.path.join(IMAGES_PATH, "lena.webp")), 19 | (ImageDecoderType.QOI, os.path.join(IMAGES_PATH, "lena.qoi")), 20 | (ImageDecoderType.ETC2, os.path.join(IMAGES_PATH, "bricks-color.etc2.pkm")), 21 | (ImageDecoderType.TH, os.path.join(IMAGES_PATH, "1QcSHQRnh493V4dIh4eXh1h4kJUI.th")) 22 | ] 23 | EXIF_FOURCC = 0x45584946 24 | 25 | 26 | # Positive test cases 27 | 28 | def assert_decoded(result, expected_metadata_len=0): 29 | assert len(result.error_message) == 0 30 | if expected_metadata_len is not None: 31 | assert len(result.reported_metadata) == expected_metadata_len 32 | assert result.pixcfg.is_valid() 33 | assert result.pixcfg.width() != 0 34 | assert result.pixcfg.height() != 0 35 | assert result.pixbuf.dtype == np.uint8 36 | assert result.pixbuf.shape[0] == result.pixcfg.height() 37 | assert result.pixbuf.shape[1] == result.pixcfg.width() 38 | assert result.pixbuf.size == result.pixcfg.pixbuf_len() != 0 39 | 40 | 41 | @pytest.mark.parametrize("param", [ 42 | ([ImageDecoderFlags.REPORT_METADATA_EXIF], 1), 43 | ([ImageDecoderFlags.REPORT_METADATA_CHRM], 1), 44 | ([ImageDecoderFlags.REPORT_METADATA_GAMA], 1), 45 | ([ImageDecoderFlags.REPORT_METADATA_EXIF, ImageDecoderFlags.REPORT_METADATA_CHRM, 46 | ImageDecoderFlags.REPORT_METADATA_GAMA], 3), 47 | ]) 48 | def test_decode_image_with_metadata(param): 49 | config = ImageDecoderConfig() 50 | config.flags = param[0] 51 | decoder = ImageDecoder(config) 52 | decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png")) 53 | assert_decoded(decoding_result, param[1]) 54 | 55 | 56 | @pytest.mark.parametrize("param", TEST_IMAGES) 57 | def test_decode_default_config(param): 58 | config = ImageDecoderConfig() 59 | decoder = ImageDecoder(config) 60 | decoding_result_from_file = decoder.decode(param[1]) 61 | assert_decoded(decoding_result_from_file) 62 | with open(param[1], "rb") as f: 63 | data = f.read() 64 | decoding_result_from_bytes = decoder.decode(data) 65 | assert_decoded(decoding_result_from_bytes) 66 | assert decoding_result_from_bytes.error_message == decoding_result_from_file.error_message 67 | assert decoding_result_from_bytes.reported_metadata == decoding_result_from_file.reported_metadata 68 | assert np.array_equal(decoding_result_from_bytes.pixbuf, decoding_result_from_file.pixbuf) 69 | assert decoding_result_from_bytes.pixcfg.is_valid() == decoding_result_from_file.pixcfg.is_valid() 70 | assert decoding_result_from_bytes.pixcfg.width() == decoding_result_from_file.pixcfg.width() 71 | assert decoding_result_from_bytes.pixcfg.height() == decoding_result_from_file.pixcfg.height() 72 | assert decoding_result_from_bytes.pixcfg.pixel_format() == decoding_result_from_file.pixcfg.pixel_format() 73 | assert decoding_result_from_bytes.pixcfg.pixel_subsampling() == decoding_result_from_file.pixcfg.pixel_subsampling() 74 | assert decoding_result_from_bytes.pixcfg.pixbuf_len() == decoding_result_from_file.pixcfg.pixbuf_len() 75 | 76 | 77 | @pytest.mark.parametrize("param", TEST_IMAGES) 78 | def test_decode_specific_decoder(param): 79 | config = ImageDecoderConfig() 80 | config.enabled_decoders = [param[0]] 81 | decoder = ImageDecoder(config) 82 | decoding_result = decoder.decode(param[1]) 83 | assert_decoded(decoding_result) 84 | 85 | 86 | @pytest.mark.parametrize("pixel_format", [ 87 | PixelFormat.BGR_565, 88 | PixelFormat.BGR, 89 | PixelFormat.BGRA_NONPREMUL, 90 | PixelFormat.BGRA_NONPREMUL_4X16LE, 91 | PixelFormat.BGRA_PREMUL, 92 | PixelFormat.RGBA_NONPREMUL, 93 | PixelFormat.RGBA_PREMUL 94 | ]) 95 | @pytest.mark.parametrize("pixel_blend", [ 96 | PixelBlend.SRC, 97 | PixelBlend.SRC_OVER 98 | ]) 99 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 100 | def test_decode_pixel_format_and_blend(pixel_format, pixel_blend, test_image): 101 | config = ImageDecoderConfig() 102 | config.pixel_format = pixel_format 103 | config.pixel_blend = pixel_blend 104 | decoder = ImageDecoder(config) 105 | decoding_result = decoder.decode(test_image[1]) 106 | assert_decoded(decoding_result) 107 | assert decoding_result.pixcfg.pixel_format() == pixel_format 108 | 109 | 110 | @pytest.mark.parametrize("background_color", [0, 1, 0x7F7F_0000]) 111 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 112 | def test_decode_background_color(background_color, test_image): 113 | config = ImageDecoderConfig() 114 | config.background_color = background_color 115 | decoder = ImageDecoder(config) 116 | decoding_result = decoder.decode(test_image[1]) 117 | assert_decoded(decoding_result) 118 | 119 | 120 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 121 | @pytest.mark.parametrize("quirk", [ 122 | ImageDecoderQuirks.IGNORE_CHECKSUM, 123 | ImageDecoderQuirks.GIF_DELAY_NUM_DECODED_FRAMES, 124 | ImageDecoderQuirks.GIF_FIRST_FRAME_LOCAL_PALETTE_MEANS_BLACK_BACKGROUND, 125 | ImageDecoderQuirks.GIF_QUIRK_HONOR_BACKGROUND_COLOR, 126 | ImageDecoderQuirks.GIF_IGNORE_TOO_MUCH_PIXEL_DATA, 127 | ImageDecoderQuirks.GIF_IMAGE_BOUNDS_ARE_STRICT, 128 | ImageDecoderQuirks.GIF_REJECT_EMPTY_FRAME, 129 | ImageDecoderQuirks.GIF_REJECT_EMPTY_PALETTE, 130 | ImageDecoderQuirks.QUALITY 131 | ]) 132 | def test_decode_image_quirks(test_image, quirk): 133 | config = ImageDecoderConfig() 134 | config.quirks = {quirk: 1} 135 | decoder = ImageDecoder(config) 136 | decoding_result = decoder.decode(test_image[1]) 137 | assert_decoded(decoding_result) 138 | 139 | 140 | def test_decode_image_quirks_quality(): 141 | config = ImageDecoderConfig() 142 | config.quirks = {ImageDecoderQuirks.QUALITY: LowerQuality} 143 | decoder = ImageDecoder(config) 144 | decoding_result_lower_quality = decoder.decode(os.path.join(IMAGES_PATH, "lena.jpeg")) 145 | assert_decoded(decoding_result_lower_quality) 146 | assert decoding_result_lower_quality.pixbuf.shape == (32, 32, 4) 147 | config.quirks = {ImageDecoderQuirks.QUALITY: HigherQuality} 148 | decoder = ImageDecoder(config) 149 | decoding_result_higher_quality = decoder.decode(os.path.join(IMAGES_PATH, "lena.jpeg")) 150 | assert_decoded(decoding_result_higher_quality) 151 | assert decoding_result_higher_quality.pixbuf.shape == (32, 32, 4) 152 | assert decoding_result_lower_quality != decoding_result_higher_quality 153 | 154 | 155 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 156 | @pytest.mark.parametrize("flag", [ 157 | ImageDecoderFlags.REPORT_METADATA_BGCL, 158 | ImageDecoderFlags.REPORT_METADATA_CHRM, 159 | ImageDecoderFlags.REPORT_METADATA_EXIF, 160 | ImageDecoderFlags.REPORT_METADATA_GAMA, 161 | ImageDecoderFlags.REPORT_METADATA_ICCP, 162 | ImageDecoderFlags.REPORT_METADATA_MTIM, 163 | ImageDecoderFlags.REPORT_METADATA_OFS2, 164 | ImageDecoderFlags.REPORT_METADATA_PHYD, 165 | ImageDecoderFlags.REPORT_METADATA_SRGB, 166 | ImageDecoderFlags.REPORT_METADATA_XMP 167 | ]) 168 | def test_decode_image_flags(test_image, flag): 169 | config = ImageDecoderConfig() 170 | config.flags = [flag] 171 | decoder = ImageDecoder(config) 172 | decoding_result = decoder.decode(test_image[1]) 173 | assert_decoded(decoding_result, None) 174 | 175 | 176 | def test_decode_image_exif_metadata(): 177 | config = ImageDecoderConfig() 178 | config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF] 179 | decoder = ImageDecoder(config) 180 | decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png")) 181 | assert_decoded(decoding_result, 1) 182 | assert decoding_result.pixbuf.shape == (32, 32, 4) 183 | meta_minfo = decoding_result.reported_metadata[0].minfo 184 | meta_bytes = decoding_result.reported_metadata[0].data.tobytes() 185 | assert meta_minfo.metadata__fourcc() == EXIF_FOURCC 186 | assert meta_bytes[:2] == b"II" # little endian 187 | exif_orientation = 0 188 | cursor = 0 189 | 190 | def get_uint16(): 191 | return unpack("@H", meta_bytes[cursor:cursor + 2])[0] 192 | 193 | while cursor < len(meta_bytes): 194 | if get_uint16() == 0x0112: # orientation flag 195 | cursor += 8 196 | exif_orientation = get_uint16() 197 | cursor += 2 198 | assert exif_orientation == 3 199 | 200 | 201 | # Negative test cases 202 | 203 | def assert_not_decoded(result, expected_error_message=None, expected_metadata_length=0): 204 | assert len(result.error_message) != 0 205 | if expected_error_message: 206 | assert result.error_message == expected_error_message 207 | assert not result.pixcfg.is_valid() 208 | assert result.pixcfg.width() == 0 209 | assert result.pixcfg.height() == 0 210 | assert result.pixbuf.dtype == np.uint8 211 | assert result.pixbuf.size == result.pixcfg.pixbuf_len() == 0 212 | assert len(result.reported_metadata) == expected_metadata_length 213 | 214 | 215 | def test_decode_image_invalid_kvp_chunk(): 216 | config = ImageDecoderConfig() 217 | config.flags = [ImageDecoderFlags.REPORT_METADATA_KVP] 218 | decoder = ImageDecoder(config) 219 | decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena.png")) 220 | assert_not_decoded(decoding_result, "png: bad text chunk (not Latin-1)", 1) 221 | 222 | 223 | def test_decode_non_existent_file(): 224 | decoder = ImageDecoder(ImageDecoderConfig()) 225 | decoding_result = decoder.decode("random123") 226 | assert_not_decoded(decoding_result, ImageDecoderError.FailedToOpenFile) 227 | 228 | 229 | @pytest.mark.parametrize("param", [b"", b"123"]) 230 | def test_decode_invalid_bytes(param): 231 | decoder = ImageDecoder(ImageDecoderConfig()) 232 | decoding_result = decoder.decode(param) 233 | assert_not_decoded(decoding_result, ImageDecoderError.UnsupportedImageFormat) 234 | 235 | 236 | @pytest.mark.parametrize("param", TEST_IMAGES) 237 | def test_decode_image_formats_truncated(param): 238 | config = ImageDecoderConfig() 239 | config.enabled_decoders = [param[0]] 240 | decoder = ImageDecoder(config) 241 | with open(param[1], "rb") as f: 242 | data = f.read() 243 | data = data[:len(data) - 32] 244 | decoding_result = decoder.decode(data) 245 | assert decoding_result.error_message.endswith("truncated input") 246 | assert decoding_result.pixcfg.is_valid() 247 | assert decoding_result.pixcfg.width() != 0 248 | assert decoding_result.pixcfg.height() != 0 249 | assert decoding_result.pixbuf.dtype == np.uint8 250 | assert decoding_result.pixbuf.size == decoding_result.pixcfg.pixbuf_len() != 0 251 | assert len(decoding_result.reported_metadata) == 0 252 | 253 | 254 | @pytest.mark.parametrize("param", TEST_IMAGES) 255 | def test_decode_image_unsupported_image_format(param): 256 | config = ImageDecoderConfig() 257 | enabled_decoders = config.enabled_decoders 258 | enabled_decoders.remove(param[0]) 259 | config.enabled_decoders = enabled_decoders 260 | decoder = ImageDecoder(config) 261 | decoding_result = decoder.decode(param[1]) 262 | assert_not_decoded(decoding_result, ImageDecoderError.UnsupportedImageFormat) 263 | 264 | 265 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 266 | def test_decode_image_unsupported_pixel_format(test_image): 267 | config = ImageDecoderConfig() 268 | config.pixel_format = PixelFormat.YCOCGK 269 | decoder = ImageDecoder(config) 270 | decoding_result = decoder.decode(test_image[1]) 271 | assert_not_decoded(decoding_result, ImageDecoderError.UnsupportedPixelFormat) 272 | 273 | 274 | @pytest.mark.parametrize("test_image", TEST_IMAGES) 275 | def test_decode_image_max_incl_dimension(test_image): 276 | config = ImageDecoderConfig() 277 | config.max_incl_dimension = 8 278 | decoder = ImageDecoder(config) 279 | decoding_result = decoder.decode(test_image[1]) 280 | assert_not_decoded(decoding_result, ImageDecoderError.MaxInclDimensionExceeded) 281 | 282 | 283 | def test_decode_image_max_incl_metadata_length(): 284 | config = ImageDecoderConfig() 285 | config.max_incl_metadata_length = 8 286 | config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF] 287 | decoder = ImageDecoder(config) 288 | decoding_result = decoder.decode(os.path.join(IMAGES_PATH, "lena_exif.png")) 289 | assert_not_decoded(decoding_result, ImageDecoderError.MaxInclMetadataLengthExceeded) 290 | 291 | 292 | # Multithreading tests 293 | 294 | from concurrent.futures import ThreadPoolExecutor 295 | 296 | 297 | def test_decode_multithreaded(): 298 | config = ImageDecoderConfig() 299 | 300 | def decode(image): 301 | decoder = ImageDecoder(config) 302 | return decoder.decode(image) 303 | 304 | test_image = os.path.join(IMAGES_PATH, "lena.png") 305 | with open(test_image, "rb") as f: 306 | test_image_data = f.read() 307 | 308 | for payload in (test_image, test_image_data): 309 | for num_threads in (1, 2, 4, 8): 310 | with ThreadPoolExecutor(max_workers=num_threads) as executor: 311 | futures = [executor.submit(decode, payload) for _ in range(num_threads)] 312 | results = [future.result() for future in futures] 313 | for result in results: 314 | assert_decoded(result) 315 | assert np.array_equal(results[0].pixbuf, result.pixbuf) 316 | 317 | 318 | def test_decode_multithreaded_with_metadata(): 319 | config = ImageDecoderConfig() 320 | config.flags = [ImageDecoderFlags.REPORT_METADATA_EXIF] 321 | 322 | def decode(image): 323 | decoder = ImageDecoder(config) 324 | return decoder.decode(image) 325 | 326 | test_image = os.path.join(IMAGES_PATH, "lena_exif.png") 327 | num_threads = 4 328 | 329 | with ThreadPoolExecutor(max_workers=num_threads) as executor: 330 | futures = [executor.submit(decode, test_image) for _ in range(num_threads)] 331 | results = [future.result() for future in futures] 332 | for result in results: 333 | assert_decoded(result, 1) 334 | meta_minfo = result.reported_metadata[0].minfo 335 | meta_bytes = result.reported_metadata[0].data.tobytes() 336 | assert meta_minfo.metadata__fourcc() == EXIF_FOURCC 337 | assert meta_bytes[:2] == b"II" 338 | -------------------------------------------------------------------------------- /test/test_aux_json_decoder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import pytest 4 | 5 | from pywuffs import * 6 | from pywuffs.aux import * 7 | 8 | JSON_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), "json/") 9 | 10 | # Positive test cases 11 | 12 | 13 | def assert_decoded(result, encoded=None, file=None): 14 | assert encoded or file 15 | assert len(result.error_message) == 0 16 | assert result.cursor_position != 0 17 | if file: 18 | with open(file, "rb") as f: 19 | encoded = f.read() 20 | if encoded: 21 | assert result.parsed == json.loads(encoded.decode("utf-8")) 22 | 23 | 24 | @pytest.mark.parametrize("file_path", [ 25 | (JSON_PATH + "/simple.json"), 26 | (JSON_PATH + "/valid1.json"), 27 | ]) 28 | def test_decode_default_config(file_path): 29 | config = JsonDecoderConfig() 30 | decoder = JsonDecoder(config) 31 | decoding_result_from_file = decoder.decode(file_path) 32 | assert_decoded(decoding_result_from_file, file=file_path) 33 | with open(file_path, "rb") as f: 34 | data = f.read() 35 | decoding_result_from_bytes = decoder.decode(data) 36 | assert_decoded(decoding_result_from_bytes, encoded=data) 37 | assert decoding_result_from_bytes.error_message == decoding_result_from_file.error_message 38 | assert decoding_result_from_bytes.parsed == decoding_result_from_file.parsed 39 | assert decoding_result_from_bytes.cursor_position == decoding_result_from_file.cursor_position 40 | 41 | 42 | def test_decode_json_quirks(): 43 | config = JsonDecoderConfig() 44 | config.quirks = {JsonDecoderQuirks.ALLOW_COMMENT_BLOCK: 1, 45 | JsonDecoderQuirks.ALLOW_EXTRA_COMMA: 1} 46 | decoder = JsonDecoder(config) 47 | data = b"{\"test\": \"value\", \"test1\": 123,}" 48 | decoding_result = decoder.decode(data) 49 | assert_decoded(decoding_result, encoded=data[:-2] + b"}") 50 | 51 | 52 | def test_decode_json_pointer(): 53 | data = {"key1": 1, "key2": [2, 3], "key3": "value"} 54 | config = JsonDecoderConfig() 55 | config.json_pointer = "/key2" 56 | decoder = JsonDecoder(config) 57 | decoding_result = decoder.decode(bytes(json.dumps(data), "utf-8")) 58 | assert_decoded(decoding_result, encoded=bytes( 59 | json.dumps(data["key2"]), "utf-8")) 60 | 61 | # Negative test cases 62 | 63 | 64 | def assert_not_decoded(result, expected_error_message=None): 65 | assert len(result.error_message) != 0 66 | if expected_error_message: 67 | assert result.error_message == expected_error_message 68 | assert result.parsed is None 69 | 70 | 71 | def test_decode_non_existent_file(): 72 | decoder = JsonDecoder(JsonDecoderConfig()) 73 | decoding_result = decoder.decode("random123") 74 | assert_not_decoded(decoding_result, JsonDecoderError.FailedToOpenFile) 75 | 76 | 77 | @pytest.mark.parametrize("param", [ 78 | (b"+(=)", JsonDecoderError.BadDepth), 79 | (b"test", JsonDecoderError.BadDepth), 80 | (b"{\"val\":" + b"1"*130 + b"}", JsonDecoderError.UnsupportedNumberLength), 81 | (b"{\"val\": 1, \"val\": 2}", JsonDecoderError.DuplicateMapKey + "val"), 82 | ]) 83 | def test_decode_invalid_bytes(param): 84 | decoder = JsonDecoder(JsonDecoderConfig()) 85 | decoding_result = decoder.decode(param[0]) 86 | assert_not_decoded(decoding_result, param[1]) 87 | 88 | 89 | @pytest.mark.parametrize("param", [ 90 | (JSON_PATH + "/invalid1.json", JsonDecoderError.BadDepth), 91 | (JSON_PATH + "/invalid2.json", JsonDecoderError.BadInput), 92 | (JSON_PATH + "/invalid3.json", JsonDecoderError.BadInput), 93 | (JSON_PATH + "/non-string-map-key.json", JsonDecoderError.BadInput) 94 | ]) 95 | def test_decode_invalid_file(param): 96 | decoder = JsonDecoder(JsonDecoderConfig()) 97 | decoding_result = decoder.decode(param[0]) 98 | assert_not_decoded(decoding_result, param[1]) 99 | 100 | 101 | def test_decode_invalid_json_pointer(): 102 | data = {"key1": 1, "key2": [2, 3], "key3": "value"} 103 | config = JsonDecoderConfig() 104 | config.json_pointer = "/random" 105 | decoder = JsonDecoder(config) 106 | decoding_result = decoder.decode(bytes(json.dumps(data), "utf-8")) 107 | assert_not_decoded(decoding_result, JsonDecoderError.BadDepth) 108 | --------------------------------------------------------------------------------