diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..44db9e4 --- /dev/null +++ b/.clang-format @@ -0,0 +1,22 @@ +BasedOnStyle: 'Google' + +AlignConsecutiveMacros: + Enabled: true + AcrossEmptyLines: true + AcrossComments: true + AlignCompound: false + PadOperators: false +AccessModifierOffset: -4 +#AlignAfterOpenBracket: AlwaysBreak +AlignArrayOfStructures: Right +ColumnLimit: 120 +QualifierAlignment: Left +FixNamespaceComments: true +IndentWidth: 4 +JavaScriptQuotes: Single +NamespaceIndentation: None +ObjCBlockIndentWidth: 4 +PointerAlignment: Right +SpaceBeforeRangeBasedForLoopColon: true +SpacesInContainerLiterals: false +InsertNewlineAtEOF: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..211c9f7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,91 @@ +name: ci + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + code-style: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install clang-format-17 + run: | + sudo apt-get remove clang-format* + sudo apt-get autoremove + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + echo "deb https://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main" | sudo tee -a /etc/apt/sources.list + echo "deb-src https://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main" | sudo tee -a /etc/apt/sources.list + sudo apt-get update + sudo apt-get install clang-format-17 + sudo ln -s -f /usr/bin/clang-format-17 /usr/bin/clang-format + clang-format --version + - name: Check code style + run: | + cmake -B build . + cmake --build build -- check-format + + linux: + needs: code-style + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build + run: | + cmake -B build . + cmake --build build -- all + cmake --build build -- check + + macos: + needs: code-style + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build + run: | + cmake -B build . + cmake --build build -- all + cmake --build build -- check + + windows-x64: + needs: code-style + runs-on: windows-2019 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build + shell: cmd + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + cmake -GNinja -B build . + cmake --build build -- all + cmake --build build -- check + + windows-x86: + needs: code-style + runs-on: windows-2019 + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Build + shell: cmd + run: | + call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars32.bat" + cmake -GNinja -B build . + cmake --build build -- all + cmake --build build -- check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9fbd7db --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +.DS_Store +cmake-build-*/ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..29bc951 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,83 @@ +cmake_minimum_required(VERSION 3.15) +project(imageinfo) + +set(CMAKE_CXX_STANDARD 11) + +set(IMAGEINFO_IS_MASTER_PROJECT OFF) +if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + set(IMAGEINFO_IS_MASTER_PROJECT ON) +endif () + +option(IMAGEINFO_BUILD_TOOLS "Build tool" ${IMAGEINFO_IS_MASTER_PROJECT}) +option(IMAGEINFO_BUILD_TESTS "Build tests" ${IMAGEINFO_IS_MASTER_PROJECT}) +option(IMAGEINFO_BUILD_INSTALL "Build install" ${IMAGEINFO_IS_MASTER_PROJECT}) + +add_library(imageinfo INTERFACE) +add_library(imageinfo::imageinfo ALIAS imageinfo) + +include(GNUInstallDirs) + +target_include_directories(imageinfo INTERFACE + "$" + "$" +) + +if (IMAGEINFO_BUILD_TOOLS) + add_executable(imageinfo_cli cli/main.cpp) + target_link_libraries(imageinfo_cli PRIVATE imageinfo) + set_target_properties(imageinfo_cli PROPERTIES OUTPUT_NAME "imageinfo") +endif () + +if (IMAGEINFO_BUILD_TESTS) + enable_testing() + + add_executable(imageinfo_tests tests/tests.cpp) + target_link_libraries(imageinfo_tests PRIVATE imageinfo) + target_compile_definitions(imageinfo_tests PRIVATE + -DIMAGES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/images/" + ) + add_test(NAME imageinfo_tests COMMAND imageinfo_tests) + + if (CMAKE_CONFIGURATION_TYPES) + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} + --force-new-ctest-process --output-on-failure + --build-config "$") + else () + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} + --force-new-ctest-process --output-on-failure) + endif () +endif () + +if (IMAGEINFO_BUILD_INSTALL) + install(TARGETS imageinfo EXPORT imageinfo) + install( + FILES "${CMAKE_CURRENT_SOURCE_DIR}/include/imageinfo.hpp" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + ) + install( + EXPORT imageinfo + FILE imageinfo-config.cmake + DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/imageinfo" + NAMESPACE imageinfo:: + ) + if (IMAGEINFO_BUILD_TOOLS) + install(TARGETS imageinfo_cli) + endif () +endif () + +if (IMAGEINFO_IS_MASTER_PROJECT) + find_program(CLANG_FORMAT clang-format) + if (CLANG_FORMAT) + set(ALL_SOURCES include/imageinfo.hpp cli/main.cpp tests/tests.cpp) + add_custom_target( + format + COMMAND "${CLANG_FORMAT}" -i -verbose ${ALL_SOURCES} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) + add_custom_target( + check-format + COMMAND "${CLANG_FORMAT}" --dry-run --Werror ${ALL_SOURCES} + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + ) + endif () +endif () diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8f5e3c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 xiaozhuai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cf61ea --- /dev/null +++ b/README.md @@ -0,0 +1,179 @@ +# imageinfo + +Cross platform super fast single header c++ library to get image size and format without loading/decoding. + +The imageinfo don't get image format by file ext name, but infer by file header bytes and character. + +As few I/O times as possible! Read as few bytes as possible! + +Some test image files are from [image-size](https://github.com/image-size/image-size). Many thanks to [@netroy](https://github.com/netroy). + +Rust version: [imageinfo-rs](https://github.com/xiaozhuai/imageinfo-rs) + +[![ci](https://github.com/xiaozhuai/imageinfo/actions/workflows/ci.yml/badge.svg)](https://github.com/xiaozhuai/imageinfo/actions/workflows/ci.yml) + +imageinfo has been restructured, if you are using old version, please check `v1` branch. + +## Supported formats + +* [x] avif +* [x] bmp +* [x] cur +* [x] dds +* [x] gif +* [x] hdr (pic) +* [x] heic (heif) +* [x] icns +* [x] ico +* [x] jp2 +* [x] jpeg (jpg) +* [x] jpx +* [x] ktx +* [x] png +* [x] psd +* [x] qoi +* [ ] svg +* [x] tga +* [x] tiff (tif) +* [x] webp +* [ ] more coming... + +## Vcpkg + +imageinfo is available on [vcpkg](https://github.com/microsoft/vcpkg) + +```shell +vcpgk install imageinfo +``` + +## Build & Test + +### Linux & MacOS + +```shell +cmake -B build . +cmake --build build -- all +cmake --build build -- check +``` + +### Windows + +Open Visual Studio Command Prompt and run these command + +```cmd +cmake -G "NMake Makefiles" -B build . +cmake --build build -- all +cmake --build build -- check +``` + +## Usage + +### Simplest Demo + +```cpp +const char *file = "images/valid/jpg/sample.jpg"; +auto info = imageinfo::parse(file); +std::cout << "File: " << file << "\n"; +std::cout << " - Error : " << info.error_msg() << "\n"; +std::cout << " - Width : " << info.size().width << "\n"; +std::cout << " - Height : " << info.size().height << "\n"; +std::cout << " - Format : " << info.format() << "\n"; +std::cout << " - Ext : " << info.ext() << "\n"; +std::cout << " - Full Ext : " << info.full_ext() << "\n"; +std::cout << " - Mimetype : " << info.mimetype() << "\n\n"; +``` + +You can pass a file path and use `imageinfo::FilePathReader`, + +and there are some builtin reader `imageinfo::FileReader`, `imageinfo::FileStreamReader`, `imageinfo::RawDataReader` + +```cpp +FILE *file = fopen("images/valid/jpg/sample.jpg", "rb"); +auto info = imageinfo::parse(file); +fclose(file); +``` + +```cpp +std::ifstream file("images/valid/jpg/sample.jpg", std::ios::in | std::ios::binary); +auto info = imageinfo::parse(file); +file.close(); +``` + +```cpp +// Suppose we already got data and size +// void *data; +// size_t size; +auto info = imageinfo::parse(imageinfo::RawData(data, size)); +``` + +If you known the file is likely a JPEG, you can provide `likely_formats` parameter to improve performance; + +```cpp +auto imageInfo = imageinfo::parse("images/valid/jpg/sample.jpg", {II_FORMAT_JPEG}); +``` + +### Custom Reader + +First, take a look at `imageinfo::FileReader`, all your need to do is define a class and implement `size` and `read` method. (not override) + +```cpp +class FileReader { +public: + explicit FileReader(FILE *file) : file_(file) {} + + inline size_t size() { + if (file_ != nullptr) { + fseek(file_, 0, SEEK_END); + return ftell(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + fseek(file_, offset, SEEK_SET); + fread(buf, 1, size, file_); + } + +private: + FILE *file_ = nullptr; +}; +``` + +Then, let's try to make a reader for Android assets file + +```cpp +class AndroidAssetFileReader { +public: + explicit AndroidAssetFileReader(AAsset *file) : file_(file) {} + + inline size_t size() { + if (file_ != nullptr) { + return AAsset_getLength(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + AAsset_seek(file_, offset, SEEK_SET); + AAsset_read(file_, buf, size); + } + +private: + AAsset *file_ = nullptr; +}; +``` + +```cpp +// Suppose we have a AAssetManager +// AAssetManager *manager; +// Open with AASSET_MODE_RANDOM mode to seek forward and backward +AAsset *file = AAssetManager_open(manager, "test.png", AASSET_MODE_RANDOM); +auto imageInfo = imageinfo::parse(file); +AAsset_close(file); +``` + +Pretty easy? + +Don't be stingy with your star : ) diff --git a/README_CN.md b/README_CN.md new file mode 100644 index 0000000..0295809 --- /dev/null +++ b/README_CN.md @@ -0,0 +1,179 @@ +# imageinfo + +跨平台高性能的C++单个头文件库,在不加载/解码图片的情况下,获取图片文件类型和大小。 + +imageinfo 并不是通过扩展名来识别图片格式,而是通过文件头和文件格式特征来判断图片格式。 + +尽可能少的I/O次数!读取尽可能少的字节数! + +部分测试图片文件来源于 [image-size](https://github.com/image-size/image-size) ,感谢 [@netroy](https://github.com/netroy) + +Rust 版本: [imageinfo-rs](https://github.com/xiaozhuai/imageinfo-rs) + +[![ci](https://github.com/xiaozhuai/imageinfo/actions/workflows/ci.yml/badge.svg)](https://github.com/xiaozhuai/imageinfo/actions/workflows/ci.yml) + +imageinfo 已经重构,如果你使用的是旧版本,请查看 `v1` 分支。 + +## 支持格式 + +* [x] avif +* [x] bmp +* [x] cur +* [x] dds +* [x] gif +* [x] hdr (pic) +* [x] heic (heif) +* [x] icns +* [x] ico +* [x] jp2 +* [x] jpeg (jpg) +* [x] jpx +* [x] ktx +* [x] png +* [x] psd +* [x] qoi +* [ ] svg +* [x] tga +* [x] tiff (tif) +* [x] webp +* [ ] 更多... + +## Vcpkg + +imageinfo 可以通过 [vcpkg](https://github.com/microsoft/vcpkg) 安装 + +```shell +vcpgk install imageinfo +``` + +## 构建 & 测试 + +### Linux & MacOS + +```shell +cmake -B build . +cmake --build build -- all +cmake --build build -- check +``` + +### Windows + +打开 Visual Studio Command Prompt 并执行下面的命令 + +```cmd +cmake -G "NMake Makefiles" -B build . +cmake --build build -- all +cmake --build build -- check +``` + +## 用法 + +### 最简DEMO代码 + +```cpp +const char *file = "images/valid/jpg/sample.jpg"; +auto info = imageinfo::parse(file); +std::cout << "File: " << file << "\n"; +std::cout << " - Error : " << info.error_msg() << "\n"; +std::cout << " - Width : " << info.size().width << "\n"; +std::cout << " - Height : " << info.size().height << "\n"; +std::cout << " - Format : " << info.format() << "\n"; +std::cout << " - Ext : " << info.ext() << "\n"; +std::cout << " - Full Ext : " << info.full_ext() << "\n"; +std::cout << " - Mimetype : " << info.mimetype() << "\n\n"; +``` + +可以使用 `imageinfo::FilePathReader` 然后直接传入文件路径, + +不同类型可以使用不同的 Reader, 如 `imageinfo::FileReader`, `imageinfo::FileStreamReader`, `imageinfo::RawDataReader` + +```cpp +FILE *file = fopen("images/valid/jpg/sample.jpg", "rb"); +auto info = imageinfo::parse(file); +fclose(file); +``` + +```cpp +std::ifstream file("images/valid/jpg/sample.jpg", std::ios::in | std::ios::binary); +auto info = imageinfo::parse(file); +file.close(); +``` + +```cpp +// 假设已经得到了 data 和 size +// void *data; +// size_t size; +auto info = imageinfo::parse(imageinfo::RawData(data, size)); +``` + +如果你事先知道一个文件大概率是JPEG格式, 你可以提供额外的 `likely_formats` 参数来提升性能; + +```cpp +auto imageInfo = imageinfo::parse("images/valid/jpg/sample.jpg", {II_FORMAT_JPEG}); +``` + +### 自定义Reader + +首先,来看一下 `imageinfo::FileReader`, 要做的只是定义一个类,然后实现 `size` 和 `read` 方法。(非override) + +```cpp +class FileReader { +public: + explicit FileReader(FILE *file) : file_(file) {} + + inline size_t size() { + if (file_ != nullptr) { + fseek(file_, 0, SEEK_END); + return ftell(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + fseek(file_, offset, SEEK_SET); + fread(buf, 1, size, file_); + } + +private: + FILE *file_ = nullptr; +}; +``` + +然后,让我们来尝试实现一个Android assets文件的Reader + +```cpp +class AndroidAssetFileReader { +public: + explicit AndroidAssetFileReader(AAsset *file) : file_(file) {} + + inline size_t size() { + if (file_ != nullptr) { + return AAsset_getLength(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + AAsset_seek(file_, offset, SEEK_SET); + AAsset_read(file_, buf, size); + } + +private: + AAsset *file_ = nullptr; +}; +``` + +```cpp +// 假设已经得到了 AAssetManager +// AAssetManager *manager; +// 以 AASSET_MODE_RANDOM 模式打开以支持双向seek +AAsset *file = AAssetManager_open(manager, "test.png", AASSET_MODE_RANDOM); +auto imageInfo = imageinfo::parse(file); +AAsset_close(file); +``` + +很简单不是吗? + +请不要吝啬你的Star : ) diff --git a/cli/main.cpp b/cli/main.cpp new file mode 100644 index 0000000..a3d22d0 --- /dev/null +++ b/cli/main.cpp @@ -0,0 +1,33 @@ +#include + +#include "imageinfo.hpp" + +int main(int argc, char **argv) { + if (argc < 2) { + printf("Usage: %s [FILE]...\n", argv[0]); + return 1; + } + + for (int i = 1; i < argc; ++i) { + const char *file = argv[i]; + auto info = imageinfo::parse(file); + printf("File: %s\n", file); + if (!info) { + printf(" - Error : %s\n", info.error_msg()); + } else { + printf(" - Format : %d\n", info.format()); + printf(" - Ext : %s\n", info.ext()); + printf(" - Full Ext : %s\n", info.full_ext()); + printf(" - Size : {width: %lld, height: %lld}\n", info.size().width, info.size().height); + printf(" - Mimetype : %s\n", info.mimetype()); + if (!info.entry_sizes().empty()) { + printf(" - Entries :\n"); + for (const auto &entrySize : info.entry_sizes()) { + printf(" - {width: %lld, height: %lld}\n", entrySize[0], entrySize[1]); + } + } + } + } + + return 0; +} diff --git a/images/invalid/sample.png b/images/invalid/sample.png new file mode 100644 index 0000000..cd085ea Binary files /dev/null and b/images/invalid/sample.png differ diff --git a/images/valid/avif/sample.avif b/images/valid/avif/sample.avif new file mode 100644 index 0000000..3027c6c Binary files /dev/null and b/images/valid/avif/sample.avif differ diff --git a/images/valid/avif/sample2.avif b/images/valid/avif/sample2.avif new file mode 100644 index 0000000..c2decef Binary files /dev/null and b/images/valid/avif/sample2.avif differ diff --git a/images/valid/avif/sample3.avif b/images/valid/avif/sample3.avif new file mode 100644 index 0000000..a43c6b7 Binary files /dev/null and b/images/valid/avif/sample3.avif differ diff --git a/images/valid/bmp/sample.bmp b/images/valid/bmp/sample.bmp new file mode 100644 index 0000000..e8b0157 Binary files /dev/null and b/images/valid/bmp/sample.bmp differ diff --git a/images/valid/bmp/sample2.bmp b/images/valid/bmp/sample2.bmp new file mode 100644 index 0000000..add4280 Binary files /dev/null and b/images/valid/bmp/sample2.bmp differ diff --git a/images/valid/cur/sample.cur b/images/valid/cur/sample.cur new file mode 100644 index 0000000..13c0e95 Binary files /dev/null and b/images/valid/cur/sample.cur differ diff --git a/images/valid/dds/sample.dds b/images/valid/dds/sample.dds new file mode 100644 index 0000000..e4f09be Binary files /dev/null and b/images/valid/dds/sample.dds differ diff --git a/images/valid/gif/sample.gif b/images/valid/gif/sample.gif new file mode 100644 index 0000000..402f8a5 Binary files /dev/null and b/images/valid/gif/sample.gif differ diff --git a/images/valid/hdr/sample.hdr b/images/valid/hdr/sample.hdr new file mode 100644 index 0000000..a4e329f Binary files /dev/null and b/images/valid/hdr/sample.hdr differ diff --git a/images/valid/hdr/sample2.hdr b/images/valid/hdr/sample2.hdr new file mode 100644 index 0000000..0d260b4 Binary files /dev/null and b/images/valid/hdr/sample2.hdr differ diff --git a/images/valid/heic/sample.heic b/images/valid/heic/sample.heic new file mode 100644 index 0000000..7a0001a Binary files /dev/null and b/images/valid/heic/sample.heic differ diff --git a/images/valid/heic/sample2.heic b/images/valid/heic/sample2.heic new file mode 100644 index 0000000..00cc549 Binary files /dev/null and b/images/valid/heic/sample2.heic differ diff --git a/images/valid/heic/sample3.heic b/images/valid/heic/sample3.heic new file mode 100644 index 0000000..91d5dc1 Binary files /dev/null and b/images/valid/heic/sample3.heic differ diff --git a/images/valid/icns/sample.icns b/images/valid/icns/sample.icns new file mode 100644 index 0000000..b104174 Binary files /dev/null and b/images/valid/icns/sample.icns differ diff --git a/images/valid/ico/multi-size-compressed.ico b/images/valid/ico/multi-size-compressed.ico new file mode 100644 index 0000000..67ccb2f Binary files /dev/null and b/images/valid/ico/multi-size-compressed.ico differ diff --git a/images/valid/ico/multi-size.ico b/images/valid/ico/multi-size.ico new file mode 100644 index 0000000..10b20d6 Binary files /dev/null and b/images/valid/ico/multi-size.ico differ diff --git a/images/valid/ico/sample-256-compressed.ico b/images/valid/ico/sample-256-compressed.ico new file mode 100644 index 0000000..3acf8e3 Binary files /dev/null and b/images/valid/ico/sample-256-compressed.ico differ diff --git a/images/valid/ico/sample-256.ico b/images/valid/ico/sample-256.ico new file mode 100644 index 0000000..bac2f28 Binary files /dev/null and b/images/valid/ico/sample-256.ico differ diff --git a/images/valid/ico/sample-compressed.ico b/images/valid/ico/sample-compressed.ico new file mode 100644 index 0000000..9cec96e Binary files /dev/null and b/images/valid/ico/sample-compressed.ico differ diff --git a/images/valid/ico/sample.ico b/images/valid/ico/sample.ico new file mode 100644 index 0000000..894250d Binary files /dev/null and b/images/valid/ico/sample.ico differ diff --git a/images/valid/jp2/jpx_disguised_as_jp2.jp2 b/images/valid/jp2/jpx_disguised_as_jp2.jp2 new file mode 100644 index 0000000..a8ae810 Binary files /dev/null and b/images/valid/jp2/jpx_disguised_as_jp2.jp2 differ diff --git a/images/valid/jp2/sample.jp2 b/images/valid/jp2/sample.jp2 new file mode 100644 index 0000000..3c91691 Binary files /dev/null and b/images/valid/jp2/sample.jp2 differ diff --git a/images/valid/jpg/1x2-flipped-big-endian.jpg b/images/valid/jpg/1x2-flipped-big-endian.jpg new file mode 100644 index 0000000..1ba30fa Binary files /dev/null and b/images/valid/jpg/1x2-flipped-big-endian.jpg differ diff --git a/images/valid/jpg/1x2-flipped-little-endian.jpg b/images/valid/jpg/1x2-flipped-little-endian.jpg new file mode 100644 index 0000000..799d74e Binary files /dev/null and b/images/valid/jpg/1x2-flipped-little-endian.jpg differ diff --git a/images/valid/jpg/large.jpg b/images/valid/jpg/large.jpg new file mode 100644 index 0000000..40a2cdb Binary files /dev/null and b/images/valid/jpg/large.jpg differ diff --git a/images/valid/jpg/optimized.jpg b/images/valid/jpg/optimized.jpg new file mode 100644 index 0000000..a6e73d0 Binary files /dev/null and b/images/valid/jpg/optimized.jpg differ diff --git a/images/valid/jpg/progressive.jpg b/images/valid/jpg/progressive.jpg new file mode 100644 index 0000000..4a0bcc6 Binary files /dev/null and b/images/valid/jpg/progressive.jpg differ diff --git a/images/valid/jpg/sample.jpg b/images/valid/jpg/sample.jpg new file mode 100644 index 0000000..bcd7f61 Binary files /dev/null and b/images/valid/jpg/sample.jpg differ diff --git a/images/valid/jpg/sample2.jpg b/images/valid/jpg/sample2.jpg new file mode 100644 index 0000000..10cd7ce Binary files /dev/null and b/images/valid/jpg/sample2.jpg differ diff --git a/images/valid/jpg/sampleExported.jpg b/images/valid/jpg/sampleExported.jpg new file mode 100644 index 0000000..703fb51 Binary files /dev/null and b/images/valid/jpg/sampleExported.jpg differ diff --git a/images/valid/jpg/very-large.jpg b/images/valid/jpg/very-large.jpg new file mode 100644 index 0000000..c452ccb Binary files /dev/null and b/images/valid/jpg/very-large.jpg differ diff --git a/images/valid/jpx/sample.jpx b/images/valid/jpx/sample.jpx new file mode 100644 index 0000000..767eab5 Binary files /dev/null and b/images/valid/jpx/sample.jpx differ diff --git a/images/valid/ktx/sample.ktx b/images/valid/ktx/sample.ktx new file mode 100644 index 0000000..fcf7725 Binary files /dev/null and b/images/valid/ktx/sample.ktx differ diff --git a/images/valid/png/sample.png b/images/valid/png/sample.png new file mode 100644 index 0000000..2bb2f91 Binary files /dev/null and b/images/valid/png/sample.png differ diff --git a/images/valid/png/sample_apng.png b/images/valid/png/sample_apng.png new file mode 100644 index 0000000..47aad23 Binary files /dev/null and b/images/valid/png/sample_apng.png differ diff --git a/images/valid/png/sample_fried.png b/images/valid/png/sample_fried.png new file mode 100644 index 0000000..a0f10a0 Binary files /dev/null and b/images/valid/png/sample_fried.png differ diff --git a/images/valid/psd/sample.psd b/images/valid/psd/sample.psd new file mode 100644 index 0000000..82bf6b4 Binary files /dev/null and b/images/valid/psd/sample.psd differ diff --git a/images/valid/qoi/sample.qoi b/images/valid/qoi/sample.qoi new file mode 100644 index 0000000..46da52c Binary files /dev/null and b/images/valid/qoi/sample.qoi differ diff --git a/images/valid/tga/sample.tga b/images/valid/tga/sample.tga new file mode 100644 index 0000000..07113a6 Binary files /dev/null and b/images/valid/tga/sample.tga differ diff --git a/images/valid/tiff/big-endian.tiff b/images/valid/tiff/big-endian.tiff new file mode 100644 index 0000000..9fa0182 Binary files /dev/null and b/images/valid/tiff/big-endian.tiff differ diff --git a/images/valid/tiff/jpeg.tiff b/images/valid/tiff/jpeg.tiff new file mode 100644 index 0000000..02742cb Binary files /dev/null and b/images/valid/tiff/jpeg.tiff differ diff --git a/images/valid/tiff/little-endian.tiff b/images/valid/tiff/little-endian.tiff new file mode 100644 index 0000000..fe9e23a Binary files /dev/null and b/images/valid/tiff/little-endian.tiff differ diff --git a/images/valid/webp/extended.webp b/images/valid/webp/extended.webp new file mode 100644 index 0000000..3e22dd8 Binary files /dev/null and b/images/valid/webp/extended.webp differ diff --git a/images/valid/webp/lossless.webp b/images/valid/webp/lossless.webp new file mode 100644 index 0000000..457d2fe Binary files /dev/null and b/images/valid/webp/lossless.webp differ diff --git a/images/valid/webp/lossy.webp b/images/valid/webp/lossy.webp new file mode 100644 index 0000000..b1ec7ba Binary files /dev/null and b/images/valid/webp/lossy.webp differ diff --git a/include/imageinfo.hpp b/include/imageinfo.hpp new file mode 100644 index 0000000..2049de4 --- /dev/null +++ b/include/imageinfo.hpp @@ -0,0 +1,1195 @@ +// +// Created by xiaozhuai on 2021/4/1. +// https://github.com/xiaozhuai/imageinfo +// +// +// MIT License +// +// Copyright (c) 2021 xiaozhuai +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#pragma once +#ifndef IMAGEINFO_IMAGEINFO_H +#define IMAGEINFO_IMAGEINFO_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef ANDROID +#include +#endif + +#ifndef II_HEADER_CACHE_SIZE +#define II_HEADER_CACHE_SIZE (1024) +#endif + +// #define II_DISABLE_HEADER_CACHE + +static_assert(sizeof(uint8_t) == 1, "sizeof(uint8_t) != 1"); +static_assert(sizeof(int8_t) == 1, "sizeof(int8_t) != 1"); +static_assert(sizeof(uint16_t) == 2, "sizeof(uint16_t) != 2"); +static_assert(sizeof(int16_t) == 2, "sizeof(int16_t) != 2"); +static_assert(sizeof(uint32_t) == 4, "sizeof(uint32_t) != 4"); +static_assert(sizeof(int32_t) == 4, "sizeof(int32_t) != 4"); +static_assert(sizeof(uint64_t) == 8, "sizeof(uint64_t) != 8"); +static_assert(sizeof(int64_t) == 8, "sizeof(int64_t) != 8"); + +#ifdef __clang__ +#pragma clang diagnostic push +#pragma ide diagnostic ignored "OCUnusedStructInspection" +#pragma ide diagnostic ignored "OCUnusedGlobalDeclarationInspection" +#endif + +namespace imageinfo { + +enum Format { + kFormatUnknown = 0, + kFormatAvif, + kFormatBmp, + kFormatCur, + kFormatDds, + kFormatGif, + kFormatHdr, + kFormatHeic, + kFormatIcns, + kFormatIco, + kFormatJp2, + kFormatJpeg, + kFormatJpx, + kFormatKtx, + kFormatPng, + kFormatPsd, + kFormatQoi, + kFormatTga, + kFormatTiff, + kFormatWebp, +}; + +enum Error { + kNoError = 0, + kUnrecognizedFormat, +}; + +class FileReader { +public: + explicit FileReader(FILE *file) : file_(file) {} + + inline size_t size() { + if (file_ != nullptr) { + fseek(file_, 0, SEEK_END); + return ftell(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + fseek(file_, offset, SEEK_SET); + fread(buf, 1, size, file_); + } + +private: + FILE *file_ = nullptr; +}; + +class FilePathReader { +public: + explicit FilePathReader(const std::string &path) : file_(path, std::ios::in | std::ios::binary) {} + + ~FilePathReader() { + if (file_.is_open()) { + file_.close(); + } + } + + inline size_t size() { + if (file_.is_open()) { + file_.seekg(0, std::ios::end); + return (size_t)file_.tellg(); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + file_.seekg(offset, std::ios::beg); + file_.read((char *)buf, (std::streamsize)size); + } + +private: + std::ifstream file_; +}; + +class FileStreamReader { +public: + explicit FileStreamReader(std::ifstream &file) : file_(file) {} + + inline size_t size() { + if (file_.is_open()) { + file_.seekg(0, std::ios::end); + return (size_t)file_.tellg(); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + file_.seekg(offset, std::ios::beg); + file_.read((char *)buf, (std::streamsize)size); + } + +private: + std::ifstream &file_; +}; + +#ifdef ANDROID + +class AndroidAssetFileReader { +public: + explicit AndroidAssetFileReader(AAsset *file) : file_(file) {} + + inline size_t size() const { + if (file_ != nullptr) { + return AAsset_getLength(file_); + } else { + return 0; + } + } + + inline void read(void *buf, off_t offset, size_t size) { + AAsset_seek(file_, offset, SEEK_SET); + AAsset_read(file_, buf, size); + } + +private: + AAsset *file_ = nullptr; +}; + +#endif + +struct RawData { + RawData(const void *d, size_t s) : data(d), length(s) {} + + const void *data = nullptr; + size_t length = 0; +}; + +class RawDataReader { +public: + explicit RawDataReader(RawData data) : data_(data) {} + + inline size_t size() const { return data_.length; } + + inline void read(void *buf, off_t offset, size_t size) const { memcpy(buf, ((char *)data_.data) + offset, size); } + +private: + RawData data_; +}; + +class Buffer { +public: + Buffer() = default; + + explicit Buffer(size_t size) { alloc(size); } + + inline void alloc(size_t size) { + size_ = size; + data_ = std::shared_ptr(new uint8_t[size], std::default_delete()); + } + + inline const uint8_t *data() const { return data_.get(); } + + inline uint8_t *data() { return data_.get(); } + + inline size_t size() const { return size_; } + + inline uint8_t &operator[](int offset) { return data_.get()[offset]; } + + inline uint8_t operator[](int offset) const { return data_.get()[offset]; } + +public: + inline uint8_t read_u8(off_t offset) { return read_int(offset, false); } + + inline int8_t read_s8(off_t offset) { return read_int(offset, false); } + + inline uint16_t read_u16_le(off_t offset) { return read_int(offset, false); } + + inline uint16_t read_u16_be(off_t offset) { return read_int(offset, true); } + + inline int16_t read_s16_le(off_t offset) { return read_int(offset, false); } + + inline int16_t read_s16_be(off_t offset) { return read_int(offset, true); } + + inline uint32_t read_u32_le(off_t offset) { return read_int(offset, false); } + + inline uint32_t read_u32_be(off_t offset) { return read_int(offset, true); } + + inline int32_t read_s32_le(off_t offset) { return read_int(offset, false); } + + inline int32_t read_s32_be(off_t offset) { return read_int(offset, true); } + + inline uint64_t read_u64_le(off_t offset) { return read_int(offset, false); } + + inline uint64_t read_u64_be(off_t offset) { return read_int(offset, true); } + + inline int64_t read_s64_le(off_t offset) { return read_int(offset, false); } + + inline int64_t read_s64_be(off_t offset) { return read_int(offset, true); } + + template + inline T read_int(off_t offset, bool swap_endian = false) { + T val = *((T *)(data() + offset)); + return swap_endian ? swap_e(val) : val; + } + + inline std::string read_string(off_t offset, size_t size) { return std::string((char *)data() + offset, size); } + + inline std::string to_string() { return std::string((char *)data(), size()); } + + inline bool cmp(off_t offset, size_t size, const void *buf) { return memcmp(data() + offset, buf, size) == 0; } + + inline bool cmp_any_of(off_t offset, size_t size, const std::initializer_list &bufs) { + return std::any_of(bufs.begin(), bufs.end(), + [this, offset, size](const void *buf) { return memcmp(data() + offset, buf, size) == 0; }); + } + +private: + template + static T swap_e(T u) { + union { + T u; + uint8_t u8[sizeof(T)]; + } src{}, dst{}; + src.u = u; + for (size_t k = 0; k < sizeof(T); k++) { + dst.u8[k] = src.u8[sizeof(T) - k - 1]; + } + return dst.u; + } + +private: + std::shared_ptr data_ = nullptr; + size_t size_ = 0; +}; + +using ReadFunc = std::function; + +class ReadInterface { +public: + ReadInterface() = delete; + + ReadInterface(ReadFunc &read_func, size_t length) : read_func_(read_func), length_(length) { +#ifndef II_DISABLE_HEADER_CACHE + header_cache_.alloc(std::min((size_t)II_HEADER_CACHE_SIZE, length)); + read(header_cache_.data(), 0, header_cache_.size()); +#endif + } + + inline Buffer read_buffer(off_t offset, size_t size) { + assert(offset >= 0); + assert(offset + size <= length_); + Buffer buffer(size); +#ifndef II_DISABLE_HEADER_CACHE + if (offset + size <= header_cache_.size()) { + memcpy(buffer.data(), header_cache_.data() + offset, size); + } else if (offset < header_cache_.size() && header_cache_.size() - offset >= (II_HEADER_CACHE_SIZE / 4)) { + size_t head = header_cache_.size() - offset; + memcpy(buffer.data(), header_cache_.data() + offset, head); + read(buffer.data() + head, offset + (off_t)head, size - head); + } else { + read(buffer.data(), offset, size); + } +#else + read(buffer.data(), offset, size); +#endif + return buffer; + } + + inline size_t length() const { return length_; } + +private: + inline void read(void *buf, off_t offset, size_t size) { read_func_(buf, offset, size); } + +private: + ReadFunc &read_func_; + size_t length_ = 0; +#ifndef II_DISABLE_HEADER_CACHE + Buffer header_cache_; +#endif +}; + +class ImageSize { +public: + ImageSize() = default; + + ImageSize(int64_t width, int64_t height) : width(width), height(height) {} + + inline bool operator==(const ImageSize &rhs) const { return width == rhs.width && height == rhs.height; } + + inline int64_t operator[](int index) const { + assert(index >= 0 && index < 2); + return index == 0 ? width : height; + } + + int64_t width = -1; + int64_t height = -1; +}; + +using EntrySizes = std::vector; + +class ImageInfo { +public: + ImageInfo() = default; + explicit ImageInfo(Error error) : error_(error) {} + ImageInfo(Format format, const char *ext, const char *full_ext, const char *mimetype) + : format_(format), ext_(ext), full_ext_(full_ext), mimetype_(mimetype) {} + +public: + void set_size(const ImageSize &size) { size_ = size; } + + void set_size(int64_t width, int64_t height) { size_ = ImageSize(width, height); } + + void set_entry_sizes(const EntrySizes &entry_sizes) { entry_sizes_ = entry_sizes; } + + void add_entry_size(const ImageSize &size) { entry_sizes_.emplace_back(size); } + + void add_entry_size(int64_t width, int64_t height) { entry_sizes_.emplace_back(width, height); } + +public: + inline explicit operator bool() const { return error_ == kNoError; } + + inline bool ok() const { return error_ == kNoError; } + + inline Error error() const { return error_; } + + inline const char *error_msg() const { + switch (error_) { + case kNoError: + return "No error"; + case kUnrecognizedFormat: + return "Unrecognized format"; + default: + return "Unknown error"; + } + } + + inline Format format() const { return format_; } + + inline const char *ext() const { return ext_; } + + inline const char *full_ext() const { return full_ext_; } + + inline const char *mimetype() const { return mimetype_; } + + inline const ImageSize &size() const { return size_; } + + inline const EntrySizes &entry_sizes() const { return entry_sizes_; } + +private: + Format format_ = kFormatUnknown; + const char *ext_ = ""; + const char *full_ext_ = ""; + const char *mimetype_ = ""; + ImageSize size_; + EntrySizes entry_sizes_; + Error error_ = kNoError; +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://nokiatech.github.io/heif/technical.html +// https://www.jianshu.com/p/b016d10a087d +bool try_avif_heic(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 4) { + return false; + } + auto buffer = ri.read_buffer(0, 4); + uint32_t ftyp_box_length = buffer.read_u32_be(0); + if (length < ftyp_box_length + 12) { + return false; + } + buffer = ri.read_buffer(0, ftyp_box_length + 12); + if (!buffer.cmp(4, 4, "ftyp")) { + return false; + } + + /** + * Major Brand + * + * AVIF: "avif", "avis" + * HEIF: "mif1", "msf1" + * HEIC: "heic", "heix", "hevc", "hevx" + * + */ + if (!buffer.cmp_any_of(8, 4, {"avif", "avis", "mif1", "msf1", "heic", "heix", "hevc", "hevx"})) { + return false; + } + + uint32_t compatible_brand_size = (ftyp_box_length - 16) / 4; + std::unordered_set compatible_brands; + for (uint32_t i = 0; i < compatible_brand_size; ++i) { + compatible_brands.insert(buffer.read_string(16 + i * 4, 4)); + } + + bool is_avif; + if (compatible_brands.find("avif") != compatible_brands.end() || buffer.cmp(8, 4, "avif")) { + is_avif = true; + } else if (compatible_brands.find("heic") != compatible_brands.end() || buffer.cmp(8, 4, "heic")) { + is_avif = false; + } else { + return false; + } + + if (!buffer.cmp(ftyp_box_length + 4, 4, "meta")) { + return false; + } + + uint32_t meta_length = buffer.read_u32_be(ftyp_box_length); + + if (length < ftyp_box_length + 12 + meta_length) { + return false; + } + + buffer = ri.read_buffer(ftyp_box_length + 12, meta_length); + + off_t offset = 0; + off_t end = meta_length; + + /** + * find ispe box + * + * meta + * - ... + * - iprp + * - ... + * - ipco + * - ... + * - ispe + */ + while (offset < end) { + uint32_t box_size = buffer.read_u32_be(offset); + if (buffer.cmp_any_of(offset + 4, 4, {"iprp", "ipco"})) { + end = offset + box_size; + offset += 8; + } else if (buffer.cmp(offset + 4, 4, "ispe")) { + if (is_avif) { + info = ImageInfo(kFormatAvif, "avif", "avif", "image/avif"); + } else { + info = ImageInfo(kFormatHeic, "heic", "heic", "image/heic"); + } + info.set_size( // + buffer.read_u32_be(offset + 12), // + buffer.read_u32_be(offset + 16) // + ); + return true; + } else { + offset += box_size; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.fileformat.info/format/bmp/corion.htm +bool try_bmp(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 26) { + return false; + } + auto buffer = ri.read_buffer(0, 26); + if (!buffer.cmp(0, 2, "BM")) { + return false; + } + + info = ImageInfo(kFormatBmp, "bmp", "bmp", "image/bmp"); + // bmp height can be negative, it means flip Y + info.set_size( // + buffer.read_s32_le(18), // + std::abs(buffer.read_s32_le(22)) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool try_cur_ico(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 6) { + return false; + } + auto buffer = ri.read_buffer(0, 6); + + bool is_cur; + if (buffer.cmp(0, 4, "\x00\x00\x02\x00")) { + is_cur = true; + } else if (buffer.cmp(0, 4, "\x00\x00\x01\x00")) { + is_cur = false; + } else { + return false; + } + + uint16_t entry_count = buffer.read_u16_le(4); + if (entry_count == 0) { + return false; + } + const int entry_size = 16; + off_t entry_total_size = entry_count * entry_size; + + off_t offset = 6; + if (length < offset + entry_total_size) { + return false; + } + buffer = ri.read_buffer(offset, entry_total_size); + offset += entry_total_size; + + EntrySizes sizes; + + for (int i = 0; i < entry_count; ++i) { + uint8_t w1 = buffer.read_u8(i * entry_size); + uint8_t h1 = buffer.read_u8(i * entry_size + 1); + int64_t w2 = w1 == 0 ? 256 : w1; + int64_t h2 = h1 == 0 ? 256 : h1; + sizes.emplace_back(w2, h2); + + uint32_t bytes = buffer.read_s32_le(i * entry_size + 8); + offset += bytes; + } + + if (length < (size_t)offset) { + return false; + } + + if (is_cur) { + info = ImageInfo(kFormatCur, "cur", "cur", "image/cur"); + } else { + info = ImageInfo(kFormatIco, "ico", "ico", "image/ico"); + } + info.set_entry_sizes(sizes); + info.set_size(sizes.front()); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool try_dds(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 20) { + return false; + } + auto buffer = ri.read_buffer(0, 20); + if (!buffer.cmp(0, 4, "DDS ")) { + return false; + } + + info = ImageInfo(kFormatDds, "dds", "dds", "image/dds"); + info.set_size( // + buffer.read_u32_le(16), // + buffer.read_u32_le(12) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.fileformat.info/format/gif/corion.htm +bool try_gif(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 10) { + return false; + } + auto buffer = ri.read_buffer(0, 10); + if (!buffer.cmp_any_of(0, 6, {"GIF87a", "GIF89a"})) { + return false; + } + + info = ImageInfo(kFormatGif, "gif", "gif", "image/gif"); + info.set_size( // + buffer.read_u16_le(6), // + buffer.read_u16_le(8) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// http://paulbourke.net/dataformats/pic/ +bool try_hdr(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 6) { + return false; + } + + auto buffer = ri.read_buffer(0, 6); + auto buffer2 = ri.read_buffer(0, 10); + if (!buffer.cmp_any_of(0, 6, {"#?RGBE", "#?XYZE"}) && !buffer2.cmp(0, 10, "#?RADIANCE")) { + return false; + } + + int read = 6; + const size_t piece = 64; + std::string header; + static const std::regex x_pattern(R"(\s[+-]X\s(\d+)\s)"); + static const std::regex y_pattern(R"(\s[+-]Y\s(\d+)\s)"); + while (read < length) { + header += ri.read_buffer(read, std::min(length - read, piece)).to_string(); + read += piece; + std::smatch x_results; + std::smatch y_results; + std::regex_search(header, x_results, x_pattern); + std::regex_search(header, y_results, y_pattern); + if (x_results.size() >= 2 && y_results.size() >= 2) { + info = ImageInfo(kFormatHdr, "hdr", "hdr", "image/vnd.radiance"); + info.set_size( // + std::stol(x_results.str(1)), // + std::stol(y_results.str(1)) // + ); + return true; + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool try_icns(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 8) { + return false; + } + auto buffer = ri.read_buffer(0, 8); + uint32_t file_length = buffer.read_u32_be(4); + if (!buffer.cmp(0, 4, "icns") || file_length != length) { + return false; + } + + static const std::unordered_map size_map = { + {"ICON", 32}, + {"ICN#", 32}, + {"icm#", 16}, + {"icm4", 16}, + {"icm8", 16}, + {"ics#", 16}, + {"ics4", 16}, + {"ics8", 16}, + {"is32", 16}, + {"s8mk", 16}, + {"icl4", 32}, + {"icl8", 32}, + {"il32", 32}, + {"l8mk", 32}, + {"ich#", 48}, + {"ich4", 48}, + {"ich8", 48}, + {"ih32", 48}, + {"h8mk", 48}, + {"it32", 128}, + {"t8mk", 128}, + {"icp4", 16}, + {"icp5", 32}, + {"icp6", 64}, + {"ic07", 128}, + {"ic08", 256}, + {"ic09", 512}, + {"ic10", 1024}, + {"ic11", 32}, + {"ic12", 64}, + {"ic13", 256}, + {"ic14", 512}, + {"ic04", 16}, + {"ic05", 32}, + {"icsB", 36}, + {"icsb", 18}, + }; + + int64_t max_size = 0; + EntrySizes entry_sizes; + + off_t offset = 8; + while (offset + 8 <= length) { + buffer = ri.read_buffer(offset, 8); + auto type = buffer.read_string(0, 4); + uint32_t entry_size = buffer.read_u32_be(4); + int64_t s = size_map.at(type); + entry_sizes.emplace_back(s, s); + max_size = std::max(max_size, s); + offset += entry_size; + } + + info = ImageInfo(kFormatIcns, "icns", "icns", "image/icns"); + info.set_size(max_size, max_size); + info.set_entry_sizes(entry_sizes); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://docs.fileformat.com/image/jp2/ +// https://docs.fileformat.com/image/jpx/ +bool try_jp2_jpx(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 8) { + return false; + } + auto buffer = ri.read_buffer(0, 8); + + if (!buffer.cmp(4, 4, "jP ")) { + return false; + } + + uint32_t signature_length = buffer.read_u32_be(0); + off_t offset = signature_length; + + if (length < offset + 12) { + return false; + } + + buffer = ri.read_buffer(offset, 12); + if (!buffer.cmp(4, 4, "ftyp")) { + return false; + } + + bool is_jp2; + if (buffer.cmp(8, 4, "jp2 ")) { + is_jp2 = true; + } else if (buffer.cmp(8, 4, "jpx ")) { + is_jp2 = false; + } else { + return false; + } + + uint32_t ftyp_length = buffer.read_u32_be(0); + offset += ftyp_length; + + while (offset + 24 <= length) { + buffer = ri.read_buffer(offset, 24); + if (buffer.cmp(4, 4, "jp2h")) { + if (buffer.cmp(12, 4, "ihdr")) { + if (is_jp2) { + info = ImageInfo(kFormatJp2, "jp2", "jp2", "image/jp2"); + } else { + info = ImageInfo(kFormatJpx, "jpx", "jpx", "image/jpx"); + } + info.set_size( // + buffer.read_u32_be(20), // + buffer.read_u32_be(16) // + ); + return true; + } else { + return false; + } + } + uint32_t box_length = buffer.read_u32_be(0); + offset += box_length; + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.fileformat.info/format/jpeg/corion.htm +bool try_jpg(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 2) { + return false; + } + auto buffer = ri.read_buffer(0, 2); + if (!buffer.cmp(0, 2, "\xFF\xD8")) { + return false; + } + + off_t offset = 2; + while (offset + 9 <= length) { + buffer = ri.read_buffer(offset, 9); + uint16_t section_size = buffer.read_u16_be(2); + if (!buffer.cmp(0, 1, "\xFF")) { + // skip garbage bytes + offset += 1; + continue; + } + + // 0xFFC0 is baseline standard (SOF0) + // 0xFFC1 is baseline optimized (SOF1) + // 0xFFC2 is progressive (SOF2) + if (buffer.cmp_any_of(0, 2, {"\xFF\xC0", "\xFF\xC1", "\xFF\xC2"})) { + info = ImageInfo(kFormatJpeg, "jpg", "jpeg", "image/jpeg"); + info.set_size( // + buffer.read_u16_be(7), // + buffer.read_u16_be(5) // + ); + return true; + } + offset += section_size + 2; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.khronos.org/registry/KTX/specs/1.0/ktxspec_v1.html +bool try_ktx(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 44) { + return false; + } + auto buffer = ri.read_buffer(0, 44); + if (!buffer.cmp(0, 12, "\xABKTX 11\xBB\r\n\x1A\n")) { + return false; + } + + info = ImageInfo(kFormatKtx, "ktx", "ktx", "image/ktx"); + info.set_size( // + buffer.read_u32_le(36), // + buffer.read_u32_le(40) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.fileformat.info/format/png/corion.htm +bool try_png(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 4) { + return false; + } + + auto buffer = ri.read_buffer(0, std::min(length, 40)); + if (!buffer.cmp(0, 4, "\x89PNG")) { + return false; + } + + std::string first_chunk_type = buffer.read_string(12, 4); + if (first_chunk_type == "IHDR" && buffer.size() >= 24) { + info = ImageInfo(kFormatPng, "png", "png", "image/png"); + info.set_size( // + buffer.read_u32_be(16), // + buffer.read_u32_be(20) // + ); + return true; + } else if (first_chunk_type == "CgBI") { + if (buffer.read_string(28, 4) == "IHDR" && buffer.size() >= 40) { + info = ImageInfo(kFormatPng, "png", "png", "image/png"); + info.set_size( // + buffer.read_u32_be(32), // + buffer.read_u32_be(36) // + ); + return true; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool try_psd(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 22) { + return false; + } + auto buffer = ri.read_buffer(0, 22); + if (!buffer.cmp(0, 6, "8BPS\x00\x01")) { + return false; + } + + info = ImageInfo(kFormatPsd, "psd", "psd", "image/psd"); + info.set_size( // + buffer.read_u32_be(18), // + buffer.read_u32_be(14) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +bool try_qoi(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 12) { + return false; + } + auto buffer = ri.read_buffer(0, 12); + if (!buffer.cmp(0, 4, "qoif")) { + return false; + } + + info = ImageInfo(kFormatQoi, "qoi", "qoi", "image/qoi"); + info.set_size( // + buffer.read_u32_be(4), // + buffer.read_u32_be(8) // + ); + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://www.fileformat.info/format/tiff/corion.htm +bool try_tiff(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 8) { + return false; + } + auto buffer = ri.read_buffer(0, 8); + if (!buffer.cmp_any_of(0, 4, {"\x49\x49\x2A\x00", "\x4D\x4D\x00\x2A"})) { + return false; + } + + bool swap_endian = buffer[0] == 0x4D; + + auto offset = buffer.read_int(4, swap_endian); + if (length < offset + 2) { + return false; + } + + buffer = ri.read_buffer(offset, 2); + + auto num_entry = buffer.read_int(0, swap_endian); + offset += 2; + + int64_t width = -1; + int64_t height = -1; + for (uint16_t i = 0; i < num_entry && length >= offset + 12 && (width == -1 || height == -1); ++i, offset += 12) { + buffer = ri.read_buffer(offset, 12); + + auto tag = buffer.read_int(0, swap_endian); + auto type = buffer.read_int(2, swap_endian); + + if (tag == 256) { // Found ImageWidth entry + if (type == 3) { + width = buffer.read_int(8, swap_endian); + } else if (type == 4) { + width = buffer.read_int(8, swap_endian); + } + } else if (tag == 257) { // Found ImageHeight entry + if (type == 3) { + height = buffer.read_int(8, swap_endian); + } else if (type == 4) { + height = buffer.read_int(8, swap_endian); + } + } + } + + bool ok = width != -1 && height != -1; + if (ok) { + info = ImageInfo(kFormatTiff, "tiff", "tiff", "image/tiff"); + info.set_size(width, height); + } + return ok; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// https://developers.google.com/speed/webp/docs/riff_container +bool try_webp(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 16) { + return false; + } + auto buffer = ri.read_buffer(0, std::min(length, 30)); + if (!buffer.cmp(0, 4, "RIFF") || !buffer.cmp(8, 4, "WEBP")) { + return false; + } + + std::string type = buffer.read_string(12, 4); + if (type == "VP8 " && buffer.size() >= 30) { + info = ImageInfo(kFormatWebp, "webp", "webp", "image/webp"); + info.set_size( // + buffer.read_u16_le(26) & 0x3FFF, // + buffer.read_u16_le(28) & 0x3FFF // + ); + return true; + } else if (type == "VP8L" && buffer.size() >= 25) { + uint32_t n = buffer.read_u32_le(21); + info = ImageInfo(kFormatWebp, "webp", "webp", "image/webp"); + info.set_size( // + (n & 0x3FFF) + 1, // + ((n >> 14) & 0x3FFF) + 1 // + ); + return true; + } else if (type == "VP8X" && buffer.size() >= 30) { + uint8_t extended_header = buffer.read_u8(20); + bool valid_start = (extended_header & 0xc0) == 0; + bool valid_end = (extended_header & 0x01) == 0; + if (valid_start && valid_end) { + info = ImageInfo(kFormatWebp, "webp", "webp", "image/webp"); + info.set_size( // + (buffer.read_u32_le(24) & 0x00FFFFFF) + 1, // + ((buffer.read_u32_le(26) & 0xFFFFFF00) >> 8) + 1 // + ); + return true; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// TODO Not rigorous enough, keep it as last detector +// https://www.fileformat.info/format/tga/corion.htm +bool try_tga(ReadInterface &ri, size_t length, ImageInfo &info) { + if (length < 18) { + return false; + } + + auto buffer = ri.read_buffer((off_t)(length - 18), 18); + + if (buffer.cmp(0, 18, "TRUEVISION-XFILE.\x00")) { + if (length < 18 + 16) { + return false; + } + buffer = ri.read_buffer(0, 18); + info = ImageInfo(kFormatTga, "tga", "tga", "image/tga"); + info.set_size( // + buffer.read_u16_le(12), // + buffer.read_u16_le(14) // + ); + return true; + } + + buffer = ri.read_buffer(0, 18); + + uint8_t id_len = buffer.read_u8(0); + if (length < (size_t)id_len + 18) { + return false; + } + + uint8_t color_map_type = buffer.read_u8(1); + uint8_t image_type = buffer.read_u8(2); + uint16_t first_color_map_entry_index = buffer.read_u16_le(3); + uint16_t color_map_length = buffer.read_u16_le(5); + uint8_t color_map_entry_size = buffer.read_u8(7); + // uint16_t x_origin = buffer.read_u16_le(8); + // uint16_t y_origin = buffer.read_u16_le(10); + uint16_t w = buffer.read_u16_le(12); + uint16_t h = buffer.read_u16_le(14); + // uint8_t pixel_depth = buffer.read_u8(16); + // uint8_t flags = buffer.read_u8(17); + + if (color_map_type == 0) { // no color map + if (image_type == 0 || image_type == 2 || image_type == 3 || image_type == 10 || image_type == 11 || + image_type == 32 || image_type == 33) { + if (first_color_map_entry_index == 0 && color_map_length == 0 && color_map_entry_size == 0) { + info = ImageInfo(kFormatTga, "tga", "tga", "image/tga"); + info.set_size(w, h); + return true; + } + } + } else if (color_map_type == 1) { // 256 entry palette + if (image_type == 1 || image_type == 9) { + info = ImageInfo(kFormatTga, "tga", "tga", "image/tga"); + info.set_size(w, h); + return true; + } + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +using Detector = bool (*)(ReadInterface &ri, size_t length, ImageInfo &info); + +inline ImageInfo parse(ReadInterface &ri, // + const std::vector &likely_formats = {}, // + bool must_be_one_of_likely_formats = false) { // + static std::vector> dl = { + {kFormatAvif, try_avif_heic}, + {kFormatHeic, try_avif_heic}, + { kFormatBmp, try_bmp}, + { kFormatCur, try_cur_ico}, + { kFormatIco, try_cur_ico}, + { kFormatDds, try_dds}, + { kFormatGif, try_gif}, + { kFormatHdr, try_hdr}, + {kFormatIcns, try_icns}, + { kFormatJp2, try_jp2_jpx}, + { kFormatJpx, try_jp2_jpx}, + {kFormatJpeg, try_jpg}, + { kFormatKtx, try_ktx}, + { kFormatPng, try_png}, + { kFormatPsd, try_psd}, + { kFormatQoi, try_qoi}, + {kFormatTiff, try_tiff}, + {kFormatWebp, try_webp}, + { kFormatTga, try_tga}, + }; + + std::unordered_map dm; + dm.reserve(dl.size()); + for (auto &d : dl) { + dm[std::get<0>(d)] = std::get<1>(d); + } + + ImageInfo info; + size_t length = ri.length(); + + bool has_likely_formats = !likely_formats.empty(); + std::unordered_set likely_formats_set; + likely_formats_set.reserve(likely_formats.size()); + for (auto format : likely_formats) { + likely_formats_set.insert(format); + } + + if (has_likely_formats) { + for (auto format : likely_formats) { + auto &detector = dm[format]; + if (detector(ri, length, info)) { + return info; + } + } + if (must_be_one_of_likely_formats) { + return ImageInfo(kUnrecognizedFormat); + } + } + + for (auto &d : dl) { + auto &format = std::get<0>(d); + auto &detector = std::get<1>(d); + if (has_likely_formats && likely_formats_set.find(format) != likely_formats_set.end()) { + continue; + } + if (detector(ri, length, info)) { + return info; + } + } + + return ImageInfo(kUnrecognizedFormat); +} + +template +inline ImageInfo parse(InputType &input, // + const std::vector &likely_formats = {}, // + bool must_be_one_of_likely_formats = false) { // + ReaderType reader(input); + size_t length = reader.size(); + ReadFunc read_func = [&reader](void *buf, off_t offset, size_t size) { reader.read(buf, offset, size); }; + ReadInterface ri(read_func, length); + return parse(ri, likely_formats, must_be_one_of_likely_formats); +} + +}; // namespace imageinfo + +#ifdef __clang__ +#pragma clang diagnostic pop +#endif + +#endif // IMAGEINFO_IMAGEINFO_H diff --git a/tests/tests.cpp b/tests/tests.cpp new file mode 100644 index 0000000..bf0c74f --- /dev/null +++ b/tests/tests.cpp @@ -0,0 +1,119 @@ +// +// Created by xiaozhuai on 2021/4/1. +// + +#include + +#include "imageinfo.hpp" + +#define ASSET_II(file, e, f, w, h) \ + do { \ + auto info = imageinfo::parse(file); \ + if (info.error() != (e)) { \ + fprintf(stderr, "Error ASSET_II, file: %s, line: %d, error != %s, %s\n", file, __LINE__, #e, \ + info.error_msg()); \ + abort(); \ + } else if (info.format() != (f)) { \ + fprintf(stderr, "Error ASSET_II, file: %s, line: %d, format != %s\n", file, __LINE__, #f); \ + abort(); \ + } else if (info.size().width != (w)) { \ + fprintf(stderr, "Error ASSET_II, file: %s, line: %d, width != %ld\n", file, __LINE__, (w)); \ + abort(); \ + } else if (info.size().height != (h)) { \ + fprintf(stderr, "Error ASSET_II, file: %s, line: %d, height != %ld\n", file, __LINE__, (h)); \ + abort(); \ + } else { \ + printf("Test passed, file: %s \n", file); \ + } \ + } while (0) + +int main() { + using namespace imageinfo; + + { + ASSET_II(IMAGES_DIR "valid/avif/sample.avif", kNoError, kFormatAvif, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/avif/sample2.avif", kNoError, kFormatAvif, 800l, 533l); + ASSET_II(IMAGES_DIR "valid/avif/sample3.avif", kNoError, kFormatAvif, 1280l, 720l); + } + + { + ASSET_II(IMAGES_DIR "valid/heic/sample.heic", kNoError, kFormatHeic, 122l, 456l); + ASSET_II(IMAGES_DIR "valid/heic/sample2.heic", kNoError, kFormatHeic, 1440l, 960l); + ASSET_II(IMAGES_DIR "valid/heic/sample3.heic", kNoError, kFormatHeic, 1280l, 854l); + } + + { + ASSET_II(IMAGES_DIR "valid/bmp/sample.bmp", kNoError, kFormatBmp, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/bmp/sample2.bmp", kNoError, kFormatBmp, 123l, 456l); + } + + { ASSET_II(IMAGES_DIR "valid/cur/sample.cur", kNoError, kFormatCur, 32l, 32l); } + + { + ASSET_II(IMAGES_DIR "valid/ico/multi-size.ico", kNoError, kFormatIco, 256l, 256l); + ASSET_II(IMAGES_DIR "valid/ico/multi-size-compressed.ico", kNoError, kFormatIco, 256l, 256l); + ASSET_II(IMAGES_DIR "valid/ico/sample.ico", kNoError, kFormatIco, 32l, 32l); + ASSET_II(IMAGES_DIR "valid/ico/sample-256.ico", kNoError, kFormatIco, 256l, 256l); + ASSET_II(IMAGES_DIR "valid/ico/sample-256-compressed.ico", kNoError, kFormatIco, 256l, 256l); + ASSET_II(IMAGES_DIR "valid/ico/sample-compressed.ico", kNoError, kFormatIco, 32l, 32l); + } + + { ASSET_II(IMAGES_DIR "valid/dds/sample.dds", kNoError, kFormatDds, 123l, 456l); } + + { ASSET_II(IMAGES_DIR "valid/gif/sample.gif", kNoError, kFormatGif, 123l, 456l); } + + { + ASSET_II(IMAGES_DIR "valid/hdr/sample.hdr", kNoError, kFormatHdr, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/hdr/sample2.hdr", kNoError, kFormatHdr, 1024l, 512l); + } + + { ASSET_II(IMAGES_DIR "valid/icns/sample.icns", kNoError, kFormatIcns, 128l, 128l); } + + { + ASSET_II(IMAGES_DIR "valid/jp2/sample.jp2", kNoError, kFormatJp2, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/jp2/jpx_disguised_as_jp2.jp2", kNoError, kFormatJp2, 2717l, 3701l); + } + + { ASSET_II(IMAGES_DIR "valid/jpx/sample.jpx", kNoError, kFormatJpx, 2717l, 3701l); } + + { + ASSET_II(IMAGES_DIR "valid/jpg/1x2-flipped-big-endian.jpg", kNoError, kFormatJpeg, 1l, 2l); + ASSET_II(IMAGES_DIR "valid/jpg/1x2-flipped-little-endian.jpg", kNoError, kFormatJpeg, 1l, 2l); + ASSET_II(IMAGES_DIR "valid/jpg/large.jpg", kNoError, kFormatJpeg, 1600l, 1200l); + ASSET_II(IMAGES_DIR "valid/jpg/optimized.jpg", kNoError, kFormatJpeg, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/jpg/progressive.jpg", kNoError, kFormatJpeg, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/jpg/sample.jpg", kNoError, kFormatJpeg, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/jpg/sample2.jpg", kNoError, kFormatJpeg, 1200l, 1603l); + ASSET_II(IMAGES_DIR "valid/jpg/sampleExported.jpg", kNoError, kFormatJpeg, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/jpg/very-large.jpg", kNoError, kFormatJpeg, 4800l, 3600l); + } + + { ASSET_II(IMAGES_DIR "valid/ktx/sample.ktx", kNoError, kFormatKtx, 123l, 456l); } + + { + ASSET_II(IMAGES_DIR "valid/png/sample.png", kNoError, kFormatPng, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/png/sample_fried.png", kNoError, kFormatPng, 128l, 68l); + ASSET_II(IMAGES_DIR "valid/png/sample_apng.png", kNoError, kFormatPng, 480l, 400l); + ASSET_II(IMAGES_DIR "invalid/sample.png", kUnrecognizedFormat, kFormatUnknown, -1l, -1l); + } + + { ASSET_II(IMAGES_DIR "valid/psd/sample.psd", kNoError, kFormatPsd, 123l, 456l); } + + { ASSET_II(IMAGES_DIR "valid/qoi/sample.qoi", kNoError, kFormatQoi, 123l, 456l); } + + { + ASSET_II(IMAGES_DIR "valid/tiff/big-endian.tiff", kNoError, kFormatTiff, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/tiff/jpeg.tiff", kNoError, kFormatTiff, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/tiff/little-endian.tiff", kNoError, kFormatTiff, 123l, 456l); + } + + { + ASSET_II(IMAGES_DIR "valid/webp/lossless.webp", kNoError, kFormatWebp, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/webp/extended.webp", kNoError, kFormatWebp, 123l, 456l); + ASSET_II(IMAGES_DIR "valid/webp/lossy.webp", kNoError, kFormatWebp, 123l, 456l); + } + + { ASSET_II(IMAGES_DIR "valid/tga/sample.tga", kNoError, kFormatTga, 123l, 456l); } + + return 0; +}