diff options
Diffstat (limited to 'third_party/jpeg-xl/experimental/fast_lossless')
7 files changed, 493 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/experimental/fast_lossless/.gitignore b/third_party/jpeg-xl/experimental/fast_lossless/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/third_party/jpeg-xl/experimental/fast_lossless/README.md b/third_party/jpeg-xl/experimental/fast_lossless/README.md new file mode 100644 index 0000000000..5f99c133d8 --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/README.md @@ -0,0 +1,10 @@ +# Fast-lossless +This is a script to compile a standalone version of a JXL encoder that supports +lossless compression, up to 16 bits, of 1- to 4-channel images and animations; it is +very fast and compression is slightly worse than PNG for 8-bit nonphoto content +and better or much better than PNG for all other situations. + +The main encoder is made out of two files, `lib/jxl/enc_fast_lossless.{cc,h}`; +it automatically selects and runs a SIMD implementation supported by your CPU. + +This folder contains an example build script and `main` file. diff --git a/third_party/jpeg-xl/experimental/fast_lossless/build-android.sh b/third_party/jpeg-xl/experimental/fast_lossless/build-android.sh new file mode 100755 index 0000000000..c155b2169a --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/build-android.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +set -e + +DIR=$(realpath "$(dirname "$0")") + +mkdir -p /tmp/build-android +cd /tmp/build-android + +CXX="$ANDROID_NDK"/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android30-clang++ +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: Android C++ compiler not found, is ANDROID_NDK set properly?\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 \ + -I. lodepng.o \ + -I"${DIR}"/../../ \ + "${DIR}"/../../lib/jxl/enc_fast_lossless.cc "${DIR}"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/third_party/jpeg-xl/experimental/fast_lossless/build.sh b/third_party/jpeg-xl/experimental/fast_lossless/build.sh new file mode 100755 index 0000000000..e2c0aa3fd0 --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/build.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +set -e + +DIR=$(realpath "$(dirname "$0")") +mkdir -p "$DIR"/build +cd "$DIR"/build + +# set CXX to clang++ if not set in the environment +CXX="${CXX-clang++}" +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: C++ compiler not found\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 \ + -I. -g lodepng.o \ + -I"$DIR"/../../ \ + "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/third_party/jpeg-xl/experimental/fast_lossless/cross_compile_aarch64.sh b/third_party/jpeg-xl/experimental/fast_lossless/cross_compile_aarch64.sh new file mode 100755 index 0000000000..a5e6aa2a52 --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/cross_compile_aarch64.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Copyright (c) the JPEG XL Project Authors. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file. +set -e + +DIR=$(realpath "$(dirname "$0")") +mkdir -p "$DIR"/build-aarch64 +cd "$DIR"/build-aarch64 + +CXX="${CXX-aarch64-linux-gnu-c++}" +if ! command -v "$CXX" >/dev/null ; then + printf >&2 '%s: C++ compiler not found\n' "${0##*/}" + exit 1 +fi + +[ -f lodepng.cpp ] || curl -o lodepng.cpp --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.cpp' +[ -f lodepng.h ] || curl -o lodepng.h --url 'https://raw.githubusercontent.com/lvandeve/lodepng/8c6a9e30576f07bf470ad6f09458a2dcd7a6a84a/lodepng.h' +[ -f lodepng.o ] || "$CXX" lodepng.cpp -O3 -o lodepng.o -c + +"$CXX" -O3 -static \ + -I. lodepng.o \ + -I"$DIR"/../../ \ + "$DIR"/../../lib/jxl/enc_fast_lossless.cc "$DIR"/fast_lossless_main.cc \ + -o fast_lossless diff --git a/third_party/jpeg-xl/experimental/fast_lossless/fast_lossless_main.cc b/third_party/jpeg-xl/experimental/fast_lossless/fast_lossless_main.cc new file mode 100644 index 0000000000..b59051d4e2 --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/fast_lossless_main.cc @@ -0,0 +1,113 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <atomic> +#include <chrono> +#include <thread> +#include <vector> + +#include "lib/jxl/enc_fast_lossless.h" +#include "lodepng.h" +#include "pam-input.h" + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, + "Usage: %s in.png out.jxl [effort] [num_reps] [num_threads]\n", + argv[0]); + return 1; + } + + const char* in = argv[1]; + const char* out = argv[2]; + int effort = argc >= 4 ? atoi(argv[3]) : 2; + size_t num_reps = argc >= 5 ? atoi(argv[4]) : 1; + size_t num_threads = argc >= 6 ? atoi(argv[5]) : 0; + + if (effort < 0 || effort > 127) { + fprintf( + stderr, + "Effort should be between 0 and 127 (default is 2, more is slower)\n"); + return 1; + } + + unsigned char* png; + unsigned w, h; + size_t nb_chans = 4, bitdepth = 8; + + unsigned error = lodepng_decode32_file(&png, &w, &h, in); + + size_t width = w, height = h; + if (error && !DecodePAM(in, &png, &width, &height, &nb_chans, &bitdepth)) { + fprintf(stderr, "lodepng error %u: %s\n", error, lodepng_error_text(error)); + return 1; + } + + auto parallel_runner = [](void* num_threads_ptr, void* opaque, + void fun(void*, size_t), size_t count) { + size_t num_threads = *(size_t*)num_threads_ptr; + if (num_threads == 0) { + num_threads = std::thread::hardware_concurrency(); + } + if (num_threads > count) { + num_threads = count; + } + if (num_threads == 1) { + for (size_t i = 0; i < count; i++) { + fun(opaque, i); + } + } else { + std::atomic<int> task{0}; + std::vector<std::thread> threads; + for (size_t i = 0; i < num_threads; i++) { + threads.push_back(std::thread([count, opaque, fun, &task]() { + while (true) { + int t = task++; + if (t >= count) break; + fun(opaque, t); + } + })); + } + for (auto& t : threads) t.join(); + } + }; + + size_t encoded_size = 0; + unsigned char* encoded = nullptr; + size_t stride = width * nb_chans * (bitdepth > 8 ? 2 : 1); + + auto start = std::chrono::high_resolution_clock::now(); + for (size_t _ = 0; _ < num_reps; _++) { + free(encoded); + encoded_size = JxlFastLosslessEncode( + png, width, stride, height, nb_chans, bitdepth, + /*big_endian=*/true, effort, &encoded, &num_threads, +parallel_runner); + } + auto stop = std::chrono::high_resolution_clock::now(); + if (num_reps > 1) { + float us = + std::chrono::duration_cast<std::chrono::microseconds>(stop - start) + .count(); + size_t pixels = size_t{width} * size_t{height} * num_reps; + float mps = pixels / us; + fprintf(stderr, "%10.3f MP/s\n", mps); + fprintf(stderr, "%10.3f bits/pixel\n", + encoded_size * 8.0 / float(width) / float(height)); + } + + FILE* o = fopen(out, "wb"); + if (!o) { + fprintf(stderr, "error opening %s: %s\n", out, strerror(errno)); + return 1; + } + if (fwrite(encoded, 1, encoded_size, o) != encoded_size) { + fprintf(stderr, "error writing to %s: %s\n", out, strerror(errno)); + } + fclose(o); +} diff --git a/third_party/jpeg-xl/experimental/fast_lossless/pam-input.h b/third_party/jpeg-xl/experimental/fast_lossless/pam-input.h new file mode 100644 index 0000000000..4ecbe6b72d --- /dev/null +++ b/third_party/jpeg-xl/experimental/fast_lossless/pam-input.h @@ -0,0 +1,289 @@ +// Copyright (c) the JPEG XL Project Authors. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include <limits.h> +#include <stdlib.h> +#include <string.h> + +bool error_msg(const char* message) { + fprintf(stderr, "%s\n", message); + return false; +} +#define return_on_error(X) \ + if (!X) return false; + +size_t Log2(uint32_t value) { return 31 - __builtin_clz(value); } + +struct HeaderPNM { + size_t xsize; + size_t ysize; + bool is_gray; // PGM + bool has_alpha; // PAM + size_t bits_per_sample; +}; + +class Parser { + public: + explicit Parser(uint8_t* data, size_t length) + : pos_(data), end_(data + length) {} + + // Sets "pos" to the first non-header byte/pixel on success. + bool ParseHeader(HeaderPNM* header, const uint8_t** pos) { + // codec.cc ensures we have at least two bytes => no range check here. + if (pos_[0] != 'P') return false; + const uint8_t type = pos_[1]; + pos_ += 2; + + switch (type) { + case '5': + header->is_gray = true; + return ParseHeaderPNM(header, pos); + + case '6': + header->is_gray = false; + return ParseHeaderPNM(header, pos); + + case '7': + return ParseHeaderPAM(header, pos); + } + return false; + } + + // Exposed for testing + bool ParseUnsigned(size_t* number) { + if (pos_ == end_) return error_msg("PNM: reached end before number"); + if (!IsDigit(*pos_)) return error_msg("PNM: expected unsigned number"); + + *number = 0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + return true; + } + + bool ParseSigned(double* number) { + if (pos_ == end_) return error_msg("PNM: reached end before signed"); + + if (*pos_ != '-' && *pos_ != '+' && !IsDigit(*pos_)) { + return error_msg("PNM: expected signed number"); + } + + // Skip sign + const bool is_neg = *pos_ == '-'; + if (is_neg || *pos_ == '+') { + ++pos_; + if (pos_ == end_) return error_msg("PNM: reached end before digits"); + } + + // Leading digits + *number = 0.0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + // Decimal places? + if (pos_ < end_ && *pos_ == '.') { + ++pos_; + double place = 0.1; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number += (*pos_ - '0') * place; + place *= 0.1; + ++pos_; + } + } + + if (is_neg) *number = -*number; + return true; + } + + private: + static bool IsDigit(const uint8_t c) { return '0' <= c && c <= '9'; } + static bool IsLineBreak(const uint8_t c) { return c == '\r' || c == '\n'; } + static bool IsWhitespace(const uint8_t c) { + return IsLineBreak(c) || c == '\t' || c == ' '; + } + + bool SkipBlank() { + if (pos_ == end_) return error_msg("PNM: reached end before blank"); + const uint8_t c = *pos_; + if (c != ' ' && c != '\n') return error_msg("PNM: expected blank"); + ++pos_; + return true; + } + + bool SkipSingleWhitespace() { + if (pos_ == end_) return error_msg("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_)) return error_msg("PNM: expected whitespace"); + ++pos_; + return true; + } + + bool SkipWhitespace() { + if (pos_ == end_) return error_msg("PNM: reached end before whitespace"); + if (!IsWhitespace(*pos_) && *pos_ != '#') { + return error_msg("PNM: expected whitespace/comment"); + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + + // Comment(s) + while (pos_ != end_ && *pos_ == '#') { + while (pos_ != end_ && !IsLineBreak(*pos_)) { + ++pos_; + } + // Newline(s) + while (pos_ != end_ && IsLineBreak(*pos_)) pos_++; + } + + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + return true; + } + + bool MatchString(const char* keyword) { + const uint8_t* ppos = pos_; + while (*keyword) { + if (ppos >= end_) return error_msg("PAM: unexpected end of input"); + if (*keyword != *ppos) return false; + ppos++; + keyword++; + } + pos_ = ppos; + return_on_error(SkipWhitespace()); + return true; + } + + bool ParseHeaderPAM(HeaderPNM* header, const uint8_t** pos) { + size_t num_channels = 3; + size_t max_val = 255; + while (!MatchString("ENDHDR")) { + return_on_error(SkipWhitespace()); + if (MatchString("WIDTH")) { + return_on_error(ParseUnsigned(&header->xsize)); + } else if (MatchString("HEIGHT")) { + return_on_error(ParseUnsigned(&header->ysize)); + } else if (MatchString("DEPTH")) { + return_on_error(ParseUnsigned(&num_channels)); + } else if (MatchString("MAXVAL")) { + return_on_error(ParseUnsigned(&max_val)); + } else if (MatchString("TUPLTYPE")) { + if (MatchString("RGB_ALPHA")) { + header->has_alpha = true; + } else if (MatchString("RGB")) { + } else if (MatchString("GRAYSCALE_ALPHA")) { + header->has_alpha = true; + header->is_gray = true; + } else if (MatchString("GRAYSCALE")) { + header->is_gray = true; + } else if (MatchString("BLACKANDWHITE_ALPHA")) { + header->has_alpha = true; + header->is_gray = true; + max_val = 1; + } else if (MatchString("BLACKANDWHITE")) { + header->is_gray = true; + max_val = 1; + } else { + return error_msg("PAM: unknown TUPLTYPE"); + } + } else { + return error_msg("PAM: unknown header keyword"); + } + } + if (num_channels != + (header->has_alpha ? 1 : 0) + (header->is_gray ? 1 : 3)) { + return error_msg("PAM: bad DEPTH"); + } + if (max_val == 0 || max_val >= 65536) { + return error_msg("PAM: bad MAXVAL"); + } + header->bits_per_sample = Log2(max_val + 1); + + *pos = pos_; + return true; + } + + bool ParseHeaderPNM(HeaderPNM* header, const uint8_t** pos) { + return_on_error(SkipWhitespace()); + return_on_error(ParseUnsigned(&header->xsize)); + + return_on_error(SkipWhitespace()); + return_on_error(ParseUnsigned(&header->ysize)); + + return_on_error(SkipWhitespace()); + size_t max_val; + return_on_error(ParseUnsigned(&max_val)); + if (max_val == 0 || max_val >= 65536) { + return error_msg("PNM: bad MaxVal"); + } + header->bits_per_sample = Log2(max_val + 1); + + return_on_error(SkipSingleWhitespace()); + + *pos = pos_; + return true; + } + + const uint8_t* pos_; + const uint8_t* const end_; +}; + +bool load_file(unsigned char** out, size_t* outsize, const char* filename) { + FILE* file; + file = fopen(filename, "rb"); + if (!file) return false; + if (fseek(file, 0, SEEK_END) != 0) { + fclose(file); + return false; + } + *outsize = ftell(file); + if (*outsize == LONG_MAX || *outsize < 9 || fseek(file, 0, SEEK_SET)) { + fclose(file); + return false; + } + *out = (unsigned char*)malloc(*outsize); + if (!(*out)) return false; + size_t readsize; + readsize = fread(*out, 1, *outsize, file); + fclose(file); + if (readsize != *outsize) return false; + return true; +} + +bool DecodePAM(const char* filename, uint8_t** buffer, size_t* w, size_t* h, + size_t* nb_chans, size_t* bitdepth) { + unsigned char* in_file; + size_t in_size; + if (!load_file(&in_file, &in_size, filename)) + return error_msg("Could not read input file"); + Parser parser(in_file, in_size); + HeaderPNM header = {}; + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&header, &pos)) return false; + + if (header.bits_per_sample == 0 || header.bits_per_sample > 16) { + return error_msg("PNM: bits_per_sample invalid (can do at most 16-bit)"); + } + *w = header.xsize; + *h = header.ysize; + *bitdepth = header.bits_per_sample; + *nb_chans = (header.is_gray ? 1 : 3) + (header.has_alpha ? 1 : 0); + + size_t pnm_remaining_size = in_file + in_size - pos; + size_t buffer_size = *w * *h * *nb_chans * (*bitdepth > 8 ? 2 : 1); + if (pnm_remaining_size < buffer_size) { + return error_msg("PNM file too small"); + } + *buffer = (uint8_t*)malloc(buffer_size); + memcpy(*buffer, pos, buffer_size); + return true; +} |