From dc8cf90cb3d5e198dc3391abcc22a295807f0301 Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Mon, 10 Aug 2020 06:29:59 -0700 Subject: [PATCH 01/10] Init guetzli sandbox --- oss-internship-2020/guetzli/BUILD.bazel | 65 +++++ oss-internship-2020/guetzli/WORKSPACE | 79 ++++++ .../guetzli/external/png.BUILD | 33 +++ .../guetzli/external/zlib.BUILD | 36 +++ .../guetzli/guetzli_entry_points.cc | 245 ++++++++++++++++++ .../guetzli/guetzli_entry_points.h | 29 +++ oss-internship-2020/guetzli/guetzli_sandbox.h | 36 +++ .../guetzli/guetzli_sandboxed.cc | 160 ++++++++++++ .../guetzli/guetzli_transaction.cc | 160 ++++++++++++ .../guetzli/guetzli_transaction.h | 216 +++++++++++++++ oss-internship-2020/guetzli/tests/BUILD.bazel | 22 ++ .../guetzli/tests/guetzli_sapi_test.cc | 165 ++++++++++++ .../guetzli/tests/guetzli_transaction_test.cc | 2 + .../guetzli/tests/testdata/bees.png | Bin 0 -> 177424 bytes .../guetzli/tests/testdata/landscape.jpg | Bin 0 -> 14418 bytes 15 files changed, 1248 insertions(+) create mode 100644 oss-internship-2020/guetzli/BUILD.bazel create mode 100644 oss-internship-2020/guetzli/WORKSPACE create mode 100644 oss-internship-2020/guetzli/external/png.BUILD create mode 100644 oss-internship-2020/guetzli/external/zlib.BUILD create mode 100644 oss-internship-2020/guetzli/guetzli_entry_points.cc create mode 100644 oss-internship-2020/guetzli/guetzli_entry_points.h create mode 100644 oss-internship-2020/guetzli/guetzli_sandbox.h create mode 100644 oss-internship-2020/guetzli/guetzli_sandboxed.cc create mode 100644 oss-internship-2020/guetzli/guetzli_transaction.cc create mode 100644 oss-internship-2020/guetzli/guetzli_transaction.h create mode 100644 oss-internship-2020/guetzli/tests/BUILD.bazel create mode 100644 oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc create mode 100644 oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc create mode 100644 oss-internship-2020/guetzli/tests/testdata/bees.png create mode 100644 oss-internship-2020/guetzli/tests/testdata/landscape.jpg diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel new file mode 100644 index 0000000..345f879 --- /dev/null +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -0,0 +1,65 @@ +load("@rules_cc//cc:defs.bzl", "cc_proto_library") +load("@rules_proto//proto:defs.bzl", "proto_library") +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:proto.bzl", + "sapi_proto_library", +) +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", + "sapi_library", +) + +cc_library( + name = "guetzli_wrapper", + srcs = ["guetzli_entry_points.cc"], + hdrs = ["guetzli_entry_points.h"], + deps = [ + "@guetzli//:guetzli_lib", + "@com_google_sandboxed_api//sandboxed_api:lenval_core", + "@com_google_sandboxed_api//sandboxed_api:vars", + #"@com_google_sandboxed_api//sandboxed_api/sandbox2/util:temp_file", visibility error + "@png_archive//:png" + ], + visibility = ["//visibility:public"] +) + +sapi_library( + name = "guetzli_sapi", + #srcs = ["guetzli_transaction.cc"], // Error when try to place definitions insde .cc file + hdrs = ["guetzli_sandbox.h", "guetzli_transaction.h"], + functions = [ + "ProcessJPEGString", + "ProcessRGBData", + "ButteraugliScoreQuality", + "ReadPng", + "ReadJpegData", + "ReadDataFromFd", + "WriteDataToFd" + ], + input_files = ["guetzli_entry_points.h"], + lib = ":guetzli_wrapper", + lib_name = "Guetzli", + visibility = ["//visibility:public"], + namespace = "guetzli::sandbox" +) + +# cc_library( +# name = "guetzli_sapi_transaction", +# #srcs = ["guetzli_transaction.cc"], +# hdrs = ["guetzli_transaction.h"], +# deps = [ +# ":guetzli_sapi" +# ], +# visibility = ["//visibility:public"] +# ) + +cc_binary( + name="guetzli_sandboxed", + srcs=["guetzli_sandboxed.cc"], + includes = ["."], + visibility= [ "//visibility:public" ], + deps = [ + #":guetzli_sapi_transaction" + ":guetzli_sapi" + ] +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/WORKSPACE b/oss-internship-2020/guetzli/WORKSPACE new file mode 100644 index 0000000..17887de --- /dev/null +++ b/oss-internship-2020/guetzli/WORKSPACE @@ -0,0 +1,79 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +workspace(name = "guetzli_sandboxed") + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +# Include the Sandboxed API dependency if it does not already exist in this +# project. This ensures that this workspace plays well with other external +# dependencies that might use Sandboxed API. +maybe( + git_repository, + name = "com_google_sandboxed_api", + # This example depends on the latest master. In an embedding project, it + # is advisable to pin Sandboxed API to a specific revision instead. + # commit = "ba47adc21d4c9bc316f3c7c32b0faaef952c111e", # 2020-05-15 + branch = "master", + remote = "https://github.com/google/sandboxed-api.git", +) + +# From here on, Sandboxed API files are available. The statements below setup +# transitive dependencies such as Abseil. Like above, those will only be +# included if they don't already exist in the project. +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:sapi_deps.bzl", + "sapi_deps", +) + +sapi_deps() + +# Need to separately setup Protobuf dependencies in order for the build rules +# to work. +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + +protobuf_deps() + +maybe( + git_repository, + name = "guetzli", + remote = "https://github.com/google/guetzli.git", + branch = "master" +) + +maybe( + git_repository, + name = "googletest", + remote = "https://github.com/google/googletest", + tag = "release-1.10.0" +) + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "png_archive", + build_file = "png.BUILD", + sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", + strip_prefix = "libpng-1.2.57", + url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", +) + +http_archive( + name = "zlib_archive", + build_file = "zlib.BUILD", + sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", + strip_prefix = "zlib-1.2.10", + url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/external/png.BUILD b/oss-internship-2020/guetzli/external/png.BUILD new file mode 100644 index 0000000..9ff982b --- /dev/null +++ b/oss-internship-2020/guetzli/external/png.BUILD @@ -0,0 +1,33 @@ +# Description: +# libpng is the official PNG reference library. + +licenses(["notice"]) # BSD/MIT-like license + +cc_library( + name = "png", + srcs = [ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", + ], + hdrs = [ + "png.h", + "pngconf.h", + ], + includes = ["."], + linkopts = ["-lm"], + visibility = ["//visibility:public"], + deps = ["@zlib_archive//:zlib"], +) diff --git a/oss-internship-2020/guetzli/external/zlib.BUILD b/oss-internship-2020/guetzli/external/zlib.BUILD new file mode 100644 index 0000000..edb77fd --- /dev/null +++ b/oss-internship-2020/guetzli/external/zlib.BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # BSD/MIT-like license (for zlib) + +cc_library( + name = "zlib", + srcs = [ + "adler32.c", + "compress.c", + "crc32.c", + "crc32.h", + "deflate.c", + "deflate.h", + "gzclose.c", + "gzguts.h", + "gzlib.c", + "gzread.c", + "gzwrite.c", + "infback.c", + "inffast.c", + "inffast.h", + "inffixed.h", + "inflate.c", + "inflate.h", + "inftrees.c", + "inftrees.h", + "trees.c", + "trees.h", + "uncompr.c", + "zconf.h", + "zutil.c", + "zutil.h", + ], + hdrs = ["zlib.h"], + includes = ["."], +) diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc new file mode 100644 index 0000000..446150c --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -0,0 +1,245 @@ +#include "guetzli/jpeg_data_reader.h" +#include "guetzli/quality.h" +#include "guetzli_entry_points.h" +#include "png.h" +#include "sandboxed_api/sandbox2/util/fileops.h" + +#include +#include +#include +#include +#include + +namespace { + +inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { + return (static_cast(val) * static_cast(alpha) + 128) / 255; +} + +template +void CopyMemoryToLenVal(const T* data, size_t size, + sapi::LenValStruct* out_data) { + free(out_data->data); // Not sure about this + out_data->size = size; + T* new_out = static_cast(malloc(size)); + memcpy(new_out, data, size); + out_data->data = new_out; +} + +} // namespace + +extern "C" bool ProcessJPEGString(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* in_data, + sapi::LenValStruct* out_data) +{ + std::string in_data_temp(static_cast(in_data->data), + in_data->size); + + guetzli::ProcessStats stats; + if (verbose > 0) { + stats.debug_output_file = stderr; + } + + std::string temp_out = ""; + auto result = guetzli::Process(*params, &stats, in_data_temp, &temp_out); + + if (result) { + CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); + } + + return result; +} + +extern "C" bool ProcessRGBData(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* rgb, + int w, int h, + sapi::LenValStruct* out_data) +{ + std::vector in_data_temp; + in_data_temp.reserve(rgb->size); + + auto* rgb_data = static_cast(rgb->data); + std::copy(rgb_data, rgb_data + rgb->size, std::back_inserter(in_data_temp)); + + guetzli::ProcessStats stats; + if (verbose > 0) { + stats.debug_output_file = stderr; + } + + std::string temp_out = ""; + auto result = + guetzli::Process(*params, &stats, in_data_temp, w, h, &temp_out); + + //TODO: Move shared part of the code to another function + if (result) { + CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); + } + + return result; +} + +extern "C" bool ReadPng(sapi::LenValStruct* in_data, + int* xsize, int* ysize, + sapi::LenValStruct* rgb_out) +{ + std::string data(static_cast(in_data->data), in_data->size); + std::vector rgb; + + png_structp png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) { + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr)) != 0) { + // Ok we are here because of the setjmp. + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + + std::istringstream memstream(data, std::ios::in | std::ios::binary); + png_set_read_fn(png_ptr, static_cast(&memstream), [](png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead) { + std::istringstream& memstream = *static_cast(png_get_io_ptr(png_ptr)); + + memstream.read(reinterpret_cast(outBytes), byteCountToRead); + + if (memstream.eof()) png_error(png_ptr, "unexpected end of data"); + if (memstream.fail()) png_error(png_ptr, "read from memory error"); + }); + + // The png_transforms flags are as follows: + // packing == convert 1,2,4 bit images, + // strip == 16 -> 8 bits / channel, + // shift == use sBIT dynamics, and + // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. + const unsigned int png_transforms = + PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; + + png_read_png(png_ptr, info_ptr, png_transforms, nullptr); + + png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); + + *xsize = png_get_image_width(png_ptr, info_ptr); + *ysize = png_get_image_height(png_ptr, info_ptr); + rgb.resize(3 * (*xsize) * (*ysize)); + + const int components = png_get_channels(png_ptr, info_ptr); + switch (components) { + case 1: { + // GRAYSCALE + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = row_in[x]; + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 2: { + // GRAYSCALE + ALPHA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 3: { + // RGB + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + memcpy(row_out, row_in, 3 * (*xsize)); + } + break; + } + case 4: { + // RGBA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t alpha = row_in[4 * x + 3]; + row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); + row_out[3 * x + 1] = BlendOnBlack(row_in[4 * x + 1], alpha); + row_out[3 * x + 2] = BlendOnBlack(row_in[4 * x + 2], alpha); + } + } + break; + } + default: + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + + CopyMemoryToLenVal(rgb.data(), rgb.size(), rgb_out); + + return true; +} + +extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, + int mode, + int* xsize, int* ysize) +{ + std::string data(static_cast(in_data->data), in_data->size); + guetzli::JPEGData jpg; + + auto result = guetzli::ReadJpeg(data, + static_cast(mode), &jpg); + + if (result) { + *xsize = jpg.width; + *ysize = jpg.height; + } + + return result; +} + +extern "C" double ButteraugliScoreQuality(double quality) { + return guetzli::ButteraugliScoreForQuality(quality); +} + +extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data) { + struct stat file_data; + auto status = fstat(fd, &file_data); + + if (status < 0) { + return false; + } + + auto fsize = file_data.st_size; + + std::unique_ptr buf(new char[fsize]); + status = read(fd, buf.get(), fsize); + + if (status < 0) { + return false; + } + + CopyMemoryToLenVal(buf.get(), fsize, out_data); + + return true; +} + +extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data) { + return sandbox2::file_util::fileops::WriteToFD(fd, + static_cast(data->data), data->size); +} \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.h b/oss-internship-2020/guetzli/guetzli_entry_points.h new file mode 100644 index 0000000..6fd0f11 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_entry_points.h @@ -0,0 +1,29 @@ +#pragma once + +#include "guetzli/processor.h" +#include "sandboxed_api/lenval_core.h" +#include "sandboxed_api/vars.h" + +extern "C" bool ProcessJPEGString(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* in_data, + sapi::LenValStruct* out_data); + +extern "C" bool ProcessRGBData(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* rgb, + int w, int h, + sapi::LenValStruct* out_data); + +extern "C" bool ReadPng(sapi::LenValStruct* in_data, + int* xsize, int* ysize, + sapi::LenValStruct* rgb_out); + +extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, + int mode, int* xsize, int* ysize); + +extern "C" double ButteraugliScoreQuality(double quality); + +extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data); + +extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data); \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h new file mode 100644 index 0000000..3fa2b10 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "guetzli_sapi.sapi.h" +#include "sandboxed_api/sandbox2/policy.h" +#include "sandboxed_api/sandbox2/policybuilder.h" +#include "sandboxed_api/util/flag.h" + +namespace guetzli { +namespace sandbox { + +class GuetzliSapiSandbox : public GuetzliSandbox { + public: + std::unique_ptr ModifyPolicy( + sandbox2::PolicyBuilder*) override { + + return sandbox2::PolicyBuilder() + .AllowStaticStartup() + .AllowRead() + .AllowSystemMalloc() + .AllowWrite() + .AllowExit() + .AllowStat() + .AllowSyscalls({ + __NR_futex, + __NR_close, + __NR_recvmsg // Seems like this one needed to work with remote file descriptors + }) + .BuildOrDie(); + } +}; + +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc new file mode 100644 index 0000000..a62f249 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include + +#include + +#include "guetzli_transaction.h" +#include "sandboxed_api/sandbox2/util/fileops.h" +#include "sandboxed_api/util/statusor.h" + +namespace { + +constexpr int kDefaultJPEGQuality = 95; +constexpr int kDefaultMemlimitMB = 6000; // in MB +//constexpr absl::string_view kMktempSuffix = "XXXXXX"; + +// sapi::StatusOr> CreateNamedTempFile( +// absl::string_view prefix) { +// std::string name_template = absl::StrCat(prefix, kMktempSuffix); +// int fd = mkstemp(&name_template[0]); +// if (fd < 0) { +// return absl::UnknownError("Error creating temp file"); +// } +// return std::pair{std::move(name_template), fd}; +// } + +void TerminateHandler() { + fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" + "Make sure that there is 300MB/MPix of memory available.\n"); + exit(1); +} + +void Usage() { + fprintf(stderr, + "Guetzli JPEG compressor. Usage: \n" + "guetzli [flags] input_filename output_filename\n" + "\n" + "Flags:\n" + " --verbose - Print a verbose trace of all attempts to standard output.\n" + " --quality Q - Visual quality to aim for, expressed as a JPEG quality value.\n" + " Default value is %d.\n" + " --memlimit M - Memory limit in MB. Guetzli will fail if unable to stay under\n" + " the limit. Default limit is %d MB.\n" + " --nomemlimit - Do not limit memory usage.\n", kDefaultJPEGQuality, kDefaultMemlimitMB); + exit(1); +} + +} // namespace + +int main(int argc, const char** argv) { + std::set_terminate(TerminateHandler); + + int verbose = 0; + int quality = kDefaultJPEGQuality; + int memlimit_mb = kDefaultMemlimitMB; + + int opt_idx = 1; + for(;opt_idx < argc;opt_idx++) { + if (strnlen(argv[opt_idx], 2) < 2 || argv[opt_idx][0] != '-' || argv[opt_idx][1] != '-') + break; + + if (!strcmp(argv[opt_idx], "--verbose")) { + verbose = 1; + } else if (!strcmp(argv[opt_idx], "--quality")) { + opt_idx++; + if (opt_idx >= argc) + Usage(); + quality = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--memlimit")) { + opt_idx++; + if (opt_idx >= argc) + Usage(); + memlimit_mb = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--nomemlimit")) { + memlimit_mb = -1; + } else if (!strcmp(argv[opt_idx], "--")) { + opt_idx++; + break; + } else { + fprintf(stderr, "Unknown commandline flag: %s\n", argv[opt_idx]); + Usage(); + } + } + + if (argc - opt_idx != 2) { + Usage(); + } + + sandbox2::file_util::fileops::FDCloser in_fd_closer( + open(argv[opt_idx], O_RDONLY)); + + if (in_fd_closer.get() < 0) { + fprintf(stderr, "Can't open input file: %s\n", argv[opt_idx]); + return 1; + } + + // auto out_temp_file = CreateNamedTempFile("/tmp/" + std::string(argv[opt_idx + 1])); + // if (!out_temp_file.ok()) { + // fprintf(stderr, "Can't create temporary output file: %s\n", + // argv[opt_idx + 1]); + // return 1; + // } + // sandbox2::file_util::fileops::FDCloser out_fd_closer( + // out_temp_file.value().second); + + // if (unlink(out_temp_file.value().first.c_str()) < 0) { + // fprintf(stderr, "Error unlinking temp out file: %s\n", + // out_temp_file.value().first.c_str()); + // return 1; + // } + + sandbox2::file_util::fileops::FDCloser out_fd_closer( + open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); + + if (out_fd_closer.get() < 0) { + fprintf(stderr, "Can't create temporary output file: %s\n", argv[opt_idx]); + return 1; + } + + // sandbox2::file_util::fileops::FDCloser out_fd_closer(open(argv[opt_idx + 1], + // O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)); + + // if (out_fd_closer.get() < 0) { + // fprintf(stderr, "Can't open output file: %s\n", argv[opt_idx + 1]); + // return 1; + // } + + guetzli::sandbox::TransactionParams params = { + in_fd_closer.get(), + out_fd_closer.get(), + verbose, + quality, + memlimit_mb + }; + + guetzli::sandbox::GuetzliTransaction transaction(std::move(params)); + auto result = transaction.Run(); + + if (result.ok()) { + if (access(argv[opt_idx + 1], F_OK) != -1) { + if (remove(argv[opt_idx + 1]) < 0) { + fprintf(stderr, "Error deleting existing output file: %s\n", + argv[opt_idx + 1]); + } + } + + std::stringstream path; + path << "/proc/self/fd/" << out_fd_closer.get(); + linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1], + AT_SYMLINK_FOLLOW); + } + else { + fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc new file mode 100644 index 0000000..9403783 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -0,0 +1,160 @@ +#include "guetzli_transaction.h" + +#include + +namespace guetzli { +namespace sandbox { + +absl::Status GuetzliTransaction::Init() { + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); + + if (in_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote input fd is set to -1"); + } + if (out_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote output fd is set to -1"); + } + + return absl::OkStatus(); +} + + absl::Status GuetzliTransaction::ProcessPng(GuetzliAPi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + sapi::v::LenVal rgb_in(0); + + auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_in.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading PNG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, + rgb_in.PtrBefore(), xsize.GetValue(), + ysize.GetValue(), output->PtrBoth()); + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + + absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + ::sapi::v::Int xsize; + ::sapi::v::Int ysize; + auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), + ysize.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading JPG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, + input->PtrBefore(), output->PtrBoth()); + + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + +absl::Status GuetzliTransaction::Main() { + GuetzliApi api(sandbox()); + + sapi::v::LenVal input(0); + sapi::v::LenVal output(0); + sapi::v::Struct params; + + auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading data inside sandbox" + ); + } + + auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); + + if (!score_quality_result.ok()) { + return absl::FailedPreconditionError( + "Error calculating butteraugli score" + ); + } + + params.mutable_data()->butteraugli_target = score_quality_result.value(); + + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + + if (input.GetDataSize() >= 8 && + memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { + auto process_status = ProcessPng(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } else { + auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } + + auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), + output.PtrBefore()); + + if (!write_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error writing file inside sandbox" + ); + } + + return absl::OkStatus(); +} + +time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( + uint64_t pixels) const { + return (pixels / kMpixPixels + 5) * 60; +} + +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h new file mode 100644 index 0000000..5d6e83a --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -0,0 +1,216 @@ +#pragma once + +#include +#include + +#include "guetzli_sandbox.h" +#include "sandboxed_api/transaction.h" +#include "sandboxed_api/vars.h" + +namespace guetzli { +namespace sandbox { + +constexpr int kDefaultTransactionRetryCount = 1; +constexpr uint64_t kMpixPixels = 1'000'000; + +constexpr int kBytesPerPixel = 350; +constexpr int kLowestMemusageMB = 100; // in MB + +struct TransactionParams { + int in_fd; + int out_fd; + int verbose; + int quality; + int memlimit_mb; +}; + +//Add optional time limit/retry count as a constructors arguments +//Use differenet status errors +class GuetzliTransaction : public sapi::Transaction { + public: + GuetzliTransaction(TransactionParams&& params) + : sapi::Transaction(std::make_unique()) + , params_(std::move(params)) + , in_fd_(params_.in_fd) + , out_fd_(params_.out_fd) + { + sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount); + sapi::Transaction::SetTimeLimit(0); + } + + private: + absl::Status Init() override; + absl::Status Main() final; + + absl::Status ProcessPng(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const; + + absl::Status ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const; + + // As guetzli takes roughly 1 minute of CPU per 1 MPix we need to calculate + // approximate time for transaction to complete + time_t CalculateTimeLimitFromImageSize(uint64_t pixels) const; + + const TransactionParams params_; + sapi::v::Fd in_fd_; + sapi::v::Fd out_fd_; +}; + +absl::Status GuetzliTransaction::Init() { + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); + + if (in_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote input fd is set to -1"); + } + if (out_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote output fd is set to -1"); + } + + return absl::OkStatus(); +} + + absl::Status GuetzliTransaction::ProcessPng(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + sapi::v::LenVal rgb_in(0); + + auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_in.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading PNG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, + rgb_in.PtrBefore(), xsize.GetValue(), + ysize.GetValue(), output->PtrBoth()); + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + + absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), + ysize.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading JPG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, + input->PtrBefore(), output->PtrBoth()); + + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + +absl::Status GuetzliTransaction::Main() { + GuetzliApi api(sandbox()); + + sapi::v::LenVal input(0); + sapi::v::LenVal output(0); + sapi::v::Struct params; + + auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading data inside sandbox" + ); + } + + auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); + + if (!score_quality_result.ok()) { + return absl::FailedPreconditionError( + "Error calculating butteraugli score" + ); + } + + params.mutable_data()->butteraugli_target = score_quality_result.value(); + + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + + if (input.GetDataSize() >= 8 && + memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { + auto process_status = ProcessPng(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } else { + auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } + + auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), + output.PtrBefore()); + + if (!write_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error writing file inside sandbox" + ); + } + + return absl::OkStatus(); +} + +time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( + uint64_t pixels) const { + return (pixels / kMpixPixels + 5) * 60; +} + +} // namespace sandbox +} // namespace guetzli diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel new file mode 100644 index 0000000..b7c7517 --- /dev/null +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -0,0 +1,22 @@ +# cc_test( +# name = "transaction_tests", +# srcs = ["guetzli_transaction_test.cc"], +# visibility=["//visibility:public"], +# includes = ["."], +# deps = [ +# "//:guetzli_sapi", +# "@googletest//:gtest_main" +# ], +# ) + +# cc_test( +# name = "sapi_lib_tests", +# srcs = ["guetzli_sapi_test.cc"], +# visibility=["//visibility:public"], +# includes=[".."], +# deps = [ +# "//:guetzli_sapi", +# "@googletest//:gtest_main" +# ], +# data = glob(["testdata/*"]) +# ) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc new file mode 100644 index 0000000..73e54d6 --- /dev/null +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -0,0 +1,165 @@ +#include "gtest/gtest.h" +#include "guetzli_sandbox.h" +#include "sandboxed_api/sandbox2/util/fileops.h" +#include "sandboxed_api/vars.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace guetzli { +namespace sandbox { +namespace tests { + +namespace { + +constexpr const char* IN_PNG_FILENAME = "bees.png"; +constexpr const char* IN_JPG_FILENAME = "landscape.jpg"; + +constexpr int IN_PNG_FILE_SIZE = 177'424; +constexpr int IN_JPG_FILE_SIZE = 14'418; + +constexpr int DEFAULT_QUALITY_TARGET = 95; + +constexpr const char* RELATIVE_PATH_TO_TESTDATA = + "/guetzli/guetzli-sandboxed/tests/testdata/"; + +std::string GetPathToInputFile(const char* filename) { + return std::string(getenv("TEST_SRCDIR")) + + std::string(RELATIVE_PATH_TO_TESTDATA) + + std::string(filename); +} + +std::string ReadFromFile(const std::string& filename) { + std::ifstream stream(filename, std::ios::binary); + + if (!stream.is_open()) { + return ""; + } + + std::stringstream result; + result << stream.rdbuf(); + return result.str(); +} + +} // namespace + +class GuetzliSapiTest : public ::testing::Test { +protected: + void SetUp() override { + sandbox_ = std::make_unique(); + sandbox_->Init().IgnoreError(); + api_ = std::make_unique(sandbox_.get()); + } + + std::unique_ptr sandbox_; + std::unique_ptr api_; +}; + +TEST_F(GuetzliSapiTest, ReadDataFromFd) { + std::string input_file_path = GetPathToInputFile(IN_PNG_FILENAME); + int fd = open(input_file_path.c_str(), O_RDONLY); + ASSERT_TRUE(fd != -1) << "Error opening input file"; + sapi::v::Fd remote_fd(fd); + auto send_fd_status = sandbox_->TransferToSandboxee(&remote_fd); + ASSERT_TRUE(send_fd_status.ok()) << "Error sending fd to sandboxee"; + ASSERT_TRUE(remote_fd.GetRemoteFd() != -1) << "Error opening remote fd"; + sapi::v::LenVal data(0); + auto read_status = + api_->ReadDataFromFd(remote_fd.GetRemoteFd(), data.PtrBoth()); + ASSERT_TRUE(read_status.value_or(false)) << "Error reading data from fd"; + ASSERT_EQ(data.GetDataSize(), IN_PNG_FILE_SIZE) << "Wrong size of file"; +} + +// TEST_F(GuetzliSapiTest, WriteDataToFd) { + +// } + +TEST_F(GuetzliSapiTest, ReadPng) { + std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); + ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + sapi::v::LenVal rgb_out(0); + + auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_out.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; + ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; +} + +TEST_F(GuetzliSapiTest, ReadJpeg) { + std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); + ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + + auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, + xsize.PtrBoth(), ysize.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; + ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; +} + +// This test can take up to few minutes depending on your hardware +TEST_F(GuetzliSapiTest, ProcessRGB) { + std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); + ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + sapi::v::LenVal rgb_out(0); + + auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_out.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; + ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; + auto quality = + api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); + ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; + sapi::v::Struct params; + sapi::v::LenVal out_data(0); + params.mutable_data()->butteraugli_target = quality.value(); + + status = api_->ProcessRGBData(params.PtrBefore(), 0, rgb_out.PtrBefore(), + xsize.GetValue(), ysize.GetValue(), out_data.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png file"; + ASSERT_EQ(out_data.GetDataSize(), 38'625); + //ADD COMPARSION WITH REFERENCE OUTPUT +} + +// This test can take up to few minutes depending on your hardware +TEST_F(GuetzliSapiTest, ProcessJpeg) { + std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); + ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + + auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, + xsize.PtrBoth(), ysize.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; + ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; + + auto quality = + api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); + ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; + sapi::v::Struct params; + params.mutable_data()->butteraugli_target = quality.value(); + sapi::v::LenVal out_data(0); + + status = api_->ProcessJPEGString(params.PtrBefore(), 0, in_data.PtrBefore(), + out_data.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg file"; + ASSERT_EQ(out_data.GetDataSize(), 10'816); + //ADD COMPARSION WITH REFERENCE OUTPUT +} + +} // namespace tests +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc new file mode 100644 index 0000000..8471d1e --- /dev/null +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -0,0 +1,2 @@ +#include "gtest/gtest.h" +#include "guetzli_transaction.h" diff --git a/oss-internship-2020/guetzli/tests/testdata/bees.png b/oss-internship-2020/guetzli/tests/testdata/bees.png new file mode 100644 index 0000000000000000000000000000000000000000..11640c7488fb114127f869572d7ffec132e77746 GIT binary patch literal 177424 zcmV)7K*zs{P)b{WyyIZw$}P$?~|Ez>27vYyoe$-l%W|7Y|jApvke&j<$g8%GPZ2YcqB?9 zMT(d1s;;ie%yV{pYxyB|w%|gex-N-Rxi9g>x(2^~_n&}`9Ptr6i*JDhAG`k!3H23u zpG@g9@y`GOoqS_|kUj%mgF$Y=NB2AGP4Jw)_I&^D5C8n@-~8sE-~H~%$8$gb$A7c^ z`@i`S^6EeT-N$YI?0#d?N8G3b?s=X4VIx0(c=h&jx!<){actT5j0@_u>ND^&@)b}Z z0K*@V=imsM*r3jf zqx3^`ItVOW4hDV?!z{bY5-)400IVu5(Eke zV1h8h05d_{LECZc*p3f(Je_IJZUb!u0Yz4c7Yz+ml_b>qLj!gi2!r7bmXEOGmJiR_ zbGonb)$vPsVV7#AstSuC1r;XZy)HoN098BL|%6d9-`>JbF$a9d{5C?5Rv7UCh|_ zyKOw|`>R)%hs(Iz_lNs^-^Uo-hN?KmwvDPy+Q!%}+jglg>b>em$ zH-o^9ATR)?(?$cGq#@G*48}1Mn4}XZ1`q%t3IG!#LJ2ikJNAq2?(}fa{a$w)YsWS+ zcGiGODpmmu*hD2j90ExbG8ou!;3UE9wBw{3>Y*Cq{opI<&8XMAUS0H1x*v5{ybm{W z6bG?MdsPkA1_&+K?s&rOZu@r3>oLD;KdB#q=XfSgJnh?@$Khknp1x&HAB06wBngnJ zvVKX$Xd_%NsV6u1sEVepaB8_n*6y+fX~0>4Blw`M7V2w2;NuKkAMF5&wl#T zUw-@k*Z=(c-~Q=$!k;?N18+uM7>aOM!eDQR32hG#mCx=jcl$nyJuca|RDne7OyMzj zLIQNsuz{I82Tk1-D!NDlHed~^L2SW=Im~N7Xai@9Q~Mn4zH_eK!&O0u(z;8SiRmLi zE2fzXLr?%a-Dt)Pn(Lh-K?rsQM!MZ|x>CaVB#HF_R45@N9KeYYcZ@jfa|I-}lkV<0 zJ=4bwr~rvV6fr4)WeV8DhkbYF)x+iEr)RJEz?w#NkR*!TZgmO52&@DnWYu1})VRBh%U+jld$`~3cHP}^tE#H8*S2kA zY-7J{MHWa=(k2NMwI;8?KylWpL72o~&TEB$76r=k`X!w%uJb zfig$}PD{8OdOhlXBa5jGRUwdx5JC*|1_WvNK>FMgaF9qC96%!t9zeZZkpKlE1nr0; zgOq^UI8yFyd)SOVX7_dGdHU7{(uRYSFF}J>Ldo#D-n0RuK?@W!6NVxIOv|2~q$ks% zid&IuR|-85n#fwWEhGWg!dOr!pwmdh9eXAK9ETwlMPxe(#JU?l`0=ZU*Y~$){QRr^ z|N4I(=h20spe!`(jvXUL(n#RgNpZXTV!M2lN>Y$q5}f z$FJPV%_ewhU<$SV3is z?83Hj-*MTs4~ZQu60ZNJ;LZ5u;lD~Uxi<^m1VxpjJ4qy(OjA%`=kCH1Iy zZBSAtKqSpLL4+h}1X$C{j_1N6PNvW60O%x0qCQKL;q1WC-5TMMd5P=gC=FUtq z9fEYGIMzNJfe8pA2m`H2r3jRyEmc%;6g7fkktGya0oLtY|8&R*beQ!~1Y|fNLdZtW zA0UumU3Lt(qnlBaH)5YSfSV(wA}Ch2&|sKQo+smqU}DDIhB1SKnL-1GJQA!sv6$M( z6o$n#0DJ5Gx$063hCfeGMNB3@U8SJd43s}0S34euZR&`i6OiR zj-0djhj(|se|7oauK0uV%L48%^>kxMA|jMo#6`RxM4D7->~e3Hd!UKvm_5>hiB9*CIG!Mb4KcHJ zg>}DUp0AVFz637d-B3r}Z2Rb-p{fl*?U9otf|A9<(}4>O5eXqch)uLK(+vhlNYe>Y zDKsHMAxAaqho9fQdAK&PUB;*9>%aZ?fA&v*|L*;}8zY;{qF07#J5D!xI!S8ZE}!k= zff$QrBnBW7f@oAENTfo6qGL*xOWa|b)966J2$aB$p3P3?jMJfQrzuAW0f|{S3Sb;` zOvFh$oy_EbZJvPN=-KI1Ap^F$lVOGuwk_MxHfr0(KD6!JFWbJ~ZTtPU?c27EF{(D! z8k%bnEDhv3`z9Z=1+an5gci%Ni6TpG+ly8U2^2a30K%ZjW^nFjX?J&b-?En0A`A(Y z7Bt}Vg-3)1X0bpS&exoTn-Hb~6Dks|?+(;G~e7W6N*c=TEu!4iE_(QPbw_?Bz9DwtaNd&}Frcq~@WtPJ`1CKyV{*M4udZ|hMK&UOQ(vS( zP+pfG2K1sHK#~bx70?p_vY`rD;wFKMcn2W3#otj$?#TcJOkjc&o=5{wY9fgVpd{4D zeo3I~(L*;~r;V%A&IIKwZZv>JTN!F?wBbS&OOA?>_=HCeW^;BpUEfPkHOinMFNEHd znJi#OuH9UQ5~n~@L<`Xp0$$#C&<2Eu`~5He>We;}e*LFE&2S)(!~flX_}SaH_wT>H zIfnHjsj=<09dsv1;!St8zhPvo4Z$z#aIuoLTGtc}n852z0wiwYAb02qk=KKXo3_cs zup5qTn}p#P-62I9sTrM5H%`Zxi7{TY#^APTsP^r0 zx8LvkW#9MO_AxGFY&En<*cwibqB*^ZS`aWN_U5$9kgSMmhA;t=&Bbe!gm8i;y;zK2 z0CI@#w1q4I+>7l-4iSn17-*3wilJ!aWtuN}j0Vt<*vZ2GYfcJ4<=k9J5JiO)*Bm1w zNDCCRG6qT#HOP`jxx@k`uP^12SQ!KhaTzv2+spz)Yr~!hLt;HKc!99adTGrmUY#-P zCX@}XVZ$vlc@!^Ui!ce&h+}2~=K6&XVTQBPRV`Iib`2TiPT8KjY=A)-%AH3|&+f~}OR==8+4Wp^F!MrHu4F+c}pP+8)SY11~mWHQH{ z#BOv4UZ{p?&zrQxchf8hY@tZ9C}~+cfr0g##nTloxm$`vkD$qgZUCA``2L%#?*8!h z2mABk$EV|O{^Ik${5SvY$M4?Pc+SfpN4#u=UixGs=?+M;#$Xw(Q)XJr<{U2zT2n6| z9zd)li#}?Dhqx;b@pql>Ov82ncC;LWH9--#WTIikX+T)YT`vvUKI~)B@isc$Jc2aS zu{#vwo^rW8Gwx8S!BMHo7**o7Yu~qht8LrI*vD4e&={nowMZ8EG-m{IGR5I=sA78H zaL^=g1OY@%f@p9}%=G~bLJDdx2|7y&itQK14lqs9IF|Rvb1jlkxkL&9$qW)8k`M_< zbv!~hARuZ*Mj+DbEeRv8R|!DKVi*NsA!AX}Vijtz1_vXtP)bk+0!j)%o*0UVy$1N= z<%tXsP6Cv5>U&ATC07&^ukWgZ8@R#MDvDyvNz7D9`;l4Nf*Mq55($=|3Si33G~MJ; z<jYad`SWAU4Eqz*331a3i-r**mJ0x%#jCA6T*bKrsOL+-6L zlv1kQ$&K8-7Do(fqSj`d#OO6BY`5L*w$XCWCD<%1Or#(bQYRW%KKe3{nqeYP35a%~ zsem$Nxr#AcRWa!`Swo(x@>;joNA(HN!rPp5nl0SGFQB2m!E*acV>6xEE1yErIu z!O6wYrBld+gEb&QG+GFgUOa|oV8x84m~g!lYdbKtw9TbiIKrd_-K=8R4(QTmwPu+I zDRzHwpP+*1`IUpccmYX@z(^BTlfDO!3PT+T%t5*nlvp`gif~!>{w^N4JlY zc`x=Ixzzz21Whkl1{Me{_bh=Yy3wWsJ%Az_S$YJCBRH4}MwWC(#}RKqC&#JAP_UQY z1d*wsQK~YE7N`3vQUy&EhtYc#$X97v8;+jE*t$cC>Z-?JSbUapAEq44N*?vGYG24LKTWH z9}*z7$f*#V;Tx1##1M;E85FI#8?Vv1s3sCI&E^UIC;~}E@=|29t`tEg*{9UFBz%|% zsH7+A8qow4wxJG-l35cdsi5vE((*t=7A!V7OS8(``S`|KAK+=Iwagw;O14TpMG@Ve zB+wx_YP|?SksM-4sp2SUaHxj1Dw4HiO^Q+_02)v*0W82D!kLQ&=Jc6k#8xNm+mODL z{>*tY9+6MUK}_fY{0t!Z89AIM@-?vmkHBZ`Ujs+!N5oq|z}5L<@(A9EUjgkrk^>;H z6AmUp@(a~pr9ZxU_4T{?)pz~bja%pTMSpnUx22@MI~ZZ&K=gqM~~EGEIQ zmokz}Vn&l_uz-EV6;BU61tyrtj74s{idbvJK*vJ_5hZ65VL|_#YWHbekV=~LvcG-e zq{Hrs?(XTMXHTbJB9(|pQV^vx7T&|O9He}Qk>g-8uMw? zB?K@?IbDm{GZg~01dSI=pVN6ngeYZJJYg^1W@p-`d*ki|$wIayO!#yS7>0ZCgJ>x zB#4r8e86A}2s`u&)ddh+vf#k-+Ep+)2`b`=Qr*P5xr2m@rk1jca4p{`K$JP8O=GKF zwM*iQYOo}BLabCQR~Q0Sg*roGb_k zd?se|5{qRVEM^{O+DGP4{UlKOkJK0B9qV!$;(Qzb3AhZ-C z+ajy5t%Cu*cu-QY~d^1@#3_=4?QV(|Z`HEk+!Lu_bb1hHNY`dS~4iPSz zkYhQcZc++vi(0Nn$b{}VEB6tgY?8F$SDh4hDWYAheQp>Gc zT$gL4Vf}n&7#E3M^5MG5^*t3}X;7q7rQv-F)^!u3OiG-DtX_eMwNnaiy3bj>hF&C| zREErOg%DBJKn<*)SsXtSt8 zq{PKrPos&^i8PidDS|0zMcv3{?1S_IyEJU92L%%r0-1CzBZ!7mYu;v(g4xc&M7>!S z2eHy#GsTlgszs%0tyy^>1J|sqg1?QvATHzv{6ORS z=I;3P=_%lm>rJ{M-}r3>Jy4CtunO~SYji0 z@E#akzHAg2bPaA22%4Q11r&TY>pJuaXl4>e^Tve$ma%qD=gFL8^UR<7n4Q_abstCf zG5atKce+r3R0@hg1%p*&skG9f6tf&pM2>+X%p?n22wFC$MAxPwSM_5~_X1LAFkgT3 z&2Qg-{PcG}uFrqeZ{FbH!w;UW?|S=+&wp@ChwWa$$YlmL(6f)@(5K^eR~i@(BdQf| zjw;B)F<5((m%bKP*h?Bo<}FW$Z4ztJ@2NUSKmT_Jfl`pU4V)iOQ{^T40kUnJ%KK z+F0gg>Lv{lg)~#G;c4VpG0h|ny`XUS#(JeqoAOHv6*5GNlW695G%{0=l8Go z8f#cG)k-;G={Hr}iep=L28URrQE=IaOBv{>Rt}S@CC_DPl_vyVnP;FP1(5-*xG@$` zNqO~_5;oYP>Bfr*LYWn&Yo%S5-GG;;r1WWQse47tQ&cI+xtsV9HY~UZ*HT}rq*4vN zxH#Y{gyh^as*+wVl>@HcT9$$bkd(Bl0}(Z-rGC_?t;QG{T7qUR@QbX=C4ol2;9d}1 zZpG{uSv_`EC=LsQ+&U}I_&)vsH1Rn=5sm9KK|-2#=8gp z+FIMMlCxg6a6xMG*rEY$%nVn4sevXG6uE`Fcn@BH3zwwJ!R*n%xKOXDhxO(w*;km= z9JK}-hy%VcGd#fNiNPpNr`ArC2yx(^)6Sg7oZZLt^fBkHBcgf%nV^yiiLrpJ+8jv|uSDPpa^qzO16*-|MZ^f5NoUTL$TwL*p*oQy5k5m<-Ez9Uwqa}0 z*cEIf;*^*buow`70ws*pVAW|>tn_W?T;x=C>!|+Ton8>gQg`aS1FFI{I5unps^!&f zg@QMGL+@yYor&VXli+%pZ|JrlJ2-pDFGC{1sni=FQLDdWq*puynLNL#9xIn07b5^m z7-qDpO97LZjv7&0bqe2%M~DG?nbV=R6$u#SEOxDxv7`Xh%TocElx+(#acbm1DHPSK zF}5Noi*1aWt2kxYY2PhCpC zA!Gsw%FeYriTl>}obxzl-*UTlVo(?gqp+Y-l{VEXwTxBbT3PQU8vq_W3{7~DQGqBW zL@yRY^$OqCP=>I@@p}I~|EK@{tAG2y{ICD%|9t**{rkJSck%_?=Vm>;+Md3h-Cm-4 zJC>79qo;TS>iPQED}Au}fv<qywW)@%joKusA{gnH&I(tA z5Qur&7fXf4Sr2|m%`9~`NnRAUxta^-VE`#|@v}bOW=ob5HHccrG6c*^t8dlOxWc4N;93d+OF_HN2b~bE zk|->TuQaw*Ha~4aobtR+y-C(dQMH2g7|sM= ziEjY{Gd_|JK}lbfz9QcM1v~?tkoW-H>_>7)Z<7!9pU4f^-M>k`L$1>2;8zO_!#5Jp z9rN1#yDwh7`qR^=jL*?eq-yL4?8m2}zdK(4qJR2>`#;Px)VCztN|m{Vkh75s?bl}bnnD*1npZfOPJ#iML*H&&7B(4Pa_rAyr zn+Cx>I@n&e>p-`|&^$HR&O&QNjKV%@AF5Jq7(?4W#^AVAZClkY4UmR>cD&C(p%b=Q zS)Rn{0^y}Z>XTwOR;LuJqP3)7VA0W&1|TbB*}=+O;f$m@u%Vo&){ROOiK`Q{f-M?Z z2dba~Mf$#fxr~SK`|9!nOAUGz$rrBTHlV>Vbhj-*a|~RH7v+)^QNi$wNvhfs7&~a- zCiimS+gPGM*ryl&{i6B)6q#sw=zV6y&`w8alnbexTJbtTp$3=7T|mH>O(aYGKT#(w zvcc7+4Uxh?D^)Of>pUdl>^UL7T@cwSnj1-avuje9x7W0izh=dkqm z&eA$V!?2*7AEpWnH=Hw$?2g;M+3s!}=K&GU>^}N|qz(TITjobqRPr!7(CChN!)Q_3(F7@53 z?fLz!svg9kdSokXc03E9)qskR;pSuYQ01(SP~- z`>+1vhp#V>{0Zw{;44zC2fX=WJ8tvi2ea2}XCf2EEu~&myHp2nHJ){tm++o-KQdyl z6a?(E6z;2WOMLXlIp1gAaJF3m8O;(gYSrQw{X0XhtML?K1q-ZoUP&bzHCJk9$!Co$ zaZ1?tad)Z9(6+HgjZNEzF=}5y_5~zrBa0kaHOJ&k)`#$6oPo+UFni6n)!hQuaVfp1 zSG)a^wv8R1-V&s&QBmlux!z8CE~uXsN0lH$sWOID7?qTVc%Jr95oKW|<>8ed##y{N zyHPZ0E~F4hMeZeT9HUsHv=uMXPLwdnQLd=wl9ASyVKs(HUHV3ISlSoQn*JI8Fp);DplUn- z`8+OX9Z(a;?Rn%j`_{fCGwEIj@$|gT^S};JKO`#t2$lE%2)N*U??0_{K2gAk->q{t zUesk8kMKwM3f%!WWCREJ0Ngq6Ill!7z6Rp{7jP3_OFtog^?K*GSI4Y+FmCWw8g>YE zjbC4LfB9_OeeE+Ya>jU_2K(h(Iy()(3~o$0j|*O!kgwnX26Wu5-)m)eQ1He6Hu+xc20nkI$U(p&Zm~Ym;D+7)Ybd*zwb7>nHbI zskSxeppYmEUi-pI3!Byoy-&M?^X!n*jE?iKVb)d1b`o)Hyt~tWsZrQ>T{i8zE|NSb3>+ybc{3aCf|p$>KVbYP#*CeM{fwyv=sEIhk}` z@W<+~*z#N(q*oKW37&lnkF{irEN((oAh05>fXgaJuLqyhDxj=`q@u9JlnI=r4wS%A z7=weC!M!xLx=5G8SUx;hixI4r`h;P*RA5Em;-)iXVGgci8mJrunO=c6(MT=*xf6iB zl1L3ZXv%LNe*5v& z&&LzSM|kZ&0-88p2IY$N`V=Vh6mCq6);UjiU?*P_BX~JdBLNQN2s(aB9?{qCPq+SL zKf_PJVNAeAgOO3`rq_V@mcXaB&!x{5K~PSG=p(ghfm># zSc_yC4yx+!iw?`aMu_!-o2%h|!jiRHl6iYK{+qx21+G6Jp0Bs-@V4)-AD^DU9zyPS zzxj*&;U6eLORSoma2_>;jM;PdwWlU#;keD2+igf5Di_3KRS2=tM91{gar@YP<+;4i z^TTQ_y;VVRXjITGFBrraWVZr&72dO|%3PLvN!_dIy$`ZM!ae?PI%a zcl+4Ks2W>oU(~%;P%laV+dfFZ3ar(TSa&XVy1TPB-el#l=A5_Lr=hpL!t_i3X_0wz z#>dQ@?Vi0d1R?{IA=OG%Sm9XtL1Ep3RU75^$3$d+Nlk|T8j)i&D~?!xnZAIAMs1t+ z(iqx@?n>)C%uy*qHDqHWmlHZSRu~Je&|wigPln?ZIiiY@)h6nomE~Kk)DdG;av;4* zF^k4pLXm@#Si(NYv3epayNtc;F0`9DB6S)-AqXcm;jJ43$W~!-6G=-MUb%|Y3gVGK z^ip;5%MlaMB5`$gTB^?2?_3sFYMp*W`IJG$tkfURW2Y7?=$9mFN9SpHLEv?CrhUY# z={KFHnZq{HoprM9ZRS`i@<+L)KMxwYfp3Ug@(~blPkp}JMdX(MkEu6(k}Nx}{EoZd z`yz6wy>~Q05)uST6h+B~BWXk%lQ=S&Y;68)nQ1YTG-K3oNKOMF(ExfucXd@(Rz}2o z*WHgk_uhq`>SUfjIug`Q3L6T|C>a+ zz3SwJ=m3c`0fl2~>{-GiVgojEB{t@UEI#oX;hvjwb&kMRP!2&RnbTDTVb;pLdisdW zKqO+;PeB&dL+Zm-4!*Q9rYLHa0>cirDf&|Vk4OrEY?_{8P^9D>49KIwIi{m@hwoUG z{Atgw<){Dr$6tT1m)DxEr&YI1rZj6*GkI^HUO~pgZ+}HmGD7B}R$`baA!C51O*EAe zl+9_D)Y_n{ks_u{UPfNrPutk$*Te9T84b&`UB(+gNAC3Hq)COs^z37X0c07w@e zAOPPn--%p_z93gXzz60Da-*^k?^Hjf7;z?2ajcO#rHQ2CI(ZMSM2K7wXXab-4!J@M zc*gtzfZzbR3fj89-uhDrGv6Ni6VoFYnWv2O9GdbQzj}Hcw!EJvdxAM8zicnwCs3k* z?Qj7I7Y<4n2sjcUz`PQ;f*114I7vQ;Z|UcZbHV#)x@5xlGVs{j zFd(T$22!k9Nv)fynwc1OJ^-Xt>=XMSxzoz_?vJ{A-B?b~n$u*?`3|$%|I{*rix#dB z%7;7s;mdC?%LiLts{~Tg4Y0vfSm$E-ZhQG8-#v-DJIL&D1Tx$mEQg@28mfWjQRkdy znyQ$oOdC`LE*YzjQ$%EeedafhGN-6avx=Cc(34Z)GTrAnX`aEHbaDpZ<;k$>U!@b; zq#x|C=;6>0U5*DmEOtCB*N5w?!(mx^@4c%vwWYVFs%kn7-H}kq6xooLqB{&fJw1b- z<$tX{6p{3djPzVbxF;imi3n!0995DTNKYT(n{OFW_PUUAaEPf`vCaXlVsi)UsiDYt zusPJS(1duShBTiEzHCNM|~Kn8iIs(j6I5|3N7vA{m8Ad7MNN=8QJaS*fmhAf3okFdWJ2;!M^he-t@_?*M`;&;$pHsyt2p4qS-~q%&U!SBIm0 zg8V|ZJ3V{?F){^RfQ{$KOdlfpfN^(TUiAi8xkMolMybShAZqDUJLdt~1$_r6gIH_L z00emvZpkz9B>7&)g=4s%e4Lpli8HtwsLWdU8cPY5Wz?4}c+y@d5<1-{qWeKUc>~(i z%yiNLL{%^cN+n$ss!Wz_b=jqFuJ?0xtfHi2H!$XI<(jT%$^D5Hb2A7VNCU$3;Qhct+OJF4vanIVAhfEXNoGLN{b;<75muZFtkP*Wob)m-L$vXn{`ud&CH~vCDSd5WQd?#qm`?$d3Xdzgx7=v2x?&fGsA-s zUbxL*dWci9T&pP&5gzHDBZ5-a#*nC1cT(0QtH-S)QZ>|AeYVbKZECVH?YX6IoSQ1{ z-L9k>TG_`JwI;o3Z`!&ZOj{RibvI4*cexVl4C~6+<^-{JCunA36lD`MQGuJ3`#ekM z&8Sgh0bkX2fvZnaAS4y68EsP-V1k`PCljAHQOZCnMAE2A4WXxl=#pB%DAFXY%wdqC zegw14?mz=+B1w@VhX?_iT=dy{)s%A%T?DB*NBk59XYni5*B7W(Waf}e#%ZFA35fbl zqZ*FdOBB~HJ=2*{z&s#?P(T}nY{^Xk!Bs^>Brk#v2zcUTc9PJ+ixL|=+rljr0~ zR;2p_BYW1H}==aD{5K*ElQove(=$!dj)7I3iKFwBAKApI=IrL+) z*7vNB1?wSaeW0rs(WxvhP#Kl<$T7iZ)uhD=Wa6dlYQYesNT2GBd#riz4!Mx;oJ=JdkUqIXJnN+|%5iOemT zoN&^=i@=$J(rpwmv-Z;-BoQu?N%?zgJ58kDh0KXs{HGY;bbr~GR9c9@Zp`= zrqZN*N?PB6qQ#u@i9zQ8fLY8H5mAF_6VN0TH0f57esMs^C?thu+0rPifS__zWo1qQ zI20j700d$6eld|efh=T zT3N-QK!(VgG)G{F1hU7nnVCtoacY$ar!^|e_&pJkEJK_iLfSNa&X1%pf>XO*z|o94 zIy1ej%@Hw&hA~4T%1xxb%c0w$Yil@kTY5hnT5n75y4bRq^(IYwS81B6y_Rni^H2_} zJtZWP8uGg zBvk~~n8SntK~4`vWkaBqo3#LoQff0yQ)^Cvx+1BBg)}0nWGWRtH7LR~fvFUFwJ4N; z?#-FV^mi&Up>i-CQKG?4jhx?%&f=j$wTK8;QDBsUGDXycI$zy7k7XiEO2VXNN-N!f z)Kr6s>BTwKid`VVXb-2auWx^B zwB$xOkRmsbYe5Gt#e4%&^o8n&5x*tR;AO@H2_Qx^Fo^dmSAq^~;3|NiiQFU}$oJq| z;07GveezA}@w~l#xQHD-YnPYn<;(N?53g@Ni}VB(CNLAHq;a`yV;y>>1?F!=gBWB1 zINb!W-%HkI0aOr_>8q%h9rNAUh>PT<9$9d>Z(E+%yjE>%xi zQC6gso7EeMnJ5hu=>n^$zrsg1ElsA1m0H&lR7}k^3v14ltU#Si08#a3PF{Ag_~%C< zP-_z@-k-62gbb0>+jqB5ey8nrqjFm*Bvc6x$=sS55t@1X{P^sXAHDv~FSC|it>PeD zLM@xq&p`&4Lo0-N7^t$SrRu0PqtuDb%$#@@WwWv=&;D3aIP_7no?f9yUa;P(radv3 z>9V_epkf9rjfbYk-VbK|V9TK`U65V=Z*D5!g;pg0lr%OW5RfnX( zUw!}P-2g>T6|&$Q_bEsGAh}oz8^+< zR%oCAVxk>QMKob@+g|+aA+FD^CdZ5bHWf|n%FQ&10~yfq{P{D-a`(fVdFj{TKc%d7 zS%}Q*yks&ekQ8JYo}@xfvuH&GD$`|R7C<3oFjG8&H7*K9_!bz2C{AYP=C#?%YNt!F z-Vw)f}LyNC0-WhJ8hJ~n6v6V&4krHP5r z3TH)uP-{yucX^r#31wyUP%$2B*Y>q;!OoJ+d%ga5J{pI*i2?h=`1re6Df#55kZT&WvWfYZ00ad zijv31a{kLDN zZ@%hszB(?p+zJR@APpF`n*$t^WauIb&>y8dkOM@(4IH~Y%ZVX4fbVLsD0+Z20fCbU zHlDux?)%pdtEZ~HFuM`-_3~{nkJlfIsEI~yVpl*9UIfdFCeIERYR`6nE|>%At~kmB za|A%qLt!!Mhy>5%N(|6}2k;E6LYEB9lt_>D;_Jyzmt0rGiWpU!lRwZe8}&&3$j~!()ud zHMVsdm$7X=wumi#RA@=e2ZoWs965%E`&hU2;e1{%Ue#0-^tY~+dXOp$Qeh9%DRb7@4aRQ7cr?MJQ zFE3+#gGv)IjUDYe(QksRg}2OWK=uX}pkzh3WX*-=;cJuBWMXPM0T^{|7M|4RA+06g zufVn-b}5wEoJTr=~P1Yi6WxnIpC_w#e9H)d*4L)Q`0o{onq2{Oxal{m~cS{`BL& z`EUNKzxd$Cf3E#S1|?55^MW*ygP;*-mQ|F6WUrm|5II7QM35(Nh+K;tfonj)GxJ{U z85!Uyc{;RPg$*2y4Y3YtUtU`Kc0K?7<^7ABCqMOZuW}UYl!Swj2M5}dgim!&EnBBc z)lk^A*`!pr*C73~5&X1I!Uk!>BV+mHU>Bh&o$SHD4csuHY$YidS_ zt?-Z@WY&~%W=F6{01~Bn#Jo#FQ$sZZ)`(y--764$CfshpkyZE!13q;LmDxC-F_BcU z-o!d4cwGZ(lrnP9VpwYOUAS_^TnEpA|08!}f6j{SS2Sg&Bu3Ibws1!cSzJX*hz%(+ zzf{69e4Msz-JCNBww{OdI_5R@SlLLn%bIc4~G>R!2qyr14H!+AB13Kx6%q2w1nk^iWlxwNQs14 zSrBGm>Xbof&PRP7l38k$of*TD(y5>j3e%0|5s>ak&2f17Czlt0&TJVOG{_qMO@(67K%_I-jKKbm!X8kw6_+`IZV%?b11mMhhs8el(XiuY+Gf_+`h(7An z&Pf&Dl!!2uNZG|c%#kAiWk8z09f3$0o{UL#_N>1%Vj9CycbItc$L!erG zra-1RWB6FtV9YhCP>h;0#IYuMTIC}{X84?f(kA;Zrx;D9E2%J z<*1@UH`D8rJXdG`Bt3KL?{r4Y2@{hG!utG_12RjFv`@n+mN_I#@DPaQ6o=NkZL^L=70xr*J+TjLv8!G@W6&dA_R*G39TUr;=G=llm|Gp1 z=~*!$$}e(XKY1jPmKg90m(WjYBf*8PKx&whSd0IDg=vUQ(|W`Rkc>I zzyi5&#@ZBJc30R;R#oLp)fTgS?75Gwv%KHo2}XJW*~2tdJ%9Y!U*Lm3_vItFMfJjj zd+Zf%s+6?K{-^);`s?o>{N(e_p5#3`=!8URPdtTXuW)yd z2qvL*#}#TlG7)nu8nt*%j7;~8tpeBJ!JLlP%q%$-Aks{hF8!d#L%%w-evsa+nYONr zwQgvwx86*(S?|rP2F5yBiF=7YM+C{j7&{}~-KXbg+hT0FK8$Via8Hl$%yx(yj_=6cJdK?l>8M7f3G{K$I{)>l2ia0y$B{MN;54A;<-ppKT{m)^ai) zw<)t3BQsbSF(Xpv%EOQ)(U`k}Uk7k469tc=yx<9-9X!-EZ-{3_?D{z~qr#nQ* z3p9}~7^pTRRb-&910*G%+#Y>+?>E=a`ts`8-F>X*vu84mfYXCS2oi%i0xFqAZA@ZJ zVo+v!X)lLJ@z|MVM+}z;@w(*;g zJVwzFYCTegP-&>E8pI;m$P$F7p#(RaCY?Gc=jTKsHKK&r6?rQeL`a}{NF*78P%AAU zsy8>8eNg>mI)O`~3C%}sYi31MZAoWFWK?ET!7%Z-mm3O-M3YK1Exb}a5izrqxGNRs zDPtK-!kJ$GnmK2Zk}#VSgNhz1S_>Z0!L0<&d&~d$+as}Sy<{KZBQq0Otb*82>Ix5( zeVRFiw>h!*Vj@bV=DsnHM`tqX3}jN684hPM(mh7_#9Q(5w-q~QqX-}x9RK{5B!{@LJWeP)?7FpONgByt&h)hEOc3)9>K=JrmRi3=e zbgIZLVvSfcJl!kVJ_|88XSf1N7UInX6irhrr@Ksj{!GNEx+{0>cf@3w2sKiiUb`<* zp-v)!>_>_0@4@7{s1$xr_)lL>2^3{3sw9|NsrurXNJOHNhURiLAu@ygD&$Q|h*@%! zDVLtUAYjj_N6bZeMbTzPj9_|jo7Rwcyt6?RFmbS8S7gp|V2|z-)rvY4QWY%;`}2SK z+1N75Vx80qQcMc8Ez376|N7hezyG^`eDj-M-sqd#CuoO*S_e8XiX30L_Z3sDdL^=C zz9HXx#AU>&q(WId`2F4e52x57fG(ozIJs}44SC)B6G)gH1lPk)F}B0uRqr>M>+sVx zC~^jdLKo=meq0``iL=nTjR3&`Rv;{#+DJ8 zK_6tMNGf3gAyg?!WA)f|GNuIKNrq%CA?I#S>s2;wmE&Yqo5~Q~&jvviyn7ph%DsD3 z+{vDY$GJzJc--;>rn7WTk|rUhYrpZOA8w!d>9nqckS!5_<`iE~GGPVJrt}fQ*q|W- z6%xHSOOA2bt`7Ri=O5Y4&H3Ttq%?(CP5sZa#zQA+RhC)JY&gD$;U7h9|~wN@dtrcHZ8Gi}XGMGYoqIvsp7 zpqFKb%c>?aJY!_|mh0v*!acTa_{DvVxC}2eKd0Zb>bto+nZa8-*+BM(77&#l*&<>` zt5!h<%|a52&c&W0YSab>=Hkl{QArY-DdLb}OpDZL1j9?bBumC!9+#7_00}PckL3z$)?*6eal5w-%j&5* zxxn)U!yJ#+A~se_ZnMlvchJyF2|yK%VQ7o8*3NyI`Ec^qX{ zl<@v{?gv<8(&3EE%#rX6r+Y@gIo-L|j7w?lgo@o?VWesJ~S$1k5} zj#}lGAXIQAvIq`>E@+rStw5l$PA)5W)ok2PaWAMio< zX~dI!e7(e2s>R1_3d%r6L}jW^ge*i-U^FwTh-tS{JLc5F%+?E*nY+WUhEvt5|C6EI z1o-cPEkx3zuFa$aR1KBX?IC#g&h%tY)-7M3E{8E9eN<3c#WX5HB{)@YX?m-A$h>qN zA}C5>S0r-1I25-ppI*PXIp^gkfAs3z2@m(DaIY8%A70TZu>%jP#bTDPoDy_TAB;f; zeZ(j{`s~r>Kx38}bALcH(XLCAwrFd*)acSgjixFlX6$O+M9ox8%X=kSXK&>o&_8!SrSB2Cz;xGu?e)YHi< zQaPt&OTi+OSEvq<5+Ru?u36cAVN8j@oO_0%^zmgpWd^j)kjE5JXO223eO!vk6ux<$ zM{E(12vBBH}Jm9Hl?E~^o}u%PWzjl4$*2B)5{qyn>FE5EP_a_#_yt!9jflnO4d z19ocUs?7>y5RvmPh^jqv3jAc(rzDY#Nz5#3@P2iw*$j!EjL(XK($=n)9BVRyG4=1H zi!+fUO-!XFeK>#q-TSwvyZ`O4|NS5S-LL+W51)Si@^Ea&nGhQ(MvRMZTZ{{R z&^P*+QS&JNP~7kn@bOr`m-d?tXcsPIWi3q35y7oO>=<> z(sQGjOwwK@-bJPd({nJkh+0yGGd$@r!C+PUb60#74p@oQ`Bs#`ivYre5`w9ak}9C4 zF2CFcD%nJoVyRSw>QFHeVG&OGFT zm4V1?2_a3THEFF*$LVqnt!b5{gGlp4Q#!{XwbGO7zswlwNB(*^B3E);3@jB)mKl@PT=s%VUpDW5$1 zv+uwEzwg&yJYTMR`{2+Gxq>Sp!UexSF>TLZ$_afbu6qn}aGpC;qMqQJ`*(L={e1iG z8+rQt`bQs!#Fl=^v2v`~Hqa3zs+egd(ke7BVO7&`26xSZBsDE$rmxmOnkFKmZfYfZ zt`EWPw#9Spm+-sd7a=qhS#A;4q(MpATnN?o@OzCCQw-Xmj5e=&7lClg{Em2?S08@8 ze4uyj178o~1bXi`n@2DXt-?JuW2H6d&5G@q_r%DtinWb)&_yMA-X7@JuWnyFyTE+pr6^Aj_?}oTB4kLTiAXsD6sihH zs3z12qileIL2mRG+C9?TONQ^I;6c-cw1dHP_bu0O9}JJp-Mw_9SvhM(MVU`X zJtli{honTEaeFuir+B)cQxyV{N{mRRhp9p+szgilR8#GR90;jIjjX61Wo7Y9lV52o zem}3+rG^&BWhI%~3xY1lBF+IB@Zd@hf#syZ2q>43W@6R=cFrVC)=r*)Fk}UmRn%lo za4YX9a&rA=VG9Q8rr|{FMkN_g6YC@#i7?;Q~h8y&7 zyhT4=R=u3!GQM>A>tCP$-T(FLhj0Gl!`=f}dVXt+7wgn~B@DyIQDWV7$K*VTQ zge5P)sNs#t=%oU1g>f{!S#C}HySsO%yN8-+)vMr{Nko+7B_l8b?zO~4DR{Ex3srwh zU0k|jECh-249T@+}cq={a`)hw>CkM!{H zjNxM)zIm*XYm6;00)v?`Gf(#5(>WhWOdDWM9!?50s6qopN#-1AOJS%u06|!SEV^@AE#)_kbe8HtT@8<%5%(<-W&H7NAXvrZ;g^7+J$p&K%PM!tD)iA}U6L zk1_NWkqUVAy!EZf^cWO7X=W=`PZDKek;-dRF@zzlNJizIH<(#MYOh?nT#AC?dk%KJ zaW!e4n*S)J3~fie)dWJ6Wk!NdB`gIs8?Vfks&RDhFl7f#PoR?h<-hr3-%?V;U7AsX zX-JH$_`Qhqb@S~qGF#I_Cbpm|BYj-#bboE;PoMaeZU^Az$?^U2{@?!QAO4U3{onok z|M{DnCoiAA{A5TLAKQbfHvs}^cGTsmN?5KlTM6Y(YZR)=(i&nN=Wk;3pMCOiUjV3R z6T5*v6>&01~0?i0mjq)+Fzcf-MpkxuqPAjkb&}@9yuoRj^-` zNlZsh8U|uSjM^d~9WtA{DLg3L@*W~LP0H3(tm$HA)_QNPTU+|lReJADwGtXs1w_t& zR6xw;qLw*jsUn2$1Jar| z$KKxwEZLGQ=w=~n=k$*>Ihb8rd*0h~)fdob&7P@T6W4+(J8s7%4GXN;U-v&i{J5Sb%55mc#_5XQQDxkr;tM;+;zT5Iy8L=mWb zZ^naT&W2?mJ!Bp8r>zn2EWpc)Lqr7t7rvleUX@0PQb$uOHc?_iJNKGUq9g%eY6-lQ z_K07SRAq@nt2XztF$$=flbbmc=^qo}YP3*~MX8zK$e0a~WTrfhbmydP=6ncf#Z4+F zS~2Y*oKzP95h+yIU;eLuP#(Z_-41Q3F^xw#Pb7oE+#@mD{EgI@RBNiFk8M+^_v03q zldgJtczs$QHr4M=m@9Z+x4%25({n-QWL+k#;9wKV8hc^K=xRx3Q zWu_oM7JJs8Z!wb5jDPYBpWWQ9X(8;^7SjpemKK2@9LJkKc|RUDcd&x5$PA=t)l#Ac zX+n`|(`)HCN1zZhn9j_E;sVdklBp%W*zVUny~WK#dvcZM!@8}(v(lR`pfH1KAy)Cg zWcGG#dX;g>NYf^86&J9im)iVr=&<(g?fY>$74lgGwcp6hxjm0vIT)nZJeEo__4l*k z+C(;mn02$(Y-!C7tsQz_T08c3XjU1MR@O<*#DeMY(TOF+u3}LFQz26~S4<3h)|r_; zGQ#6Bw$*cu+}ziZo-#5cF>-G52!IyV%$92|CCC|iK?Vi1Or6W00-8;D6SZC0;e(}r zdeuHy+6S#aTiVUiZhO0K?MCcI?MUq^yHa#7Cu+E&5&|BJ0C4t>+UUIgMl`i{C-kH8RdDNmx|@3}i$d=QD{h9iHioV2+Y) z?pCDQ804g5BM?&>R2NQpJK;4qt66oa4oJ=Fqq53bVN|>%JOiYqvQ$$hisIhY?7ZwI zIuqC6IT7tKS9*o?NH~#@k#zx9(rZ43&FpEx6`NS)Wl3=B#4MVD?d1|=x-FR$`^$g* z$J-c}ZHpAIw3~`^qar19inBx&*41iG*LMFo1)0e8%--a9NN#+dw-2%gpa(lIarfq% zKluDV@5jFR0G7ys*dPbfril7%VD7vcB2;fgyJ|T-T3=o~oW6N?e*2RT|47?`5S4?r z<9he=FMl|@E+)^P-aJ3vG?J59SJpTRB!JOGU%&b4^}9QW=6X8+uv56@qXllpKg6`Qt-gAnN|wjX9xk&Md!BQ%*?v*w9p^D%mh z<5honSkLd@7a)c+IW0?&5&=ngFhiytWw-6m0#ho48e(d#S!+u#r=+U2-dZ!O|HoRl zwwN`8NzFGa2}@WGS)o>c%<9GF%Ztc@AVw+eJ$+0?9Mms>dWH0MwMNgBc0(ZtRDnwE>MM8xlsZ-&$Ax5%HSJTo`IwWSmS@fuS@eBT3 zD3B>&n^3{3&g)N>%7)H~JT39!#5W)q`%sXzfSy9mIkzZI^IT^}#$&q^h4F@Cj<{p? z@=S;bWw$G(XEK#3%tAiTWYaq7X9oJC=0~R$f~at51mY1QGgT*hlyM}yifyrS@bmMk-8Dg0O$_Z$3|#@EM?>w4AO!4oPP=3e^N^Y711G z&T!(&%YmnC^syZI^n)u&xqEnc6Q|rU%5Tp1ad-du=YJ+}XPM{2> z^bdda)z@!VD9j`7BOXq9S$PR-T;+5(4kn^ab!kmf$jvDfYg!CiE4A3X-T_go%{5Ey z2X)8$cN-qokAEcn@a*AUm8LRafcDxFidhgMoydrd4wXhpRTt6TTT=}ua`7>)Z(Cx# z`{5zh%kEYpQiF7#Pf^q!l#(&?8(IlORLsPhSkq(c);gPrs+yQ-Gt;J4F*YU@_oJc^ zqo`;J=7-l7R-koF%tuiEf2!WBSGVj+(;D4eYkke`Pv1miJHn1gr=8^llA=gw6;g2l zB0&NnZjgXGxIp3u@Ppuj3y`=$0tpaBN(q&%EA4bdIGWS%v-j7pX3jAN7h`@q3Mr(J z!j3rS?7hFW<{aaFpGRYh5gs)&^HL&f7-kB~Ln(dHsn}Q|3T~rW3G#9E;Tkw&gCRIdjLD z+(E6_vB*cqiaJugqXK~-5IT|)22CWo+mYSzc%qpHiQdf(G&+Z-f!KEhfTCtjY6x1l zbXa%d#Z+UyV(RtrXEnfeOjD^HBVr1k!K@Ahj+Gks5-2(=7S3ObZ$*iXg;jdaF{25H zj%KE)=DEtYJh(p;cDZ}tpV2x zI3PnJV04xmcp0H^`NEI>h`v`r%Ty# zQg;ezCpfxiKtR-+n{PI&8}QC2hTS5uaEHD$nYC8d1_c)vBn0DL);A}7F%qq(y%(^f zW!xba#e_$3FhK*Qd9`=|03ZNKL_t)*MMduaROb)ZyEgzSXDrPFKKXpL+uZE7+XLK{I8A6hVnE*BG*fxoovI4r~aWWluoBg;Qv$`3@ zQ^Sc^Tv#ZI`ACE$M8wP_!bE@qOaMSe5k_JJayKi*T#sn(+JudPBN$lBQk4q>RnL>6)MME#npcFNkI0G@XLZ{4E zOsA6OZJIH6!kutO(lDpQ4I!h~Pa!Y^5F-PyQE0dvRLKpo0=tAr5)z}-NGdpE;4A^D zJ_wHy{D>n)$jG(cua5^w2Q_n7bvi!a!li0fy(z`4PBS2I08v}c?iQ2-t@+_Nyhh~* z)U7BK1U(waJX#OOjAtYS;)kq z)=Xn}Ls6kB3Kmk#p+IpBe^zyFkQWCrSE5CyyEVSp-9CBp zcsKU%H|t`+j$l+2|M$QAKQ8Y*I=_Eznv=}J5?As@%RwE8#H?3m)xA4e0g(n=%%`z< z0l2#Q=^@|#?dxBiEzduG`nf^^bvID3v&(x{zI}D`#dr46ldJ1y7C!pWdNcqE4n7sj zhDq0NzmuCcSD*g~_SwO-Da7dG8Wb%-wp;`VZ$4^Hz1MHc7D*|fJM4%IXnd?gg7{p! z+mfejvvk&vCzC@dK5OXmUWf8zy}jv+4Ou_9eE#!Ketdqq7$<%4`m3Vjem9IrJ0lYE ztfShPZAOZZpCsJ3U*GnRnXYI%)N1Qq^N$?se zG`&!cUKVjOupGyQdK?U@6Bfi)hzXelgEUD=P#BR=kswGL%tQ>t3Q~c&NHsc=14)S; zc(I}c3NRIr20XbXbY>W+Fkw?FxXv(u;8vW91_f>!OveJnAgh%E*)eOFgX63sW`JNG z9B*(fRL6mU<^<+$2~pJ1ohqRZ8GVj?=Cn*S6Yf|V!4!$;CMBi@T}>4@Y7s^NKw?Oe zpmEcbwTwtpEeT0x^hC@@x;i2PyP#L240-g9B9^Gm>K;g4RU?N2&c{qaNX5yl0qg@F zMbt1EF$p!qLKIrZxGrPmTp&fW3N$8t2%yNIIlyZP-H?dgphi0xp@0Tc$iy2|iaWIAahHqPbTM8l$5DA*zRb>9|!7Fq$1f#pVXczx)qAi6RGp zmtqJQ2koGR5MTj!Lnd@Iq)Lmd@MH%BK?6$Z(KGS9|K^*+emh;BKTt24r>t%SuD&nR zZuREb!%tJ2vi4R+Fe5~BE#-h3H%%2_rqgVhB)`2`jiWtXrjdDcGxrJI+{*UNS1-T%TP;ZHtC#DG&wl>wM~`?pA66S~ z!AtMv$S`ehl@8t(YebkO(~HAyG$=se&H!_nAy8%Z5uBIuBg~!q?KGb)FTQ*IemA77 zsTH|@-i;Yw-L6-g@yCxo{zw1d^F+fq?#=r1WmEFuUwrj)Ff@l!AQvSxpZtRd51v0; zUf-@?@1bwz1?^^cxY-TE?Pxp3+Cv^07R8`$xKbljWi;{sbOXkLP6rVe*+RDISXLaPD$Je5ESn=GN34q#bAhs zLEVcPR<}^Nc1_jFWI30Tvya8bG7VWLEr)Dl_EKOdUeq&qRzJ@7$ULgw2woYQrl{^v z&^&Yn6;^aK#RUPI7(3>ZB$sWvXwpnX5+~u7McULf)G|`lfgHOHf$1d_cTiIYESAZ$ z!-$Z9j2MK8n23PLn1BcvB6>*#kPw9c1rdR~;yj|bsYftm^lzY&=Ng(J)Oh6~v%fJ01x)-S z#Oz3_!`zWIEDVfB# z%e$MC^9z1(HN1JD{ib+k%Ba zXoL2TKcr_%diwNgvwi=UUypCC&FI6JSBL4)kHc7o?AaaJ+e8vSc*xy+zHFo+V(=Wo zMT4wIW`;t}hL%t6EkNee=#Gp$bK^XYpj!~tO0Jcowq6*DLslRpVkknBI1nnLb=*w` zkeYsXC^^RtG^*w9r~xkUqkEW`1e-*ODW#NB;v~!>!h|HkB#3yFgELX}coQNT#WA7Y zm4m-X2*q=7BSR3QZW{U{_iUC`bFoQnDmoUMj(-is#^RHDE}-g@c~&qda3}#Xtvk`s zsD-#Dv}xo(;9NzSMlmE)fCAylYemjdbJ3*JMmph!xnbspnph$iGeF9qh^QeqwYw`2#ls{rLBPjD@L^=C;@j* zi`X4kZgZ6dIuJ4hHxh}e0?n&I!efwb=y%kF$Sws}6#xmykg9ToNRE*pK-XGIzkBPD zq1`#?2t~Y&wG~>3J`v3Clp!@6L>Bg0tY1lp#}q{v9>L>OTj4j~cRz z6flDM+-5V|?+{g|LmB%rjnuTKXBQ`Cqm@aR6ztvY>(=1$laG=4D5FsTLlB`82Pj(H zoP59vsm_GNymV0XB7APs_-^&P)nVIuxjH#3I)G>J$<5{`OR~-D_j|)qKxJ~XE z-A@)L@4x%1f8TFk-8RE4A7K0HmF2=Ra00`wWH3B8bTr)^^zS!^uZL;K5FHWWG~;xN zy~qzocmU=4DR3(%uX>!H|7f$>o-MldZrJY~Pz2!l!`WnZyPck$pFh58RC~~%9qw7g zZ7{{(y<8iTDD>SQBBKttxDol^g&y z0?RQ2utJOmx!7nG_fR#HvFOME*}&9QZ3;wuv0R|8%L}N&q~;nt8!{)Y&KCe>cd99H zsLOPBG$eEKBO5wep9=BVBchaO-ZW=zI!m%-ZiHKA77>w#sX-Kms4CWa86j0G4;~w| zplDMm1K3_jg&+|y#k$-R0x~5EIZrsu0)N1FA8QI9a6Gl5@pI%~#Gx^m8C75^RlU1susGQX=dE7U)c@J%#`)In-o3lOX~J!u}{>0x+}?(1LTsqDnOt5rM>8jB0c( zs%cC|OjDeS35gL9ImifloSxSlmxCMfFaPutVNlIz?5^mfj=;bKT1-e9Zpi^v5duXm zvHABJ-Xuo2xbR%y{Tober*0*?Wtn(TZ*l5=or&j|hC$m$D|OQY_^?dvc5ex*LDn^!PT zg0>WS)z5Z?FHb)^tlnSrZ;Y1TY>G|g;iC`Av>Oc9n~5ob;iG#528Z4B=>El`GqbF* zn6)fvR;JZh_^a>U12Q1yDS!OQPh_5d_j>c&S7Ww1wuVx)6wq333#p=O0Wc-*$amWB zO6g1PM=FYwmKxzJW@@8p(Q-KCX+J)B+}?j8?s7N~LSjmsTGxZxUJ{U}ViE`%Mn^C$ z0}&Xs0itDbMnW`IMe=TbI`p~U?ybh}ybj302~eB0TO~yIOmQB%~AtkYC6`ENyTwfafr0Js+sxZp4Cdcb<9+~C`NCxctk4#dRe2+ zHAZSq^%tPxc+4PD-IxSi4I)DCgJtDIx zc)Y?^hHt3YkFx`U@TVpS2~iC*IzW`_Fwmi4$j0wc!R|rCBM5CVLHMzVDf-9!eO9b) z3JLgbB^~oUfdnA#9*<)OKtr>;LaBQQ-Qugr4#dDnOh!Ot$15=uu*$cshA243@acGJ zM|7a$o}S`T1RUHZ3Y!-pA^TATtboxzFmO{*LwDyt{&zk^oM#<@+0`{GBD=X68=1Km z$R+#+wdko3Ed@6~4)DR%Lt5QJf3TsS=F1eE88ts_{N`GJ_~DZefAFl&{WSE1etQ`H z{;R)OFrA*Bx$ol7iO62bG1);Gg`nYd!9a#i)Cxl``$V(T#gDeb+hMw0@2*c5=d(6} zTihrA-~aJ{=+b#}0-GWC`Geb+Unl!|`|gX^fB!dcfBm;j*T8KBA zOLH(PMyF0oCwe!&*;#QTH1_(_j%qTEsh?)IJ3XB(y7>n;W3u^2Z&%Z+_uI!;=UtP) z#`mj1F)6{rQ%a2wV>vxZO|ri*IkUp-$zGK7aPn6S>`O{_5{G>mlA| z>Yk(0k^}4Kpzf-cRY$j(@G$l3GVY3o-*G<_BbuGG%xsa8Q8iGRF{#^dd-0G>&V69h z?56I5Jaf(LW7FXTVjxt%2WCz|ya%LExT;PL9uD-Tp=RE#*PC(}VpbDNfIGrT93BJZ z5Qi@o7Dg5TVHQG)!mv36Cl{RRN7vE$d&6~nbPd1_R4Esm>Nyjuv8Tqj@%ZQrUT&ch zr8%qV!VIoP=In;9sDAfx1dV41@>H+qUPo{ty*tu)zHhOBGl_85@(Ign*PMuSLc$W) za{@|4a#Z`^-R~3cQvS8!XlkR`P|Du*fL>U_I%Nz_h=Q012|5cs`Uz}-^Dnj zk^~u^gA5H$F(RcPJyk7yh2R8K9uS4F0Yh;@l)jyATNMnoQ8o2K|DyRm8Xf3 zmek6%t+A5$r2&iyfaqSV9XYYN6F{I(&Mt7afcLL+DF)`Hz-%sR#j=3Q zFxYor@6S)?Kl|*Xv$L~OH22e__|5e|Wc>!9l$g9jY_iO=yYKCSs9!%w|oe z@OUlemx-@#H^19&-VE=r&n_M;yV=c`fA+=y^*;?mnV+6~^utHD@4ncV$=i=!eeB5`X`AL|)4#p#VLCj2e8mKBu6L?Vj%PDk&IN%MGZ7}M=rkZE@Xxmz|ZrsARMb1>IZ0ZE5(To3tBrm>V< zEElDQuFkqelg>|PS7+U#!~6HsAN=6*MJOd_;2NSi zRw>C4jyJ@(WYyU{c_1iJ9gQRF$6!!gbIsEoj|LuHp+N2`MSxJv)g%86dIX3YupS{e z3P+_Vu-6t!h)8NS$DG`VJSc#Er)7_R3Q`j8L{3vWX=RqUMNY^PjZh*^K#0yrcamJg zYNfP#Ifvq<=xAjq`QWxkm=XiAp&Nk{y9jUvv!ci9HNd;Ed#M+5*ILZ-K>BC}S70gb zMv-kt0zpbttu}~EmnI&4ek`jW{8SM1*H?A6A}^%a9FaD!mo4$HbxjL zI!Jz`EV>g=98Hn^h(`n>0wkg+>TpQggEA4C%xXZ6=Or4T0)!Ay-TA-yZ~x1aM6DrC zc*qkvT+J7M^hdvF=C-?C9dgxsA`(h~hJ$Har@-d+7oc!lDYshzXs9M>T) z_WpJ6;2%6YeR$Db|Mq764LJMz!`s#EyVLpN^yD64hdv|f0FXU18loT~b0Q$~$sHIN zfQsjf`D_8xo85QYem@c3yS$p-z5epg|HgKk{{5G!Ne`dgfBEfSuHJlE_B5^sNW`)R zN{a{gk$RpZcL~~sw=D=OiE2^L+56RSQ_u*hD0Nx>Rq49YyqKnft($0(%|UV6KYx6$ z5j*t5o9o?bpU+x&aPMTj+io_6QBtzYb0oAjB_b#x0(B=s)q~qOD*pDHT|@ZGKm2%> z_KxMRe|>#>s7I`7*a^!`pz6qvA`B<CR4? z51%~#(ZxjvJ}A7o*?HQg`Ci(>%%zArBC?~nXSV}Fk<IL2UN!m(di zeMuEX5yvn_M3R(}$Sm~6i{)G`w z0-Z@NbsXzJg+Py?=r9Y{c{!mV3L=rmYK;u*qcz%2R6%h^^j}y*BCL)f{Xt-GsZlqK zQ8N)Rax9tRcm2J8*3qg>$V?T?`M>(F|MTVf{d@PGE(AMS-n;nl*^^H$A3s2ue7m~d zOjfsf4oFgqz*teti1V&JyTI4q^?8Kj;Mi2v0GD&tJgR{qn9}vDu}t~oT-)UcJW zef;Fr`u(>rziNRVUOgvH#S6=fcPskhT{6bAvjFg^2Qbmya&f%h^pEBSfY-xnJN9K9 zF6Ix27LG0#wm-aXrOn&7ee&Jw-`xK8?d;TnN5(3tNN5xE%->50os&4S~RC*Hh+9}`AO5<>wU4wyxR5K+gn-g zPOrw(%g%M4dkzmMVME|x8FY|l29nItFu4`LOpqPbRAVWq&W@hQ!KeA+M4IN^>vz$- zkZ_z5a$I`|V&{VloQP16LMjMA&Wse>f&f9)N>D7KV#I00z~Dv%h)nKO05hXA3j=YT z<~to9e{jSGJAzvP@~Jl9V-?^4KniijfzW`5oDr%b;+TQ~M}|A08G};=xT8?)dXmT@ zaVO0prLN_ML&45W#9mk2=rvJ(d_+_VDwwzf!Z+t$J%O@w#o=(Gh9J51tdo%9#^?m|JzdV))3fsjpN+%15of}wxK8ESz2(LE35&mbw_oq=FlvM4+2ixozV8Qg^Q%+o z8e~o}EhiEJK-8=UB-|cKf0+N`^FM55sW^XoyZ!Z7Zy|-#``Be8CkJ$YHcLOt+x>OF zYLeBY3RV!h9V?#ZW66^#U_n5X<^1yVdl%2UcDYwSWLqD0*RNNmr90KdqQSiE_sL?y zLT>eDrq4Lx$>Ml#bOiuS^?2GQf%5Byve~JQCEf356xJ;?^;GZxqK#bOfVd z=vb{H5&$wHkQYF$`ECF}CQOJ4F)_BtEn{Nrm=Hk7nV3bGh)5cqi_DVDI_X$ggrxrA z0@e`E%yOsXj)#D`W>eMT z>qNq$g#&kmAawwc7++GnvswZnuLd**5+oF4p(GO5R7M6CazWs#JP+MBR>@d6105(H zQ5sQn^P0_#?ew+0QUvZ(E9l|hA*gb!fPK(r(#TqfUxG>l(=8dffM$Lw++o`0x?UKjf3oT%P3B*EFt2E&F%h zff>#mu*~nx4dLFCSy~`z(Dl*1H?7v~?2 zhgHsFDR|P+=RdqaEQBNty?MPR#@~AYS4+WW_Ud{liYIeBKSjXKg9wBSNb1=vGtXYW zU4QWK2lp;7+#D$xH!ohi$mT>9gb;iL11RQH?D=K)ruP+8KGjiDi)a|7U^7&;GHLdr zMD5Ar=l6biy1a)_w&USuzj=3iz1tnmmhJ3>K{}wz{m`f-^oVFACqY0n&r>d#B~eQ4 zqyWx|=L9Y+F+MUl&FVmdAQkkp`=|TWZofV_IUq3sa%6}Bh=iF5m@$wX%qWTKfsBxa zIPjRnghcE?LaP5dlOT$?FcKDq$-!!H36K~OS-i?s!6^X7UhnecP7q%)Bcmz;tsB`m z1;m#S5Q*6#F(rXUpcCp6cMZ;jT7ksW2)05AF%dPA5^|!@3C~4l+%%FJVG2P9;+-y` z`k5oTSXpxF>RNm%xgW;el(!{sN;x28siYYLaAp*CqH1}>5G*oQV&T!cXc*Cc2uzXI z;Tbr9I%ugWMhG5(UPa!V!+O)>_|+@sAw972{wZ zn3-tE0=r9Fb1GF8Mw!vqr_Xsv5>?ulYvo59SeQM z!{o6503ZNKL_t(f1jqXAhWcbCj{t-~d%t*Am;eE-|Wvk7m0cYQnhy|b-Q`#61)x}|wj$|$%c9M#L@W$d$Sb1o0(vn&1w z5Bv2uZ^ke8fAQk<>IZ-qUH8+byiRDH=gG* zFrGbo@Z%32Arl$c(A#dm&jcUbyV`AEU)^t>KKr?U_nSQ4{{E-;x;B?7BeYMhnxFsV z{>|&xPtU=o@&4lM_dh!M^1E@hFHd9J?Cj+3iAd4WVP*s!%^p3ZoL8nv-NYRTXu_Iy z1T<5OPsilxYU!Une|P&Uo5KDTR45ErVtjrgh?5DY&BVMpzufm~J`LNU-)i3L-UUDT zTL05MhjcmJk@W5eS4#QM(aWb2_))8&S{b*v| z1iyXW&42F?fBfhB{b4hN0-GE$z{^Zkb`+Hw0;wNSVvYp?)I%=bjU%+mvpI%pgg}O+ zxVdA?^@Ha~9EnFlhv21CB%l$=juSUmFlYZB>{mYup)GSn^ve8Bi3Fi#Y|s&j5v9`8 z*xVG*6;xQGB?IEbD4x^!pF{u}gd2;At-70O&Ng};J@=Xi@S>m@ zfDzD?#7s$zB``iQTce@K3dfU*^oYa(qL1%zwNT!idbBrRTAAg_VH78)v1=eHu zD0I=0T&l1mU;;s;_=y7$w+sov)Z&ZajAY=6JQE2B|Kg9I_Tz9E$NjL~4(rY8?X0;x z=ybb!nTLZBq&9u@;}7y-v)*b}H1{|bNW+gF%yKE~6^4k$;0|&})yDyz$Fex<7_$#0 zkkSt2&8uD0mBrk|vfJd=t7T$(aDMOV>QOIT8tjK@cNo9B`JL)ICG)AsNK5xd2Bosw z-A;WAbb_*!v!@^_QOVOd6&iMH$UD`XC(%Q}{&uk(PZuzb!>gBvPk!-~akMF0E{K!& zqm9Px#o5J&Cv%@mY1*z$v&CY*Jn5P)HI!4wWgI*4^Q#9Ji^aVYZ*5Yy;^0PSXYJ); zY$+=*0r32!y?=>1m;^zKDLR=>XJ;4gPzt&iX8!i=w_m+{TQhgnMQMhR zvz{$x|M(yM^s6tw{N~*hlv*<{Ue6@Q6F)LmMm8!8xp+gT^ZQcD^~21v8Y(1L{#BM7}ue$lTK2TNQ6?N#3anju@|jyLywvpBO}LF71)5)J-e3} ztTAwuH|nMVj_Gf#ZRuJV2Q~~R51~_W2PcE5C?X(N5iL^^oCz!%o+W6L%!TJfEm23@ zASK2IQ3%6dkyu0|QIj|k3sEE72nr|WSR{laGvd>L0a9@QGgHR`P_*QHDARVz`*FI> zc?*^?RKEy8>SaPOeYkpdl4OA=U?#BD$b5jYZ>Q11bN zPKXJe*~#jdB|@wg^&bZ_EA^xqB_4|L4LB*F2>}s1I@MG{Od<@A8m8lSp%SZOw{D1x*)$>rI-`=@6!U>y4C-R=9g?|*mho2O@K9MhN+q+ZLQ z2fbLm#OpVk_1cG9+1;k*WcI;*<{>4MB`PT|Phd8m#_{^q=J0;#NBiN=9ut*d-5Urs zM7gv%VSbz6e+~0{fXSnRZ0>-@4B$Y;4A5Y?=+f@1Gz}9aH2@%2ZPxJ9W zo{%d!w4fVN7o2!?cOyf}PUIj7oY0C9AmYU2!XpW*H|`n16E{~6?_a;W9``-oO)8l{ z0636#oEl0EOOv>vMmPv?07ilkV?eHjlK3&NxC(Lv4M<_I#R$T8fX5;rgA3NHAHfmS zNY3U^i+WH)D|B3rDw9Aht|X$4Y2MI6xNB%G+zK^}4Phckh>VbcBc~%NwcI3OB4z}R z$P7!0Y$gdud?0|3Rci0#bN^k+0Ywrbn?aO4LKF_g9zQ-Qp2u9kf!r+X?fZP&k2^a|c}Re%!Zl5u zT5irx&hK5Go7sB38wX|2&mS$8R~;(bG=P@j)vbQSyUVH_s8#%dDj(B+}9mb-gd-%8x=6gX< z?IwO`>AnML*_iEsT1b~gr))id112IQ7DSZx0#VQrd;lm!DBKKF?#GdNy1sq?SAX|% zpNW{Wfoe>JtrRMX#54XF` zyX~9n_1nX6FoLIN7c-t-?hh}9v+8Tp6{9=Epf55CR|7<5J_1)v*zI8gi1l<|XU|y9SrB))8uj zTgHYs5fRn8GF2i(4eAphF-3t7fGEoLC`=LqzepNbL?*n#&CNqr)R(-Sru8_j$6;-G z@0uMZ(BkOsle;ls!n%w$Fvn_U2Me<_RIrMP7E%>oe^kEug&k8`6kgGiiLL5RE532`h}5)xAt z>cy!IfCZZRBM|7~+69f97GmWfgM%SiTuF!{pYKG*=C?MKOv246>rX%W@iZLXUGEPQAsQ3_=wd`1kq&@3tV@imSkM4-Dcvat{Z2yu zVmNN0AMJp8GaSZoXt^+XH5r4EG#8f_XXmHu?fQDVe{;LNI_1+dFWzIGQt5AQtb6w9 z`N_=J2b@-V77S}>2T=HhSmq#mfeJ#sSt@96mu^+=5o@>KmE5p8xOaC_UGU1wO$mWf!fgH8e}sWT&WKs5Kstyh>a7(gK}&!1fM<8F1csmy&rW=6zk?`1Igc^K_ zhcgM(-Gm5s374(RgyuqBqK2tqVj@n+5k-k}mvih=7=&vw1rQ_{PqB*P_&f$Q1C6V= zCiIS3Z7jK;rrnS?hjBX&>qFjTAJnFznbgg-SdI;dp;y#S=y&}H%z&`@Xfbtl1M((42S;W{cXMS^h+eNwn)8@7Z=k`&vipZW{j7O%ar64+>UNq6xFQoeV=+K# z+(0BD#*{U}yd<0$SPM=gon9?$EK|`~6fnX#ASh7s{h&Adah#@xoS|5ZC774XlZW>o z_j!8z{zi*mU7lecd9yDs-nvZ(*XysoYfoD`ZEe;8%i{cTnzg0Bb(qvLpqh`S1*pY# zM#*?4zj^WYH}CxgJUPEeNgOu2vX|G{DR?*IH}zuS%|%&s-L>y@5TVSyQpfWR@w&zn1A%`m%FS8X@|E$2-h zSN$+=o~V8>X#*}ugay>ViGdg}9y-7djxK z;Bx16IS#B49T*7=k21PC-vUI9V+SI5=w6r@yv`^g5iJ_-gytk&f|e;E1@4I;^qdjR z0Vbr_|HHAlRL{LNiAj)aTF}kCAQW-~pVW%#n6;mB-%q=KT94yR9#(nWxMnSr>SU${ z8siy{&b5P+Gc#x~fZbhHb5?Z?u8t`|F&!O>#YNozC95V@aSysMHHkzLBS{=DW2a4& zkQxRNl331B5)x$Os4UX)kBWd~L=Z)qYHuWr)^mMFa7J_%O2l1~h7$qQ7&_f$uuj}A^~KrSSX;8?|KMg~xjN?ZXLY7wG7UV*r}(fIBp5yy_0 z1tdTrPsE*wND>l35hTKj>7oj4;v+(eAY6)u4h2B@2mi`tQO~*LaTtcd>(v7RKmY`( zfNuA@amv}oMcY1k{_*Dh%iFi>>upkM0Vra&$TPy0h!Mzu5E5WgLJ%&Fgi@_rh!;K&T>}nb~eXZu$W*CoGjr<<_M6(|aGJuANr9hl@o=L%w~tUU~EQNm^cY zk1pDDdwzPaJAF8g*PZoeypv82QvsXIiaR?pU^2IU*z9(kukr1*%$I#rG_Tdw3=+ms0--;EWy7L6fCKT2($dYq3kj=^h(rd6 z)VB8k#L6bRH^Xo<^qbT$SSh(!0xkO6H;1^Jk@*ilxccQUKl$$4-~F%u?CZ@K(3>#v zfJRRUz}$a!fBwm-zwccU0iZZInAAv6H6#Tj$Pv)W>FMSC?DrX;HtpjP=h@f+0FyEm zod9YJH(IO+h-B_=AX%fn3{0He5U0eA7IvH(!E8pvijAwFPp4;1TBO_S6_TfvW^HO( zlK+pXH~F>fI@0~Vh*)dyeY)Fc!=4l+iWEzoRaLO@Tn+q;=Ye6nfai%P9{E2s40vL| zcxBiHm8#KGuc9c4%w%@=X5Qv>d#@D{9>h9H0fHu?q|AHn*?X;s@B94{bxFFODUk_D z3!59k2wJ@oTu~xH0xPgdQQRlDNC!l;n>|DtXy3qga0w7Y12qASB_9!)2xdDR1!Mud zU^ugfBapby(r20r4;{~P%3x*^n7|Y9pLF7*qD)MQnZPClR*tMW!t;O%Ox7wrT6Hul z9-@u1tGb`+Z5?mR{&pJIW7!nlSF6lL=^a}lDQXm1;}Ws zHT;pK<=q=TJ7`?SzPnoYUKbBn2A#Uuq-)hRfMQUqV$8R9E*)rsZPfNv zjhvHYINUW!a??JqlH|mhTu~wu7!vo1R-MdK$^o}!Fsv0I8|o+GEP?c5Vm=^= zElhMuO{ztxuu*-j7~Ly;RNractL?1ZP2+7D*Sm2u*6movYLn?`I+@qeOGw&7I-w!n z@wi(zxp;9cZsu0qRbkaj^-_!!Rh#NNAZ~QnLaT$=vWPH=Bt{b96sTvB6#t&$j=`CG zkb%V4pF2W&`|Mg4FP(zCK-019X7bgVGBf=i6zF?k^&AK zuUQZAV1Q5JuX~`Zy48k%hrIi}BcrAoCXg#ity3)o7iT3WAmQ)>a6P+NUft++Jyhyc zM^=y|;v81IoDgX=;^aV*PNZi~jwF=e>?Fm+j8Ym>H*=CCT^32i8$qf{qsl22B( zR)A4Mn)AYvK$zOc`qhXaldv^0|=U|pV zYB&5O5tC6wZ^#WZ!Z$zt%lXO2WIarP%rbE5D-3QN*9n+gB{YzcySo>VFwyE_2FhBP z2%!ovBuYx8Y?Ba-5}XosMVX10a~b+PZ8tN)a!7qILoZz?U6*sp!oh~8CMG;2`QbQh zebLsZ2DC~PTWfyDGn5*htXX5&YB+3(%v?j%8@^4mP|Zzoq6`?EI}=Af)S3zqiIF3>o>F2F zK@#pcob!;>F6x<+NXOyj;2>Va9y^GQ+Da8{E$K?pdWVG)3eM~-o>L3(NUKr?|GkZ{ zU-!@v7j^^&IJl@5LR7bjths<3#)o;nvw2`Y(fnUB7E z1P4u%7jv87>VKt5b0!qy)m`uI9uf>?9PIp`>oE>uG$v z+dbJ&n)6wiwp#aQ29lJQM`v$+_~FBzH^2JLAAa@wtJ{s65IcqFIXE5WUdpuUyZ`b> zpZ?(c?~J!sKmNs|N7rgV=0AB+9xChP2V{ZTy$2@`KK?Z4lW{BN;*^OqI0Zk#ofzU) z39N2p&P>MYR)yh8eaGEAm#w>1j-VSv!2OUxeJxvvI+);UM5aCa@$sy>r7{gUcM(0w zDJSW2N|J;Edqz$O40yYs;ZKBNQ5_*_2(`wJY-DmiSkPNHr9C4LK%C}YZh=K@qtFJ@ z^@C4bfKD*vG)p|BMu89_Hb+1)T7CizN=fY@9w#Mh(4Tlfw>w=FPq-D9Ref?Ts#CF@ z+P>7?SoWpts*SZyUW5EuRJE9?HUZ6n9#1G<%(R*wupG5I%*_Hd-Kcl(-cUzSdj47uP|>b8FEyOM`d8r8Wq&+fJi5HVoRZdVkU+# zheJGw7({Ml?c``Q%1$mHiU`EYzFTxebc{ZCZ zrCXR12)%Txw#C3Um{Jjs}c>mtT(Dg6wUaA%*=9JG@55||5 zpZ}-7Pbt6g!~alkchu$G&Esy6a{Wi}4E>4wnz=(+_p=A8tmDRg_Vh|5FTV3Z%3K{n z+z};Eb~&o+-@EQmK@*IOXY7@2bFtXv6RGa6T7@s85sP`gXd#*}Gd7 z=4K6Lvk|&OE#vO_e*1L4f4Q4BMVCi=bv4ciU&vCs;8>)Hjf)ldtA0lEL`2Y0umg2K^|Y| z=t{b|Hl`67LE+GtS-su}FoeC!+zW*^Ix~qg$bwwYk$(wIj0YijG&lA}p%5mHB!grT$}CwVvOB`B;{ojv)H0r}?-)Rk=F5l>m$lbY zJlQo0ECeH}Nr+Qi|D{cy;~Py@S_Bu=^2JPt-lVyjO&SGySn+BWyC*v&L^OzgF>}!v zXp-lago?|C3kW2J*G{^?x>*Q0;3}Yeh)fA{qHva^!>NOu!e|%!0MKhV7OXZC6`aI~ z<$wL({`}pG<@@*Vy?OtQ2j_3k7WYDc8yr$sAB*Z(s@5zyB|)j2cp~wMN#oTFK@tdo z;Z}8YB>+Q8u*mfnCy>#QliQ|D#VHGtsg?<{S`1RPmInHZeZTRzWw+tvbn($!r}HeU z+2SO1uhuuCO-zhD?`J3Z^eE@6a`V~I+aJtNzfr7Q{`Rw{KYzM^`Q_|vhOTEJ+m8D5 zvw7~0Pw%bHAH4ncTkk#Kj6FG#WJiG;phRMFTweX27*>t4$RfkBeN^j4$@l$YSoQtg zwqCs)9-M%<+m*dTbCDKGorcE91$~cX+jQerF*^iSehGBl*%@^*s z)A)4U-lWCq!MhJX`sV$kRrmDCb)71)vw&TC(8IH{zxe*g|L(v3`1Fi^#y)zj001BW zNkloxAGpZYAVS{+jMjM%%hcp%k+d_(4_u~;UC6Ct&wy2B#a+30QUMjZxM zCewD4X=hkrPchIG^hvT%$J`~!hjJymv$Z4J0h#Yc2%~&RyV?XQ$TvWZm4m^^864%R z5CFTGlBrfV8%rsrPO8J zO(Pz<@L2j&J5**4Jd_AAHxHDcRoRt=U0M^-+XVSQeKTr4lsJ5vIvHGH7Ly?|79%nd zCT54kQ>(T635g&c>GkNkYQX8u%`GT-@u-hccEt4KFVx;#4O=hXt|;u$5$BjE;jjsW z6M2nK4)IDVZU(oOp^rruHTIJbGv@}rizfX+i{u>QVposQ7y@6c2Knp%`0>t)QSFi~ z7WD9VczAYka`M)EdBLeypu)|omuj^()QOaSnY%@ond(@Z`~zxK!OYaF!i< z8MzmtQo&|ys!To`dQLi)9hLpXX|l=H5|DuKLBm0H!sgX}+;6#~+0ZX$eKNbbyDmC` zSmw+A=%l8^Wq148Pi*)2_^od*-~4p>&hclQ<_nqC+uq7vKt`6Qf;F)MwAj- z+>3c3bLPRU=QfTSsVmNVEv4*h-C3Qunow^hTGX_Z z>DJ9nZQSjzH=9lI$uht>&0+gew$G3L!yUCsIogKgZ;N%D2{>Jye`~JJ{ z9*xthU;fLl{^1`#e|qKTXUh-YJ^#@U-hcC*<5J3tvA!6CGX^ymi$PmJ{NdzK94+Sa zi*GLY^u$T1n~)Cf!GM*pf#O#mZbmg&>O!8gxMX#&I!)D_D53K(qEIJ*nzJh>hAPMn zo!gqcFC`@bQSI2ss{aX5kywNf{50ZBgxA=I!xtYOATSa*n}$?Tv<10|T?h>#VR7Yx z!zi51ab2x?kQKBsuSdT=>_ZVu$xI!m2-|mY_Qq=iL%VotvugMOq(OElwL3_ODqwEW z@ofFT1Ad6z$SXkVrfya1R3|G{wYV0oP;G|U*T`RO^Rc)T9~{C5Y;Euy6_HLt+{#s< z))a|muGPcq$}K6)k(zwn6il3QJTn>6CK4c)cCtSdsMIYMXYSt0@@|c&1*ns0q-Ud= zWPp-53kgYj4NaxSHlo8~|MmGbl8F9#C+%L{5Tac;89;}-IyJt6#e6%KPHKdFSC6fR4fBuB1 zx?A<4Cu< z&xmG=EYw>&!FVqz3btced#rEW)G{SvoAKk{LS;*{d!n-xzA?U zY;Vmbk`5w?a8fpluI8E2c$rA%oVmZRJgehNq(O4esoPcCYn{gHsTLx2SAx42n7Rv5 zVoX*p$!vYM+ikYy&^?zY?AsOkw2Vj31FeSgI1DCMIpv&2cL6B#lu-h6QXy?5?EIG%UC z-`%|W{qG2DW)I%&&M&%NDeqzv$s)vXGIt74slz1!MiC11$;?vjld!5)Q$nj>AJnG; zR6vAAcQvphnPs8^PmW&2GS>`+8Iw1hdz6fsnIc}-e5)3IdbH_#I9|d538U(!)m5X- zCN0u_7$U`4&@HZuUd<07n6^pgSa89~Sr`IuA)|qeJz%I>ORZY8ntFAusIF@5fkZHUeQty^!K0yY(0;_~B*4^h z8k^KjT@7khtJRoC#PQGoa%nZC$o)WwvWQ4@T;qI2V5dM^;h@z;07hEO$lctk1&1*1 zj4?Ne5nm+2+V!~-JQ^$EQ{&ef^~M1`NF`qv*&T3KqiR~bR;#9_KnR-5ygB;-XnSn|$ z_7n{N_8O~jF(L+)0zmj^k&fp_Z`70BzC3;P>iXu@?f!atwIMTfscKIP?xC54q}%Vu zolY(c<__7cXsy%8nMEY4dLlEM66b^jio0M3!nTx@3g;ZB=tz+y^{Kc4iFmuVfB1hd zpZ($H)BDrv{=I%!-tDfzx!&KD&GF{bpHpse>H9&*Znqi zISEFWsaRP*TP=?go1zk6?1GVW)NyTAOnMBb>+6@Z45`>Yo%;Uf_-xxBo$hDvqDx6E z6IT*lwf8kY^|&EHzx|3;>HS7+VSlciq1c=F``{_)exJ9S4A^vo$q&}Ye8)!AOJ zU(GwAoM$V3kbS1S7*(7r;K?cCv_M82IEmENkvTNYu*qr~$JuO#j&0hR7EhUZ7K1Kl z0e7`zu6rg0wxGRZ3Ey>jH|eTOM`^a76WF$n%5BR`F409}BF^r{*^+nOM5~eRiS>1j zRfKs})0ogZ8^er*$S9~KW-SyC1)R*B+O$l-@w6c(@=7jbsVz}WD76AfDAd~>FDO9> zjW?NjxG6RI&j2Y95&HI@nZXp*JU1HVyI& z%IRM6bg6^cQ z*aK{xOaS(`=f2q zaW~fe&aLFE{X!nRcb;ZEZYQ&j3?O49G7_+9c1hf&TI1)|=1vJI2U>@iGP_z9VOG^@ zP&b25%u~o=g^g72F4xaLd#=h49~=#{RLcfeSBvW#Jv+Uu$4}Pzi*d0ThYo%@ZGQXg zi)Ejdr|%%IY`S%t%Knbrp#>~NZdRerPOiJn^|PyAd`Y)#nAVSOY(k!kj=H{{#*<~Y zx_|WW&t}KxxywXsPK4R)=wlj|w*RV*FRa|{H!pKKUA^_ee*Hz_GwzR(_-=jm>SkvK z5LGMHv>%o!A33?w-nh6xU8@$SYCamqPNrF3R|`E}*lMY>q}kU(S!SzeSFis5A0J=e z!i2I@f~Um7k?rSvYPe9`9jjhkr%>{_sc38=0Nj?4JzyqgM)1nQgy#gaHLx_yTO;_`53BWI^ZKVV59 zF;k0+RM6AmZmjC+RuokewcI`tqzyeSC~fU&(kwpBCu`i2EgpQ!Aj+n;BLD?TH?^tO z>y?5yM?NWTIc}(i>di_Sj}Lb;atG8fnODQ8stpK$wnm7;XLyFy%t}?ST2*!Q+P2;1 z@ECElJWt51iCBQ3;YK*Tt^1|nmqmiPM+A1G*R9&7I^zJQAkY>0B_9|Y;m+6#ArQH4)LHRdLKfCI<7!JVj08DpLtBw=?0!wo30 zOY#U@sm7h!%*|rlO9rQA)r_NVfq^_7nC;O#_e0wu^lPC%iDP>N4>X1@E+CB%dj#m1 z@@qrYYaubU1VXH)P~E0>jwfYuW`O0d{)caogx!-2Mp58(cvO~J1y$p{Gr#(xU-mLPPpUSKW!hG^mO+V=O`XQ4S9ja}tLMAj<$m+!_4>t{%raxT9d9qU z$M3#>@$nCMHb5t<)vAgd&n~{Lc2{n`9H)&t=X`48bocxT#^G8dXv!hc;zI32rTB-%RxJ#!|4yP}lT z@I9n(50*K(hJuRtH65N=2xP7ziCoMLuIjDA4Wq>wp?mj z+5_wciJJ#8aw;+7%h6!N0n&g&{yL|e0A0JoOK@J{?$t0k6(@}| zyS1`>^Idv)%DGXWTRI7CSZuKcQZLo3S18mDkx28ZR+TIgVvWP0vGe2LV-gWJ*jxe; zARI9;`}(mS!uQu1w6K-5_Zkwj2YxTUZ2&ccq#4`{=BTjZRn^_yC>e>d7+n79@4gj$ zy_gdTOxzrx$jVb&q7!0)Iqsk-rL)CswLIFpjGM7m#Wb={(&a?GRj%sbt}WgK%0;p; z*c3^m@8$+mhcE}ppw_9@sXD!l=kq^ zrn6-}dh2A;v0KP+ynOKezj^TS$ASu*teVv^<@4e6TjTo6{q1uDOFr)x-ypJa_w?$I zk2crD;#Rjid-3FVzkc%c=KYU9C7-s_zKommqxspzJ-A&xdAd5k|DAvLUtGNR?tlKj ze)`S_y_dDah^M>t_Ud|9A(H}ze*66LtLOjcPoDk!^UYY>aZSzRe3()ah8k31-ssiM z6b1}^e*67zbi+H<=8mLH3USH?D8wP|HO|cuIcN8fv%}RuVaahE2L!P;gZF24yS{d34O-E>5n1IkVs(TjU1N3snvi1Io;qIDrqw);Qv2 zqUPl?=ZKmsB3B-%1aBC1vZ7ilje1yn2cg}XTt=PZl<5XHGbOF6t@LBAP_(4gA(um$ zJh#&%L=N!F#3GV%mvfh;7w(w*F!}_?EXg40Pz;S=CJ{@}C(-`7L_#bg0%Ybx{>Pa; zrWNs|P&g2x_&7U<0Zn5%+2C{TPF_(Jqx;lmfP65D0ur^Fz%Fwv#W;KsZO`vyZq+r; zan(wk##P_bDLK?#%&6;~mXn5)EpgEY)_Elgr9M|BXjyEjK~ zW5w}d{pjwf8sJl>3a_SBBUZ&u;+!JoFMs`aAH`^g2@H*tgT^tsov+obR#=Et5qUV! zkmYnWmwtXXj<@@Lv65#p91q>9$7FRfCv%bT+L7m!le)N~*wz6ia<%x%DiltjTn)l3 zNvhXY2ZM3QJ>6KM3kMQsMy>w%tIgBr8}f0*n?+B3Hz?)pSZ+q|vAUT143OOIw>Lnt z)jjpeY-+)O!nE7m+^jdX+PJUcD>FUPyzJIeex6Y0->^2gtOcDuiRwx71F{>_iSzujG-)>3u*@=m<- zv7DZW-9FzweY_ZWaej36#z%dB%A8s3(UaSs{q*Y9<*tlS?w-9|zu4%LJ4Ucr<05Vi zsoiCY)p=kCEe3)S(T#+;)rk-%4(&{Q$b-ecT(kq?ZbzQA}5@6x0k6Nr~Y_v;#P*+(P=QNa&sF36qh_x0#WflYMQtiJSzOzhT?}oZYz~;!n4X!d zYwT)@X;4}LS7K%713jHN%Hc7r5@8O6hKO`s*QFs#FOpa?IUk_QQG8{A~9hTe(5vsX+4Fiyw7EH1HQIE&v=vEsTj$X(7TQJ-X z5iI2>6C5^Zil!9O%7;0E#=Ne@Iu(->+ zAu?w$Q=-6{!s(y_F^>?l5R*1yCj9k$#||<)^J;Dmb*V%Kail2z$lv_kM=_-(S9U0< z9)1kuW>zXRhEiXXU*XJ=ki&B5PZmdnwBB#awCSOXRT^n36JQy_>;fl3W&xwr;)R5$ zqt_8^IAS186lOu;1UIkM+aR0-35lJNqzUg3#|P%!x_tF$_w@O;?$&ecmowykk@}08 z&PwGoTuP@E#`7T`pOTP`o5RGz*4MZD(PqngWnAbqDOE~RM(DbxlXp)(`7;hdwu%{% z%!lJ|O}npjd&A4OhUG)}X1e{iY5N4ygSs-^ajKN$*T25K-O}lqNwPOT{A9bm+~1b< zZQb2+CtTL+FMs#)r~mx&Uw`uI@$a4=pNei?!fNhQmf7j?>Vpp-y#LL&=ZpOL=YRag z=a)}6Y$VmmiBrR-u>q8c8L?J2bF#L0ZdzJ$H|x88cKo*Rib%|vkXq7?;jV}cHp9He zEwv4>z~m+(YGwhCb%S%tQsJ&9oR~TXRSP(vf;ibarz*_u$sJnDVz_?i!M(lpH>0no zH}<$#+iJ5LhrTy?({RRV$!Q?!jc1@#cvfxLm4q@ePZt-<7c|(IJ6+H;!YGw z2VxMrNTap~(IFO2B;10Ee2^rP1iv{*PEH`^#F?mPNVI6;svl1}vtli1>>=2Su62+( z_G){#8q0NR?Ax!Ws||EvYF;&-6KGa;Sd1PsfkKGiA7R+ZH>=e*X4PV8A&Ic z7@RoDEQvX#Oqs(s&M9$D8REi3Y?&ZJod~x>DTlYt$@y#i4_b}&@Yt}hIeG&tnXA=e zR;wv_Q52;qVqNh>^MkF7q9J~W7bwVM#&Hmld$UqQ4}6oNX0=w=YJLdndxTn}RI2{9 zhZE#XM$8mIdydy20mkepQ`Sg&I=541>!A~a9YmpzYN0PPYYo(D-b9fnuT#D=ztOs0&U8LtIU9~oPZz6YB0lZpSZq6)D#DRs?kTYwVtziF*SHPEJ+YUCQe8|0h~B9 zCoe|60ro5gWdg>jy!VD2A0NNGU8|ofR_epuZa-Ga`JT}X)PWKs#XXX^H=aAXQUp>? zPmfmf2bAs)^Nac76#c3=yX^#?d&#}v&}(kv_XFOBNyD7Tg4mxlyKDf8V@eAI{!%ay zbzkE_;@e`X8kcKT_hL{(b*k>gd~)Bbjn!*CgpCgB2Ro>BiCP9qt#y9P9mEC`BH>Jw zSlQhqOz{kmCngb2oKof_ELn1z3lGdGvZTQ#O`JtUQWj28_i1-?K}wXPlEW4(3KrtX zR0)_UgA>@=QwB|Xd_bkP9R02 z6dy}9s8ubs5$()c%8Ti(xgq^WysD%j>6HB$b(tPu*yn4PS&Ry0=U*2B7nmO6!&3-?5RsHIco?rUS9#@LBR2Ytz>FhY2F4BB4 ztY-bYfA*mF{nf8NpJ)EnB~3f1JA}x|s+L2zf}=zxaeN?{JFtGWzWL&3rypHl7@V>> z0YXrAbBLih;1EVKSD3b*Fj(DLtH79A2F@wD*UqQD>*@qISE>vm>D-;nlx7L%J@58< z$;^cLdg}i8?COUf&1U`eI9`YyGmG)j^WBzuTlEgHMm;xTWFa3C*N$`{V{vsMdT{Uf z)Cp#F8pjv?)#kPnympyA$6qHu1AI>wAo5s+l9MD; zn3gPI{Az*fC|5-pyqc=Js**#=6ke2e8q6CbPr-OJcQYn3VmBp#O#x~Zoq*wx8*z35 z6%y4dHG$**H!+3U+dLeN9f-N>%)9Io;{pOFBDI*U<~Zs(8Iu!bIEV_f5^41qS=#HI zd0ZwVR5iJCOmsj}Te1lR@%Z%3ot4_Y+^hxuLyzj!%{|gs++w93g{F}xyNH`nB|so< zFs2x$QDLwG;bgBx%?K4yqXem{DMh~BP|TvN$?nw^K*um8&jzr;q8wxjC1!WQ;qOCn zHE+BAfY&*LVBW~@?8Fs9PG*Xd-Mm;$;zE=;IY&_;>=_OcQ&y_RoCFC1(qyGJ>r*u| ztzM?03I=h?oO74^K5+(d{P_YEqXaQEVwf|#u^EU{|M1@Fyw87p_7!=_^SPr+F<8pz z6I^g7;zpx$O5~N8EJZh<;U)wiGKFd9#OTbauo@I(#av0rJ(0K8U(S=KipnXYTDg9q zkAL;@!|y$~)^tkcgQZ@h=hTx;u3pW??JcElb34+W`m>oiG5fT8$ugHbTzvccl>G9G z-@kf#RsQui7q{2PZ=Vm#Q_h3cUEQu2@7zB6$CtnU@#W*^PaoZ$z4^uu|NDPm@4nby zetEOscIV$r{rU4Re)g09=gFJ*kMGR~-Hzm9Q?Y647kqpo^EBM9v7aajPt)EE%w7!F zH`Do=Ru30<+uh~m`1JXRKRWsHTW>BOCOIk7`H;VyNUP2{!>pQlle#jjIRav$ z#6ljEC(AaPByH9)U<}!*yHGkTHj~5S3MLLnGBu0F6pvXu=UAx{FPeXr>)R}WtFjLkD4v{!3WoPfYx!D;o zscN52=iQu;RLEw?!_iGjlMcfwaZNSr4c%3}d-pi)Ccj80)sH2#T8v0X4TaXQ8+3V%L@kyv5Omwn3-Eqovgu$nj)+j zahySvnfBF{ET~Z`Mr|yB)GnVQ=)ergeO{2yP=(l-T5iy}5~cVkiOiHFw-h3wxG7MW zq3&WMbHwB=Kz~Xwn80Bqj*vboIjI)zY1}*a!lep4nJP0h!NE1`@Y8A{dR;ff1@dl1%aVUNKfL=QMeUO|*BzGym?JzT8hC9PhVam`-V>p7VGiivo z9M#~8KMqF$8L7+?+#R^a?ArdsO(H9%UMe(o@^IrtORtyY_0 z)rtD9I~rD-?QXJNcRWkrY429G)~eAz_D0brCb)?qWXd3M{B_b#k-A0tj)) zm(=`t+@K{X<O*n&!>iR&H#Z}9-D24f&NE4MTHoAm z%Cvp;gQKq-CSRdo13v{(WEj^w<~pDEz2(P)vJxp^0YN~?mCc-FCV}9;@3BY zXnB-pboc7ntN-->UA^`0#kW2_-{0+?KDk=v^4@#wwEwqXuV3EL&BpGwwjV9$JY?~> z^;payF;1JOmoHykUEkiWr*SeXDNz!&*w)Ub@`aMb0VQ+U~`KvtN_)jdIJ=MPzHc`Au>0O$lXEb=nk+cnXzORcdJfD zP9g&wECf>rn@_90GVxA(XtJ#5$30})?9w#NKrE62cqGIM9Ne=_a1@dT7J-nYG|y?s z-Mr7U81o5oPRq0!(z26Tm*?|5b6TnolzQTh;CUd)@F-a|KNiO=^{@^gqU`A0iQCg8 zz!6s6B4}N$s`*r{R4+wG)oNDVCbO!5vlAGx=1!8kE-$-$obySS&b#is@9uT^Jj-dy zt0W8InWrQ)i*gO2tsaEX9T5=emvl(wcOP@$Cq*4g0j)@t<)7aSu@;}+Nm z;Y8?3xUE1Uy9$TbL!-34jShS6P}4`HvclacTJ9D*8*(7-(+4#<1FeM%dJQdTzw`kG zXbep!2e}9(NjY~Sso{tZFzfZ3-}&&HN3&HOpV{uEKYMK4+46LLa^Xj3!}e8PzC^Hm5AD5jevo|=~UWbHP&eLQC zQ1k6w-ER50?e~*%S-*^=ZsZ!PzTy?16- z)w!SU>2A#QTnG_>MA1Zwg!oJ|>0j%G6i7g3kP#vz5QEF~^i22Z)8|sR%-lP|-Syy+ z2YQ^Iv8n2+ti2=L*ZMvN(ZiH1TEZu|x6oS3p-NWe3TBmqx`=3qbe)f7o;|uAx3GPA zxSRB9KDYJp-Tl!{4~#8`w1_;M!I91&&2wI65``I+&n#l@Kmz8?vc()0W*NrI@_afP zMancyB3D&*Ql?t>MfZ`wvM_k>z4d;s{n&MFyi-1&c^ei*3?5mDP9qyf=23-G z%T&_NT@+@tQgZDtxeqPkfebAnwp0{uI zf!T`-Gc)(&kx)BGNHXS&Xk&;* z%FCQVPr6ES@6u*37o7(qqJsq#56^!*(0wA}X}4FET54fo;ymebNda~DbjxMwJWtbE z`x@i{5z&%$hvLTQN`NjD{h$8*Yxl5Vs=53E))rM(Rw*?XdRh_;WfoiWt@qx|S|CCO zaW8P`n`PFi)@i?7KRi6PeuPbmr*OJkvcWw_S*ZImSBLvRz8E1AhG>om$l*|5T|IfV zf3mKp_v;$UwAqS``Zi z&dXyvJXv^E*ZbQKfBWu9iKjdJ@|Q1`tLXJSNt>qnXiq=dS0C+FH-7)!?Ki*Icem5y zsos6`#;IN{`=DxDV9ztPff_7*??rEj6PU0TkX%^n2JIn_O zEsJL!gV6icFaBh?{*}m1bwWZ`y?69nBYASje7u)4ECC?v40&TfFhdSXbnApDvq3>a zNKv*P-6#{qF%rBD!EPQn!_BckpbHOEeZt^h=}gWZYjG%y-{LAfCV;@2LtMi-7@(I0s#n3J%-FfaTDG# zqL`WTOEPfFC#LsqzV&VE)|;7+K_-=|rIg(?-R$aTv%Z+?lQKVDcF%Xqi(0RzvY+Zy zbXUq=%Pg91;5jgn@PT^|N~lgMQwp!s052+{rRcuMwMxZ!5GWxv4YY$=q?%_1jGpS{ zHkPR>13TTyknxW413kcK@FXZ9G;fX>3`j~xKGw{=!=r!t*%6Yh&&${3#S4zhsV|Ud zy8QnJ;TNYx8d(tN5oxZ^0l?t21Uax{BYKF7;W-Eq7K%u?W=c55*A|jKT*5luW~x$E zr&5-w>`I+wDx8%HC^=M=`O&_>wH(0?Z0=^>-8Xk*AY`~T;uCl;e`Q)C+jk*`-a&U) zhEiRkfEF!Zg&HAJoErIfY<-0Vn>JC7+l2~6w1-=Cw;o~p`sDMQ*Z=tAH?q53_(Ep% zM{R3VQ44o}H0-rZbKQ5fww=N{#U0jF)hN!#htuEQ9seeN=RAvoUR(>Kgg44g)s$%N z(it9|{j~Y@l|OuU{NMll?%5a5e)iMD;rZ2-USm6jpYd>fxRYtW)Mtm@0jXdAi!bf% zt-jnfFaCqvJhe&B^Y_1b`sDqO>)+hob2=;^+Hz{M*t9;}J>K7msuod+wzk&&)f4&U zFO_@Q+_lWFzP$eC*ALs)+tzn8S=}sHr1rrSGh*GEnTJPmwC5tO+&^DFo{srB8yUUi@YMCFAt2{s00ndi2h1O{qA50#>wUA3NlKNfQdJ6s5t~P+6fCm} zt0HKz)opv+rmZhUY}v~^9S&k234-p<(^Cm})4>tQxXJR0-S>b;xi@F~2vu8`w<>vOHUsUGI}u*^3i6$u*4ey4knwzl5qW^%uiNm+xl$$i5k ze66B6ayR#E8wR{GZJtwv_{F#pZm`^LNf}8?zpzkcbf)1-=@9`13$(a&$;ap?w0#?A zLKpY3+8?m?fCtHLqhNFC)Q>cvKsip7X`+|3F9AIapok!d+`%{_RI(rMmtF=zpl7Bb zX+rsLSBQwoB*QyKN{McSU$}u17kCN72?<2F!3zu&2@w>kXc3WFS!BRcbLR<$s#sJv z&x3Owjax+VWLt`ajB-*yU0t1NC5Tbkwbq>8Aw{AX-PtXYEfPU-4|q?*9)JoHm&{uf z(TMz8CDXx-05&F{uCf$WRTf)4EO<5FeEI5&w;$guSHHi0apT|et|6j9B6`3d+xZAl zashWCoBPI5dXF9u3!ykkm9o^nr6pl%CeR3o%O2|T#9s2g|{qm=sww{iK zC-hslwZOF4?XTZF{d-eh-FJr1xvsYM*c={mJo-1^J-&U*FTPkT%(mzc{qvV)UV=Lond#Wp6Qo^V zPmgCmoi<0fn}~ooZ?@ajIu{bV&+3QP{_~&z?%)0|U;Mj&_r+iS#qU>R&yF?10_fg1 zvxy3poDX>bVU|4KOPeDhl@VJ%Rc`=#w-Cv(l0rH<*c>Fpy^B^!;4RbA?FPCOgcDZi z;c8H2C4$i}ld7_r)nz_!7HyqMZQfN>A))4dE_x_seIS|@YxVtO`})O;hkd)>n#Lj_ z2=CzrQx*~WxS{kMw@9R9(p%Gq2&s0X*v&JwibOWlgoRkmw}!Hm5=4>RQlD;JL-%G@ zP`Z6QVVx%Q?UvTU%{tR{0mfh27zL{hKk`WS)kgzaUud3E;)sU`BEwikHwRuxhufd`@(ehxH2ODqckvj|!^NK0H+rdByV z6cK)T^TFXk^&Td!|LWg=nR_?{B{)S9BbKCTvzk~w(Z$el8Eufc7oYjYTeql zo%_~%&(XO9waokFfVNH(+fh$z5vu1T3sSULhtUKMD76GtLM(#GoEExUw9SH~imM`G z=sR`jJ$wJ}KimH3Xv?)K^?b~n7a!t?9$*-x*(`1zC1KHtB3 zwZE=1PcqMY73>f4{8@eevg~(U&$gc9w3_u+Yt^Zk`PzF#Bp=oygji;MdNV!$YF`dw z#Qm}V@cV~9`ReM+8$kTygIO>xUOQ2WYUYW}Jb{>5@9@ikP=o;#^7-qR`|Gb2iz2Hq z>Ru2EQ7w5-i)Nd7cS$0#1Rb!v_c;OYkQR^%l%knMp=Pl{2A6+cqOC-sVp*;WSM*%Z?7J zl5yVFhvusbspf1!aS_wa^NAq6bG1@Y5gV(Q%8_kn9awfc+y@yx8+5^Lv3Zzz?>!Yo z8COzLwW>~2+3k0)r|Gjvujk#>vVXoz&lX)6MJY-urBF0RP7-;f&?>bSSwf0X^TMtu zDyrNmtFRPSBtA!q@W9-EB$P)0Rn1ByRYZZY6_HQ;`}}fYpn0r<=0LzM4@I{@dUlLA z!6?97uH_e^5dayoaw=5glfFEOq4NK;&1IF+GBBT<0nn2I=rQxAfv?;q-x~(BA?eori|g6qzJUA)FM@;Qgx?gDy7JjW4t0TOccC|!iE=7)3!4{#@E+TIvq*{XuACv$Sk?uTWwQU`=Jws=&2obm*SGg^|E7KM zYAJiVguy+?W!g@?lwwDdG7F-pwn<*xT)q7K8uR4l;4bW{-}3F754RsqZIx;F<>9b@ zy#MaJZfDrbC%T*aEqOX^%`w*k%E$Zl_}HYNP=M$e8!mFVE`RhdpB_G&{_dNH^T#g2 zA0O@i`Y+#C;?3D}cwdpVTUhhfe6Hz%V*2ZI3^X=8GP({x?6vHdsW&l;j8HqMl&Vuk zh6jkIZ3v-=X+T7WzEm`(rbb^CYYRn3K^1631YLo-Oy=DLn3%Q_YY!UUfz#INuvTBpYGxv=U~>u8 zpuur!CY0_p6w|4wnMFmI8>qpm%eI>j*zWswH_ntO7%p1~(S#aa zLwk+MeFL=QOmMg^HG(3lWQu(pP!k|LEQUa=lP2ENWHeUc5;kVmc^@B5&%xjuX5`BQ z6hzv3DG9(5A=Onb!|q&8%NPjep2i3NjZ8LI&OnSAh)KO&5??6Li&cFjXD?Nq2?F;j zH1X!p$b*GfPaZoy@dgM=B#SCzMk-KMS;#6{sY+6OKshixd=N$m5!J=v64-J|ae;;A z0x-L-3Wl?TgH`}xq&qm=ql^)1g7pR_RWdWJh)Zs5JmB*OCKu@uB8(}aN;+#ELp0q` z65+j{9!}QVc0SGXVK>cnH%*6my(6?zMOEB&@OeB$s!CX_BAR=lNN$Njin;@yHOx*( z9-yt82MZ=F&rWgDOp`JqnnX|}(4s#A{qF6pK`;n9-D_RluCxjY98>S`t*5y{nhU23uWD}fBkTO?8^A&&U2@k(X(Xf z0x*weHWyM*6lR0+fB7pNLGGmg6g};(ZnslC9%~P`@oV%f%aF53frRtyXrlChcq&!` z6e_AMj?OqK+vWr9=uS}yL^E^)&}!YcIGM*ejC7Yk#1<1{QgpDL+HvpuV>{2WJwL<` z#}+;TP^1e*KzMd$WgIWvIuJ=sJol};tBUqGx6N6WT2w+puBNKab?Y8zhVE!4l)6;! z;ZByIMlI!x`a-0w&W^L;$ULb#0JE^ju6{xkEtp7&G|gOilAna)weG4u7wn2o({x?R zPBC%lZe_Gs2Ld2Q3#TZx3Y6ilUTQ-aJX-1IUc6aHAT$x;RF66+>`8in2ulum%aAot zgOJJH(yAA3K@IBx1d9g)VkC_UdQQMCbC4MwAK#^i`}pp0@iHqdq zk;@0dY1urJ#Gg=Dl9`=kLd$MLPMweoF?B}I`P3!JTM|~QW%!r^z4dK9Kc4P49Cfb2 zL+dpm_(hS8JOqo*$zf5QiWDkBGz*l3Fg(J#TO>+y!OPcWch!3B`xlJzczS!hZa&Ym zE>qTF+IqX){V0O)_3rIsGfLI0P$8&`vWP#dvALh4iEdYO*-tW;`R4HCCoiYv+1B^J z|E@jU&C}u8-Tn6Wzy4ub_O+B#*V74)=MM7t_~HGBclVDc(Rtm@+vCRY$8&#wZ?4m} z+Ih9sO;vVPCFz7=Q<1By=?}iX`s(M0x|FNK`#Z*C4&wsU)Ua&zFOl3LDuS&u2w3vnMkaVrBnC9pn6btHAwm(Bl8;S@W=2v=K7n-DwuP5Vc|IcLvOZ7a+(;{p zv?QP!8u^@(L`)@ zVBDialH5Lk!xpeyi+MyuPZDw@qaNnbd&^H_1{FxEmc-6x+*kkffBgd)E2=QKc`gzt zmu9>;Ju$RIfhbcyA{I`{g@%=Mk;s{agLO!_0aX=E*%v+fX6VPl2<*)=U_n%^Gl?oP zmr~6;;XSNbn`cZ@G%ux83M6`|Y2EU9DleX#e)#q`@!|G=jPuu5&p!XvAJ5OOOya|j zw~r?g32f`URQlG|#$|6Fp+G4?K{J0m`*Dl?bXIw6?n{Yjmfe$SzV@zf-{AgX|IM4b zZ@#_V&AhtOH$T4p;qB?k)7QFOo0a>A_3`okytVDTt*v?ZX*;{wJShdT=A@q`>AME?q~V%{(OJ52&TJ2Q=JD&z=c7CXCaIr-NW)u zJ)Cu z0ua6d2Etea0#=8ZZylLdHo!a#=E^PTj#_41@0PInX4&qJllLbYM1+KN?|FW~FjPu! z9pTE5h;D1MUWH51NyUI5o5Mg-jpG8ic?4LfH%obTn67uzZmLh0-CSx=fO2wa9^}@x z+rB+uM77+**FH$lmC<&F|r+8Cx;@UbAg79%qFv%wlL_|t(Vs7jMsTL>o0z1owPa-L@Vhyuy z-rTqFmZ^x$A12z-Enjs5eQz9+cwU}Yr3_p=g<4odRTS#^b1rkXNHX(d6;FNI4GLvJuO-!e<=A7FC~bacsU3`8TW9J zpc3W=;S?n+$`a1ROJ$4Ca@!b=%hE36zlxq{Pti%0fT#>wHeharANRXG`E*+)q&z)9 z5BI0LeLGcahA2>*)nDIS>2f@NY^T#UEmxemEW4)tGzHf&e<{#tThsWi5--|r1XISru<~Xg7Dx#%|htPMEcC*d-;(B@I<=fxi zZw+&y7Fkct+~=L{icX7Oznpi^=10oygX>eBRIi_uuYZ32@x!*7=S8IpQm*3zm`Nfw zGcBwNX!3g8Vz`VcDO+qOT<{oT=s zT3C8C2a*Mwm$kKR9U-WUXtr(Jxsx8!dOWOcqOkRX+rrs;W-|vY-aRaub@z~NEpqt@KthMJL&-T}Q7#K6 zh)^}4`KOscgc0k>!V8K}LNp*Uunj?qLaP*GRZd;!=x0FGB&7}{JP#tm9wA}@O>l50 z)iogb1nKA!ZYe?v0^S4W&8_9hziVAL`UyCNV_C`qMKgbcDq&Q^2x zL~5QBGO{0(U>~by5nwA056#+d&H*(-L`Ac?rc}Y${fl_w12r-pz=3h$08R#l77~_^ zK<2A=@B=~<@(aa)o4|B3IW~ z+h9VoxmrJ&Z&Bu%W%vE>9)A0GKmPXjr|rytb$zwA)4R9l^A;c8Z&&+s+l+bxDR8J% zco&^S&3Z>fom7OUwOgmm*xyW7&&thJ{P4qhT}_33LTr6mxW6ukmy6!a4~B2PJHLIm ze)ak8t1qvfJ)6Jy{ARzqJ>PlCZ1R)TdBGZK6^(#XdHGN|XwZz|A-s`JYFL08M9@U4#k931gXI84g}}TiyJB{1 z*#s>JaYtGgLY-A~f0(xq?c8i*l*4r8zWn^<&+l$m^U^YxBa&a9K<`_(4%OU1Y_YC^ ziB_OFqO7VrEqk4=s!H_DqZGN(xz{M*p-lTq76oK0-kWn4*F6daVRG`VPCA{XnxDfu zLc`$j9)^U6TXYYzmeLx}bf6FTxGObEVbv%M0gC2^W0YO|@>{~FWarX0(k>}^Atx!K ziIS*bCl%5`qlk@LdY()Obw{^NRE(2^WX{C`8t8<(IuV}K6Aw4^)jVl*0VqNo#k_lf znT<%;Zw|s47X}g`BfCw@ECV4>3J2y6!yeCpVIvqq4kcxhZ^IH06`8$&0%`Qk#WO!` z!9mc31u0M+Ozd$`#_*bG&S4qDjm9vJI^$)II)+Vj7pO$YC2WdRXAQ4!&aMM0F*2!X z9VH=VbW^yE;jyNuHXmyep8Z;w1PFD8qnAtoK#|ZTiNvF)6BZIQPdqI=mxGaw^RaXD z@fMOi?vH0DM5L%x6(oIynFIvFQ@hG-JuS<3*N58H^TXZ778B1;>RJ!BfA-8C-<{rl zdpsVy(?zeA_2J>}{F`z;%`a}Qp58n^$MtbLgQq|I+4V1e`lYHy?~m36BajdX3Y6CS zwi%jr5AV@ARm!B^Cc(MK`L6jxD^r=4>HF`GfA$~VeD_0dW>>rUI0e_S7q z8s_(Rck^zydvZ-!uv=IJy!q~+MSa_RYwm834?G^X=U*+)_xtM?^>p8kcWqq}CR9DV z*!3z8<_$FB<3oG@x9j&mo__iBt7Xw4GR*RS0EB6bpD!T}NNmlMJYJZY^6d6TC5w=Z zo2w@;U;cNdH@(ef15%DCNQ@TbBDeL7QJ6p-?lkL>|Hud1H>?8_qD7XtgPvQ-AiX1h z%kJqy522TQ}s-*N`F5Ias z6A#OBGtZ^!zLxzwPg*Q|$)I0=vP@d1QjqylvrHV9{H+Ed!_`YrbIG6t z8EU?b^HtiHNwH$R0e$nR8le+HyGLbo!w7oky56nF*892lHHOo-1=HbpX_N;$*nOaO z(+W=s(NEH0`T%3{QwBhSa1mPm=PYApZ#xN237`11)Q;ie3zC|XN9e$t_Vk*NTwDa=44r5Y|04rhd7H zpjhOm-xxa6xQxTna*QIsqn=XEs zPvKF^Tz9yxdUL+*PoI?KwfOqO+xORp!{M+m(<0(T*t&^U^X>Ni56jh|_gpS?V-)4F z$6x%X@8F1Tzq;0&8=0qR-Tcr0?EBw*dvem<&ujni@igt`fVZ&o<`4HxCw_Riz5UzA z*PlOs{n@@u+cfocv*mg!)%)2jKuL)Tx3#y2%@>I(({%w+PH@OEO6*PSL%n5vKduwmG)bc7W{Ns&#kk2#;L6IIY7x zYk--^N^b~ZFmXV{;gAk?&#cF|#=?CZ$bz2Cx2%97V$kAf!z~WU8bOT1DIo#Pfr&uE zQU*x?6zzy10q2b{G9uuM@ z8JSNMTqGDNjpezpAJ9$E|H91gl0aHm$JG?FeB?1(%qoeC*IP` z<BGDGpVj3bzxjs`caL}PKmP3Xi$DC;=YRPZfBnaQ{EO~2NzzVU_LBbX zcc-`Sw%w$^`lDyXNXBpe`u*Sk&0_>y!XvicKR)&w6_Dofc;1f3ay3^LKHNW^|M7IX z-!1#Po>7)s7e7C40p?)G9W4St6}HK__;!oc!ATz;<4$4CiSV#eb6p9OFvi2#B|}A?I_wDV%5DE)kSK7Qq20X z`GZ9W_od85H^d}$nd`onb$2yS^Me+1!un=CLIlMFJ%}Xjrl!}zLGkFwGY(y#LHB6p z-DK$lLMtg%2Mw-iMdmQ`#Vw3ciW`zTlQ7S`-h0OOBz>hr<&^X}8l0ZLTt&Eu5fPfp zBV43>0ZGj$gAQT&{fGNmRH#(e5>-l3VIcx#NaB)(Du4=c$=K=vH}OzKr-TJnL@Txku~Mv0HQZbpdhn*~y>E>T(Hs?|g@wCF z9$bPzMoJgu(}nZy`uR>E>om2g#IhHir~Pi9oa!)HxNXkr6=3(ab=$mGu~N93q?SVW z&8hQpX!~th@Ap@=>`>TxLUj@5By{uu-R;sBxu6;}&%%Ld@YXrI#n#;B)bGLr=t9jN zPuK`7=I!>PLdCu(&L_`!_G!$`WTR7D z&58sl@=pp234u5Vj0l5M98UWLRF0lQS!I-Xgz6=w6E5^DVkPSa6cfwSPZms6v?z*%j&)&LS(4o(L2|(IG!Nuq8Z^MX zcZ=4{)~1d*T}I|5jZ_A(^%6&S2YI6CHdWn~(^TtMufIH=Ki;ivndTQaWl=nC^7YTY ze*M&XCgVJu42EcpQjg~cORItK?|*!J=thsbQ@j3Z$2W31_57)uy9lNt1YvPp`_q^X z`|05vr}NnmqS!6d?Zb9DZV~2|D$eR%n5+=gS?NdVX8+CD`DyWex@YqTe=o}vKmFU#k z`q5bt;h{Z(q;yjItOF;qX$_uZ!NyyThb`W+YhVvMp1++Anj^ z;DymeAf8#pdoFixw>uEf9fQR}_tT!gVjVheO@Z^=|6z=$&CfHL27dXe zfO|A+<`*5Dga|4{DksuL_eo;BM-%~vB|)rcjCmGFM})? zu;>P-nXLjv@$d%**#6m1p7!(kn;(83rF{A7l`OlD$5V58d2^USNO`sC$uLVWz;e7@ff&(xZ;#uvZPyZ8F>V^FFnqbE2LsX0Wz`e?gm zvtn4GI4&i5bn(dJ0vYh`wQ?%7(Q3uE^?A9vnU?#9+rRsVZ+>!m{QT>Ey?#+jHD^%A zDJC5w;buk_As8i~aPQFrqZ*XnUqVyfOh}Q^*v(8zeinM}jDnqMVZlaq7O49D^f}C)+>?p}2^Ys#HJw$)<8MFQLtZQ;{m`)>?N72FW6m zN)4>(1f$1Xq!wbT)X4#-BcuRTWuD4@**$x9^}M`({D2>CAEExMFRq^5?0>l5e*N2z zKfHT9yGzir$d7Rq@(6J{y?a~RxwO^$Db{n-9jp?lV_P0U@hns3T0MQ}R0Pdai5|@? zdXF$OZ(HxybAKo3gD4SRA{;QLOmCDRL?RI85#7d~+wB5aML5j8 zMVOBOn~i}Uq?16m5C;;_k`|?$r;=4dTuow>lzi;SW5@vVotG7OWg7RGc)yemjZ^$( z%{A_x8kP%U3g9Y$4X9FrfO6Eu1lC0`Xp9V8x}@$YED&fANFnkpJZn~JqJ)!q@8;O< zkB@9NWeq)v$r%qtktiiC$Lbi--9QZ>=N-Jngo+b18b{0k&dSS@-nex*Hq|7DNGTGO zGzio4Pkq#*iWF5TcaOKX4;2>L;kX^ot#wz{=S3E&cQ#kO+2i`z;qmBi z-&;}H&zSbA8sV5k4>NmoAm&M*TrXe!{QB^;nwz=ZpZa;Vwryne+dffVeYwa1c<~KrHY#k0%c9^@FE=~|Hwn2)B!`dD5W7bOOBF`676SbISHGH`KDqz# z{oA*o|8dekTetRM^~BRDVWdlhj7WxuHShZ9+e2%IQ$3;Y1!YAIp-?)8 z$Bi-aE>S`yE>(1Qpoev{T(g?>)|#1T0y;+mxuakRM7*!5zwkVe7eLE~pf^|+(-SC} zP4l!~8gt4A9~Hkq&nINapf9;5xV;O+Tvd`Kl5k86-5#B+(IWjFR9+)d68)zn|1eahPbJOl0~%?ttFG_u=HKza@!#xJ;{d>r8!!- z|C2R31S8*=z$Wu3MD>7Uf=m<>*Z|)TYXt;FQb%8iNk=z=Sl9FU^zn3FTkFlu;fXQz zQBMWaJmMbIBs_Z{VD6j8IYJ9d5iJk~mC+>(59WXf@!76kx8tuLPp@CU`uQ)u_|O00 zum0-S9}b7;vA3i+3 ze|LMj{djzQtQEVfI;$Q&+aGSGQ+WBucyTkmcr};9^yQ;PvsW+6AN=(2%lX-lN1XfJ z)%8**o*vi7b8}F_-C7{bJs$aS-EdQWdQ(?fpF9IqfBeQuK`BgHPjmGAU)wlih>Yp@ zWwMYP*2sbSD7hlcya%sJ35zu>%+VgFdiCV<&!?NO+j((0oIBLK2r}g<5uG$jwiGf! zMxkqN**#cXdcypbUINYuDnV+5Ctae0y9t^89UW|DRr85wTfs}JGb+uaa~GPp8Iw*?-o6zg?ZS* zqS{<0cVzJ*?=%KEqjVIl^|~xqeOL6W=X!p6(C5#dKAWfYw0`w;dVW3M-fi7UrN_k^ zl(P!Yy>4vneBSO$yRL2T+e4!kiVr5X1yqVr6;|twQ$Q7+Ttp)17T&`)ghw>@X2?8@hnoQ1#;B)=_HGPbz?IZ|n!xj4 z3Wx?&hnPT_NsGuww!?_xfVc_ZyqI-BXO3)Q#2H7KNLZ9}Fw+`A4~58RV1OZc7xu86 z_rN2elcNp<&(9+a8OqF8Mf#O97@lC`$diBGR>N0o2zU3rS4?h!qFGe};fY2b88D9y zU(>yd4$w3gN-psK5%p$Gl4Qx1-Z`pz&CETPTx;ttpwVbF1|S5S85x4) z1BD{w@8%1I9GT&egfs@gkXQ(y0rXatRhb!!yPMsss(euM#9LQOEs+r(*RH9a^L^-% z9(1I5N|4Zj6cM|u(5%cu6qQ?dad8z?L%|8?muwzFt=n$pZ!Occe%2~VrBVnh&b-HI zJwKnfzRu~YNYV7VLLo9oA@=H3eJ}vlqmhy$!m1k=(q+y#sI-aNh#Byp%NDwh{6+U~ zzU#NQ_kZ+X{pkPtkAL>F|M&grGXD4vfBf0&m&E+P|NH;@fBc{R1Y}gz+_`?T`iX?*kThhM(??)m%g$K&%~fA_?1T_~@Aa(sP1w?)>| z`sTI0{o>V&SI7IiS2xS={s_14-v8z|r(d1>#W5yp>$Z-~Yw~F1xyKp#_kVxte)!2B zEURz#ual5({&7UgY&tbgV0U=E?nH8TC>CU^#z_zXvVJ*Jz|;ib0XSn++boX9+jYh5 zN!mQm_g|bQ?HhYxTM*TlWN5t?0jVZnV*hK2Oj3nOosvL$N~)w-hme>o!&>T$aBuS| zXUR>0!|EQuu(tcMd`JZMWn}d5Bc>*zodnP0++<4Xq{CzG$SKBETE|)lh>`o2xR7fiI7)7{8mCANqEFg>_n!}A-ozZ8C0!EVDbAsQ_%J`PF}%r;&q`z#J||YtMJ%J zt(&I;qFNJOO(&a(=$`e_N9KoU{Yf5oGEW(3{ z0RkhgRQ$T*)K<*%oFWoSQq4tq86Ln?E!dJgU2~aJHVYv~f4cOibF7=tC#5t^WkNjC zbH9f=WM3}e0h8O>VNs){@0I>a<|-U{h=h#A}W+$fA{`B{pH^sZw3i&I$bF59ck`m#;HJe)9_- zBX8!m%o1J;v#hw1SFB8udk~kA$x8a{LFwITb#`y53-ZvWmtX(5z4_5OryIGov?F;@ z^bE=2gTg8I>jVJ{U<#x#1$4;>Q<0287$)izaI>x1X{>8HpwDQWkF&&BpSJB(`~gm_ zEz72uO%9jU+}*^LHpV$IH46jMoyhI-^jdCDp2JsAC-jTw76YnMigKD{_VA7r=aT4P zgiFTLdAkhR5(6S5-}x3WVlshL$y(*M%{wyEOX8WHd2y_n=>Z=|C@gWm>GQEo^K9hk zT*I);H_Nm%)0skxLObv6#wwtoGWbc)=zWt1y-W>KtLxy+R=f>JohFiGU@%1^bJXs1 zj2SHVQ1;SDif4d92@FVz zSY{ST^ZJxak1BChSRHF5!UZ&8I(o#`V;%Vrm?j>ZPLtSNm?#fl*9)Gv)t?>f#?2?8 zED+KITgC{Yy!YG(p9N}|kh6wJLIqPn-$F))OL{_3P;eJfKwWKEtRZgy_3!=k`=9^Q zfB4xq=cFS3-B-82eKUXe-MjzeKmPpR{_B5L(VfEgQWCcYl4Jmu}PZe3aW; zyT6_8@0RK%4oCe>`{3pZ@;b_5At^(WbxtWjvn7 zJV|R2lGgqL>Yrc-s2hQ#_*qq*4>fktxXTjL5C84;O_r71pwT z7Fe{%)wCOZgm#FNmL3X02LJ#d07*naRGR5MN3WF&Go>Oq-iP-wMv6m18gsf3Lxuyc zM_^5V)a_X!mZ{CNc^@L0!3>HBoFFafpSr{x!=L+wgK|;2#=LD!?=Ed7&Cm>rqOPGL zJKaYufiiro>+^d4{(Sz>*R{%agFC^@teM@a9RM}E)wTc%ilmmbRG^JEx8}BlubVaB znr*7&1vxSzE`SyU0u{E?Zy= z@mvIS7;r(JGrCs>K23xa6@;W9EU)H8qzMer6j7K`J-rmgMk=aNpfden;LY1t&!>m~ z@Mk~21R~F~CdUtzxaX-JfIV{IE&F$Eh`AGWI}* zWtEznW=f6_Bs3%g;uJBx2DlypHO88xm>c+ew34##t~?AryxgF+Rml4z%K z-nboyH<4y{Ejc1P=-r)3hwmCOFj8s{lqJ7HKsqdke?&|oOP-(l<;l+vAI}e`Ej>*! zXNx^Hnnz=Bt zy_9mF>L`crU)1BUV6@ zSYzw44_50`_6&C^n^$<#J2Z*b)3kK6MF z91^^m2(B+6`1aj)KF3e~;PCQo-XDGXvHs&f`||^jlg_hvYQVk5{d)tbKIEY9+{_p$ zq+0=v;Nfq6@T0pgf83V4#wpyqm@|zANCTW(Wy}~N^xQHf(Y@X#dI0DN!a_zl{lYa< zNg=o7894+I-iZ`0RD&(yTcDSWgAjrEShq~0zPUR-$noiM4d1Fmd%AF$Y`etm3;5WC zN)i$?L?~7kZw?TXAvr%-Wj?AblP!Y-W zsD7qb5}Q|9%;p)ssr!j%^Rth~@1Fddr&w1A+iV%abh$FlNtgXaQzSDdR!e5|c;Gge zo5oqj89au~U1vfym6Q@K2Xc^-4&S_AF8w_ER+YmNZV(}2+GbiK)nI0estG4ejjMC9 zE`s%@F9}dZRVf1`u_AJ8iT-4pek1B23V@7vV){gc+MiQ<~DW%;pUU zO6((<8n=qFa;ZRMT2ge6vL&USq6NtWD9RX_DVR+FT++K|COWPpvVy#4rh^W%uyX81 zB2q+XGQiQ}lG!6KBQhwp3?hb5$U(1j7UDq|L>QuKS*;ix0_mJ*k=@}5$XM^^-?%Q$ zKX`T6*7fa+KmMKH`oa76-+sHU%V#hA+)u~5<>r_&2`3YMdi?NT{_35W?(-eld#SsJ zuZIC~iTBU(QAbngByHyL_VMOU?_Su8yW`87+v8!Hr<>zpxj!to?XzFqO&@;!FwOHA z7ZmY%oTB@I`ok}OPtV_e_xOI@#`Ota#fcQk`ILYC=ck|k;mw=R*_QsJAIUHNDIY)j zJaL&VGPV>*hG#}rPBO0`4@htzwYEY;rcCYiXJ0SJ7iLEvDq>P+ZLJ%PSlC<{yT2^Q zC=lhy;YIJEVP{E1rVB4%jS)ztUs*8Ht4k>L@s%+=FraE0lMWG#L=z!mghOM} z2DY9gW05nWF*OFyF%;-5KE)Uw;n}Ol?A5x@HA2Hzzf951$9X+{`|;a>GF6k2o&szHhVxvAcfdU=CRI>jiF8K|C1Rqe z)EkIVMMO=+s3s<+OK_G=p*f$O^k-1q$ z_=t2FnT6ZCMh3}E1+3RnPa7RRr z5ezg{v!pV$f*G0WZfOde;`Q;>Prm-6hmU`4>qpi6?DH3I-rg-Y^X*|unJAd&_Ot)| z=YR7L7g5>i^CfaEBlAv~+BvQn={?sq&zE?5^wY=j_95}vXDs1pQrN8dJzBvX*&g<6lz+p5pMfoNIdL_tg-dTe7L(?nF+>Rb_C?H)W>yIpRMHZwQm z*<)M7*MW5e5eaP3SMM=4jUL&1vLn_Q5$?J9@a|)bzDB=zzF+gS&F8LLOaWun+lmh^ zH5PyfMWMA&ED1y%H;K&Q(Y>EXJiA|dtfQ}edtA4tOMkjtKAtZR>*@Q;<>Tda@@>l) zi9p%wy+^o6s+i~`GHE+Vo785LSzwt3Z+=cqGlpSQHU^WaJ(CSzyVZlxO)x< zMsi?eZ3dZ{&DfeW<|6k$J5`vd;jm|;Q`TTJ_eAvEw zc>4JC+}Cw;pXJ8S>*eWrxDW4rcmglp-Yv)ZZn^nvc`@U#`53WP8$hmzY!V*$^>0>z zzI6f41e(~nG9HtZ*qhvLo1cLkNN$xL1lgRJ<^v8$W`)0FydW;^U zl+W(nBTTOw_p%*O%v9AVR{w@9?F1tj!w2ef22D_q6v>1^2?&B@qy)r3DL1uW>gp=m zBCcYBWFJ0H3sQk=?}g=jd31h^B5>< zWR8sLpoe6Lh}a}D>9jPRnk{B?R^o9#8q*R$kOh*j&n`vuDv?Z2&+M5aV>s5}=F$Zj zN|h8%(NKkG8qr9x-IO5$T0xIWTDDs>MRGTg)b_g!H&IIC{h30Lsz?(y5JnTU)}~2j z?q7ZZHH8qZ_i-^L6*}qEbW$2zKq+o#(aMcX+7(x!6HLVfDrHs`YAQ%U2_RKV8Zgp* zaO}UpUg6M^^Actg>OYHX+0^SyOg*_lgrUL_3F#FZ@>ED%~xN%e)0D1)tlSnU7L=rHQRUj2?e6Q3s+&M?4gN_jPCHB z+ZyW$j~~Xb-=A~S$?ne253Bb`U}Rqh5t4)IP1Ur$IJ~}@UnX)JTclqFb^ByCfDhkq z8v5qt4El@L5{b(h$J^!2%j5lVIvi$clEA3+L?r=smMcqXrIg?L@tfuD&QyU24V5&> z)J<$Ob5qv!sGblzDuqThmHoy-p)**RpM5DHH4s7u>n@mON=alylp9z`%Lrx&6-ZA< zI+dVGZtLi0-|m-r(dl_h-#*;m+s%>RKV2@T>L0&EwlVspZ|mru7>I;7vdKh|%38`U zcTX@P`WWM>kEan^_sAZ(MfyMv&$Y+eH{UV4BDLEhk=Yf|fE1 z=ahPvQ;JT9LHsumTl()7BGiefc9iEoUtVab8NY~pD7!duxtubsHHSm zL)mjg(J00gA{BL1)Vf0ooQ_X7M9mZgu>*`hp~L}bZ5d2vCL2UWP1&@ms1_|rC~2V9 zkyjB#QcO(Egl$Ko#C5F%Z4iQ*Vx-Z8qS8ukf&GNJsy8I0PE3G+kLV*tl|~E4R#q3G zDowdG9A;aVX`b}ZtTk<|O|4C;CThA1n3GaBxr_>F)dReU{Lr8N$=jv)STB(qqvxe(r?}cbZ6DvSe!h&yj8nRgj@SZjKzctThU#o~ zXm-C$ua1Y$Zf`z+bNJ%z?c2{@zJ2@Zhsz1^!$(kcy1T>5#HT5-})u>B8~HOcyV}nH{Gd}gr<8`x168AMURi}ows+U zns@iK#`TPO*7?xpW4phdkB3%bJFnV#LgX541c%4%4LG4+o_suOo)D+Z6Z3=SgYc30 zNC_38Nk)MVS^Qy=qGA*Uj zLDYlW_I!Jgr@oz@9^bsYYxDHs`Eq`8U~*ZuA-(q=qX3)Y2?-HV$_Vd0_AjE28y{=r z2zaK)7`_en1gTAc=gW8)V{?o$#{!K}FF>AKPh<*G0uq&ZhSq1#r2xA$F=|WB33Hj# zZrkD2ah@5B^sMI8bzD*5?pl6cUswul9*LdE3!-LMf=}mXWNtj#vhtsPoB z>a4-fH{g=eRkKYwHEI)^MXe<3DrNi9THln6 zV&CA@&F~Y^UFy?@Ynw4YIpbIK$5DAjndai9kp($UY4kt!4umIi`RYC$kdycWX!9T6#I$MX>*MqEW75s6){Db{F{-W;Z7)_HDI z&0L#KW=(~KD&nU;)4q6ODnjGx8A?EZ?@vA_8Qg#UcA@&U{xDB%nq+g|B8OvmW~LUv zWgp+~0MN!=ACY_i2_k_hc8~;fA!|Y4?chW+2^lcy}iGEd2F|{&Qtkzam|la5o!hY?F)Wq20V(Ufj23L|meyq*{9x3?BLLaXdfa&F2$j z%yTXawo95=JIFlGH#gJW?Q~cgtQMaITPtP9CQe(xh2a6{r^BUsWkNMi1}4H=0X!7^LU!()I9BlbK}dj@Tdp)rJg6&m2M( zP7+RfX-^WV!VI`y4)c7uoae)|+|H*2kN4zn#X z9kk7Lz%)=qD0`Gr3bqMPl(@be!S$}@kpmc+Tc9T{)YT@RX3+_1RHZ7Lh&7l%t={7R zy|%}N3N-Fd)4C??SlkLC7Fb30Es_X9YiUZOSbZT%6*Hj;%G;m@Ev&hs-b+NNO64^S zfAxd!Ojjm_QlmnODNMD-A&~vR1mS|XI@4u`?InGT;aRJ{;=Wh=*HnXwCOVrUy=c-Zbb%y2 zGXkP4(X;6uyN?~?pGK)^=H}revcOYBCXH}O8f2>nduC#MvStuWc*1H|$28S8ad@74 z^nspP)2!=%&9d69XZ=A*rwro%L~xivE}QIjSH zX{vKmI~>}p`z8H$>#v`-@qF%&@1Ng&e0qG?PTRVj*6q?s_TjmCHBTO&AMfwlmi<@n zpT2wCrsML}Z@qbWFa5)5Jgk>D7z*tI7-r?Hbxj7x`n*Mt5wS5NBBcp(Y=KOfwXf;tJ|wED zSyQ>NnsDS)SOSeH+q~x1=qi3uH5;2Fvt5$SXmQSX4s=9HFn7p0RGFTx5y4&ZBT@Ns zI7IgO7byttDg{!liI@mwnp>M~nPr;LCKdfO3e3vxK$z~4=d#LQem}ouZr%@71^i0qY9A$WEj{hG$#Vz z72dfkKrk{Tv(;81_ZdNL2&bkh>|=}pADNjF;z&ncGMWpKTXxB$$uj9V@cxSrqg6PPn^xBRGC%h#ac=__?cfP*4e~BR*L^|nD;hT>K&-f66v%7Wk)l_W~ z^P4W7Dc;xNefS{R#Ojx^&`R8Y&w_h551ARx5QmP@wQrrw$=akwxL`ONvlSc31dbsP z=%meWmbg>vJ(!`?)OidM$p{H|q?0Pibl*HzWfQd)5glpaNO>xg zDrA$3$9f4VH4MTf%bimbE5Ze-+?yyn^F0c3LroP?Hfyu#LQzw-=`hbXtsS);%oejr zRkQHtg$vo$5{%tvR`H$v+Et(_M>ujIT~ai(s`H|x*F?9~eNh9j3=VY&G^mhKd}>H~ zjN-x75EW9}j);VkLyPXdljlm&1Ew&Y&>$4qm@*W)GvoHVs*Fzvz)Ftm&e*IlRCz#z z0KGKgDVj{ngbPJ-gi`9**%3#DOUq<7B65n+Tg8S%gpvqV?y=NHYo^B5)S8GgbyuhZ z$aJI@lZ974t?WlTJ7h#eO`oGUUXp3pN%pAaIaDyUiR8!(Y2ZeCx4KR1lZlid{$UdZOQW+(|sTWv6m9|IENk&C31Wcs+4iXlj zK{O<|yGac)BK*1>PLp(0Phe=>3$CVTOfc1<814Btf~m#m-~eXawKDo3f4dbM(PJXnb1TGZI*elYK)OGGQAHxjTobxvmhb`5G=x36O1bLdtmi=8a`yw z7ESa;JKieKmILz^4(6nhI6O*U*N@xfgfVKkEsV&$HAhdUJ1fD>YZ*C{9_obIv{<{< zc7#|HqnYYtZKh4p=0z8gW^FQ^g<7Pm;K&$x742prEsLJ4P?+ICU`Kj_##!5+yA}A-5L=c_{A3|+1&+1!d#E2RL5Sktksc%df5U^!(W!l*Kci*4J$LMQpTlA6X$iXpU zWKJX_p4T4h!%eJr_iMwJ_EO~jh`TRsf=$18{N>a6!AGw*nDrcg_qXfj%jaL&x~(QM zk`Xg~P$=1U`2pf16*hh(;E|R|)$1glSXNn};`{m=i z=Z`=C{0Bd5$Gh_C@HiY79~xGJcj=^_?T&GeGD2-0=+i59>~iEH=6jXqLF?%|2c z8Exa+!}P1M-I|RQ-?{^n&65%E);O6eg|rk7-+FFqbnKN62Zy?z9^L8CQb^5mRezx-mVXB0ttDm8mkNX#J|GXaTt@zlD)$ znI5FZhNfG3(R1|R3VJ_H39I_j)69EkjPbI5A zT^=+FL3AcUIKb*+XNBg=OH)nFvPl*^!x>_Qc9+~U#mTH2e$tbBVf#uc+;!j;ETKjQ zF{)i%%dT3z)(zF^Mc{Oa{m&r*4Y;djFinvgNfDE|sj1K)hLT8XMX7`Z*NO<)`;DdM zt<)U@bl)5GIuMZ6zlm$l+zk%!^jMQj;ah3q*)ndPb5NML90%j4768 zLctL4G5U^U9sn|WKmj46oY3_Y-i=DNN0NvT5fV)~H~Mh*4CL@iO)&rip%lrI9q-;H zf)P}i7VT?{&^BUMV+HmBT@C&#d@Y3zN*BN(Kwyj{a!^PaB-*T#VOn&Vby;MYdAH2R zoB1$L^P(a`RiQw@X-vnI6O#x}ANVvm-$q7oRv_czKAz93Z;ji%wz@xOhSs@%#R-=*3z~?8W^#ZW2}@6x zG18`;^0<#-i6Dnk2s%roe$78~PtQbZv0?W2S;BI6?&@gkma2F(H(cN~%H?Qtmy+ zE=1IXCR}NhU?8ZJNV>Qn!|N5T&PI~ZriGOs1Dl6}kTk_7{~8@;*q<>8uPb;7ld_uZ zF_FunjW~~p`r1)4qC_O9k#OJj$uVMtP*9}fX_CuPnxlp^g?rK5-*3WEE|jmkrAo%bT0q<1gRe{_^3UFVD}Ty8!h3=YKv6?*99K z{UhHVYR`ihqpyw*Q&nclh!~lkY^qGI5$F)cpsQ&q{h(q-&jC|XBD*ML8jD^=UbZ;3 zWqbWwk1wY2;_xjvZePCr{`*DsjybObAu5wa1R@Zg6sI)Nh@{YoOdqkeJ`I8W|3tl8 zk7Y@6rMH%u``#xaGO{YGn%$h{t%ra{q6VTQK!bT02-5!t^aBJ8bOA}Rnq9ZdxSX@M zyV=r%`N@7DKoXtIVB|S_yPK``ec0LseO?g*Y7=3^7`g4{mwsMvKfm$aD7#wwAZ7eMdaC=S|j0*dtxu zR2bShb3I?;WDx#xHRf?kg=WTNhbhj;0h)WC9NljtPUYJ;rY2gho;H;#h8a>LWJ=!K z0SU7K323-9-v~E}uHHBA7y89o->QdVMmG3nER1JunhnMoqGa@NLme_#UQ)>#dLber z=g7n2NW4pL@KK)8%J#FOqbr|Ov?rM_7J4nQf%I-Q3wry`m(krN!Ob4SIHjEO)6BPe1@ ziIwWkX;Fp)H~_Yw+`!^|g*BSh3f&8|XpTnoa%~D0gk>^Nqu&&0QiN0@64xqcc?mrJ zGBb(9ajaCU-=GtqeOn{eu$VAXrDt@6*O29;vLWSP#;lYv8)Nu140GZZ&6&3rQrT%6OGyXq=I^yuR!9K0m+CA3u+uzl_hX$MrhTbHt3S z8&jzfknFGqb>&i}Stc+!kGPKcKBqL;C)%_ocDBp*>G7LKK32R6Qe^z*yXXJyKmWsR zex8k}c#WaC8I+ok)#kxkt1{roT$>Y%z9?$x|^bBQlIJb2K{HO;=(JO(`3UmZFbP z=5XUoh&IOSfB!ds$-ZA_LLqIGmlI`V}jd+WE8@FHL zTvBdi=x$}mV(LOlEM~jpM)tkgQ_5++ZF}D?wmq^vc5luNq|uvuH)}L&opdy-D_6bv zmE-PfsLjyVho@4R^O*BZc}=~i-UIK{2}>HxB#&g>lIuyBUmNAT<9lq8^O;o1!cB`D zx1g&~#SWB^;MEb3Aro=`k7aGI059QgRg2dDU6xg?PEMtXm8~JAY(_@R)KnZoAwEKq z84FEWpN5P%bOvUw)}RuJw9FV9Lo@FZ$to4Gu7U<@>TZ35H&&*7p=IwGgnA{|8V_?O z63mtG%Hn#uXpsMh|NOg!0QmFo#Z{Xb38Ap+2vsYbK$3YUq%0D2&3f5zx%5uXh+B#w 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

slNF9=bRxAin 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�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 literal 0 HcmV?d00001 From 258dbcd622c21253e336d7669c026b06b38e9185 Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Tue, 11 Aug 2020 15:44:13 -0700 Subject: [PATCH 02/10] Init guetzli sandbox --- oss-internship-2020/guetzli/BUILD.bazel | 65 +++++ oss-internship-2020/guetzli/WORKSPACE | 79 ++++++ .../guetzli/external/png.BUILD | 33 +++ .../guetzli/external/zlib.BUILD | 36 +++ .../guetzli/guetzli_entry_points.cc | 245 ++++++++++++++++++ .../guetzli/guetzli_entry_points.h | 29 +++ oss-internship-2020/guetzli/guetzli_sandbox.h | 36 +++ .../guetzli/guetzli_sandboxed.cc | 160 ++++++++++++ .../guetzli/guetzli_transaction.cc | 160 ++++++++++++ .../guetzli/guetzli_transaction.h | 216 +++++++++++++++ oss-internship-2020/guetzli/tests/BUILD.bazel | 22 ++ .../guetzli/tests/guetzli_sapi_test.cc | 165 ++++++++++++ .../guetzli/tests/guetzli_transaction_test.cc | 2 + .../guetzli/tests/testdata/bees.png | Bin 0 -> 177424 bytes .../guetzli/tests/testdata/landscape.jpg | Bin 0 -> 14418 bytes 15 files changed, 1248 insertions(+) create mode 100644 oss-internship-2020/guetzli/BUILD.bazel create mode 100644 oss-internship-2020/guetzli/WORKSPACE create mode 100644 oss-internship-2020/guetzli/external/png.BUILD create mode 100644 oss-internship-2020/guetzli/external/zlib.BUILD create mode 100644 oss-internship-2020/guetzli/guetzli_entry_points.cc create mode 100644 oss-internship-2020/guetzli/guetzli_entry_points.h create mode 100644 oss-internship-2020/guetzli/guetzli_sandbox.h create mode 100644 oss-internship-2020/guetzli/guetzli_sandboxed.cc create mode 100644 oss-internship-2020/guetzli/guetzli_transaction.cc create mode 100644 oss-internship-2020/guetzli/guetzli_transaction.h create mode 100644 oss-internship-2020/guetzli/tests/BUILD.bazel create mode 100644 oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc create mode 100644 oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc create mode 100644 oss-internship-2020/guetzli/tests/testdata/bees.png create mode 100644 oss-internship-2020/guetzli/tests/testdata/landscape.jpg diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel new file mode 100644 index 0000000..345f879 --- /dev/null +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -0,0 +1,65 @@ +load("@rules_cc//cc:defs.bzl", "cc_proto_library") +load("@rules_proto//proto:defs.bzl", "proto_library") +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:proto.bzl", + "sapi_proto_library", +) +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", + "sapi_library", +) + +cc_library( + name = "guetzli_wrapper", + srcs = ["guetzli_entry_points.cc"], + hdrs = ["guetzli_entry_points.h"], + deps = [ + "@guetzli//:guetzli_lib", + "@com_google_sandboxed_api//sandboxed_api:lenval_core", + "@com_google_sandboxed_api//sandboxed_api:vars", + #"@com_google_sandboxed_api//sandboxed_api/sandbox2/util:temp_file", visibility error + "@png_archive//:png" + ], + visibility = ["//visibility:public"] +) + +sapi_library( + name = "guetzli_sapi", + #srcs = ["guetzli_transaction.cc"], // Error when try to place definitions insde .cc file + hdrs = ["guetzli_sandbox.h", "guetzli_transaction.h"], + functions = [ + "ProcessJPEGString", + "ProcessRGBData", + "ButteraugliScoreQuality", + "ReadPng", + "ReadJpegData", + "ReadDataFromFd", + "WriteDataToFd" + ], + input_files = ["guetzli_entry_points.h"], + lib = ":guetzli_wrapper", + lib_name = "Guetzli", + visibility = ["//visibility:public"], + namespace = "guetzli::sandbox" +) + +# cc_library( +# name = "guetzli_sapi_transaction", +# #srcs = ["guetzli_transaction.cc"], +# hdrs = ["guetzli_transaction.h"], +# deps = [ +# ":guetzli_sapi" +# ], +# visibility = ["//visibility:public"] +# ) + +cc_binary( + name="guetzli_sandboxed", + srcs=["guetzli_sandboxed.cc"], + includes = ["."], + visibility= [ "//visibility:public" ], + deps = [ + #":guetzli_sapi_transaction" + ":guetzli_sapi" + ] +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/WORKSPACE b/oss-internship-2020/guetzli/WORKSPACE new file mode 100644 index 0000000..17887de --- /dev/null +++ b/oss-internship-2020/guetzli/WORKSPACE @@ -0,0 +1,79 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +workspace(name = "guetzli_sandboxed") + +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +# Include the Sandboxed API dependency if it does not already exist in this +# project. This ensures that this workspace plays well with other external +# dependencies that might use Sandboxed API. +maybe( + git_repository, + name = "com_google_sandboxed_api", + # This example depends on the latest master. In an embedding project, it + # is advisable to pin Sandboxed API to a specific revision instead. + # commit = "ba47adc21d4c9bc316f3c7c32b0faaef952c111e", # 2020-05-15 + branch = "master", + remote = "https://github.com/google/sandboxed-api.git", +) + +# From here on, Sandboxed API files are available. The statements below setup +# transitive dependencies such as Abseil. Like above, those will only be +# included if they don't already exist in the project. +load( + "@com_google_sandboxed_api//sandboxed_api/bazel:sapi_deps.bzl", + "sapi_deps", +) + +sapi_deps() + +# Need to separately setup Protobuf dependencies in order for the build rules +# to work. +load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") + +protobuf_deps() + +maybe( + git_repository, + name = "guetzli", + remote = "https://github.com/google/guetzli.git", + branch = "master" +) + +maybe( + git_repository, + name = "googletest", + remote = "https://github.com/google/googletest", + tag = "release-1.10.0" +) + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +http_archive( + name = "png_archive", + build_file = "png.BUILD", + sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", + strip_prefix = "libpng-1.2.57", + url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", +) + +http_archive( + name = "zlib_archive", + build_file = "zlib.BUILD", + sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", + strip_prefix = "zlib-1.2.10", + url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/external/png.BUILD b/oss-internship-2020/guetzli/external/png.BUILD new file mode 100644 index 0000000..9ff982b --- /dev/null +++ b/oss-internship-2020/guetzli/external/png.BUILD @@ -0,0 +1,33 @@ +# Description: +# libpng is the official PNG reference library. + +licenses(["notice"]) # BSD/MIT-like license + +cc_library( + name = "png", + srcs = [ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", + ], + hdrs = [ + "png.h", + "pngconf.h", + ], + includes = ["."], + linkopts = ["-lm"], + visibility = ["//visibility:public"], + deps = ["@zlib_archive//:zlib"], +) diff --git a/oss-internship-2020/guetzli/external/zlib.BUILD b/oss-internship-2020/guetzli/external/zlib.BUILD new file mode 100644 index 0000000..edb77fd --- /dev/null +++ b/oss-internship-2020/guetzli/external/zlib.BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # BSD/MIT-like license (for zlib) + +cc_library( + name = "zlib", + srcs = [ + "adler32.c", + "compress.c", + "crc32.c", + "crc32.h", + "deflate.c", + "deflate.h", + "gzclose.c", + "gzguts.h", + "gzlib.c", + "gzread.c", + "gzwrite.c", + "infback.c", + "inffast.c", + "inffast.h", + "inffixed.h", + "inflate.c", + "inflate.h", + "inftrees.c", + "inftrees.h", + "trees.c", + "trees.h", + "uncompr.c", + "zconf.h", + "zutil.c", + "zutil.h", + ], + hdrs = ["zlib.h"], + includes = ["."], +) diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc new file mode 100644 index 0000000..446150c --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -0,0 +1,245 @@ +#include "guetzli/jpeg_data_reader.h" +#include "guetzli/quality.h" +#include "guetzli_entry_points.h" +#include "png.h" +#include "sandboxed_api/sandbox2/util/fileops.h" + +#include +#include +#include +#include +#include + +namespace { + +inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { + return (static_cast(val) * static_cast(alpha) + 128) / 255; +} + +template +void CopyMemoryToLenVal(const T* data, size_t size, + sapi::LenValStruct* out_data) { + free(out_data->data); // Not sure about this + out_data->size = size; + T* new_out = static_cast(malloc(size)); + memcpy(new_out, data, size); + out_data->data = new_out; +} + +} // namespace + +extern "C" bool ProcessJPEGString(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* in_data, + sapi::LenValStruct* out_data) +{ + std::string in_data_temp(static_cast(in_data->data), + in_data->size); + + guetzli::ProcessStats stats; + if (verbose > 0) { + stats.debug_output_file = stderr; + } + + std::string temp_out = ""; + auto result = guetzli::Process(*params, &stats, in_data_temp, &temp_out); + + if (result) { + CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); + } + + return result; +} + +extern "C" bool ProcessRGBData(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* rgb, + int w, int h, + sapi::LenValStruct* out_data) +{ + std::vector in_data_temp; + in_data_temp.reserve(rgb->size); + + auto* rgb_data = static_cast(rgb->data); + std::copy(rgb_data, rgb_data + rgb->size, std::back_inserter(in_data_temp)); + + guetzli::ProcessStats stats; + if (verbose > 0) { + stats.debug_output_file = stderr; + } + + std::string temp_out = ""; + auto result = + guetzli::Process(*params, &stats, in_data_temp, w, h, &temp_out); + + //TODO: Move shared part of the code to another function + if (result) { + CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); + } + + return result; +} + +extern "C" bool ReadPng(sapi::LenValStruct* in_data, + int* xsize, int* ysize, + sapi::LenValStruct* rgb_out) +{ + std::string data(static_cast(in_data->data), in_data->size); + std::vector rgb; + + png_structp png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png_ptr) { + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, nullptr, nullptr); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr)) != 0) { + // Ok we are here because of the setjmp. + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + + std::istringstream memstream(data, std::ios::in | std::ios::binary); + png_set_read_fn(png_ptr, static_cast(&memstream), [](png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead) { + std::istringstream& memstream = *static_cast(png_get_io_ptr(png_ptr)); + + memstream.read(reinterpret_cast(outBytes), byteCountToRead); + + if (memstream.eof()) png_error(png_ptr, "unexpected end of data"); + if (memstream.fail()) png_error(png_ptr, "read from memory error"); + }); + + // The png_transforms flags are as follows: + // packing == convert 1,2,4 bit images, + // strip == 16 -> 8 bits / channel, + // shift == use sBIT dynamics, and + // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. + const unsigned int png_transforms = + PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; + + png_read_png(png_ptr, info_ptr, png_transforms, nullptr); + + png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); + + *xsize = png_get_image_width(png_ptr, info_ptr); + *ysize = png_get_image_height(png_ptr, info_ptr); + rgb.resize(3 * (*xsize) * (*ysize)); + + const int components = png_get_channels(png_ptr, info_ptr); + switch (components) { + case 1: { + // GRAYSCALE + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = row_in[x]; + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 2: { + // GRAYSCALE + ALPHA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); + row_out[3 * x + 0] = gray; + row_out[3 * x + 1] = gray; + row_out[3 * x + 2] = gray; + } + } + break; + } + case 3: { + // RGB + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + memcpy(row_out, row_in, 3 * (*xsize)); + } + break; + } + case 4: { + // RGBA + for (int y = 0; y < *ysize; ++y) { + const uint8_t* row_in = row_pointers[y]; + uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + for (int x = 0; x < *xsize; ++x) { + const uint8_t alpha = row_in[4 * x + 3]; + row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); + row_out[3 * x + 1] = BlendOnBlack(row_in[4 * x + 1], alpha); + row_out[3 * x + 2] = BlendOnBlack(row_in[4 * x + 2], alpha); + } + } + break; + } + default: + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + return false; + } + png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); + + CopyMemoryToLenVal(rgb.data(), rgb.size(), rgb_out); + + return true; +} + +extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, + int mode, + int* xsize, int* ysize) +{ + std::string data(static_cast(in_data->data), in_data->size); + guetzli::JPEGData jpg; + + auto result = guetzli::ReadJpeg(data, + static_cast(mode), &jpg); + + if (result) { + *xsize = jpg.width; + *ysize = jpg.height; + } + + return result; +} + +extern "C" double ButteraugliScoreQuality(double quality) { + return guetzli::ButteraugliScoreForQuality(quality); +} + +extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data) { + struct stat file_data; + auto status = fstat(fd, &file_data); + + if (status < 0) { + return false; + } + + auto fsize = file_data.st_size; + + std::unique_ptr buf(new char[fsize]); + status = read(fd, buf.get(), fsize); + + if (status < 0) { + return false; + } + + CopyMemoryToLenVal(buf.get(), fsize, out_data); + + return true; +} + +extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data) { + return sandbox2::file_util::fileops::WriteToFD(fd, + static_cast(data->data), data->size); +} \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.h b/oss-internship-2020/guetzli/guetzli_entry_points.h new file mode 100644 index 0000000..6fd0f11 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_entry_points.h @@ -0,0 +1,29 @@ +#pragma once + +#include "guetzli/processor.h" +#include "sandboxed_api/lenval_core.h" +#include "sandboxed_api/vars.h" + +extern "C" bool ProcessJPEGString(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* in_data, + sapi::LenValStruct* out_data); + +extern "C" bool ProcessRGBData(const guetzli::Params* params, + int verbose, + sapi::LenValStruct* rgb, + int w, int h, + sapi::LenValStruct* out_data); + +extern "C" bool ReadPng(sapi::LenValStruct* in_data, + int* xsize, int* ysize, + sapi::LenValStruct* rgb_out); + +extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, + int mode, int* xsize, int* ysize); + +extern "C" double ButteraugliScoreQuality(double quality); + +extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data); + +extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data); \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h new file mode 100644 index 0000000..3fa2b10 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +#include "guetzli_sapi.sapi.h" +#include "sandboxed_api/sandbox2/policy.h" +#include "sandboxed_api/sandbox2/policybuilder.h" +#include "sandboxed_api/util/flag.h" + +namespace guetzli { +namespace sandbox { + +class GuetzliSapiSandbox : public GuetzliSandbox { + public: + std::unique_ptr ModifyPolicy( + sandbox2::PolicyBuilder*) override { + + return sandbox2::PolicyBuilder() + .AllowStaticStartup() + .AllowRead() + .AllowSystemMalloc() + .AllowWrite() + .AllowExit() + .AllowStat() + .AllowSyscalls({ + __NR_futex, + __NR_close, + __NR_recvmsg // Seems like this one needed to work with remote file descriptors + }) + .BuildOrDie(); + } +}; + +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc new file mode 100644 index 0000000..a62f249 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -0,0 +1,160 @@ +#include +#include +#include +#include +#include + +#include + +#include "guetzli_transaction.h" +#include "sandboxed_api/sandbox2/util/fileops.h" +#include "sandboxed_api/util/statusor.h" + +namespace { + +constexpr int kDefaultJPEGQuality = 95; +constexpr int kDefaultMemlimitMB = 6000; // in MB +//constexpr absl::string_view kMktempSuffix = "XXXXXX"; + +// sapi::StatusOr> CreateNamedTempFile( +// absl::string_view prefix) { +// std::string name_template = absl::StrCat(prefix, kMktempSuffix); +// int fd = mkstemp(&name_template[0]); +// if (fd < 0) { +// return absl::UnknownError("Error creating temp file"); +// } +// return std::pair{std::move(name_template), fd}; +// } + +void TerminateHandler() { + fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" + "Make sure that there is 300MB/MPix of memory available.\n"); + exit(1); +} + +void Usage() { + fprintf(stderr, + "Guetzli JPEG compressor. Usage: \n" + "guetzli [flags] input_filename output_filename\n" + "\n" + "Flags:\n" + " --verbose - Print a verbose trace of all attempts to standard output.\n" + " --quality Q - Visual quality to aim for, expressed as a JPEG quality value.\n" + " Default value is %d.\n" + " --memlimit M - Memory limit in MB. Guetzli will fail if unable to stay under\n" + " the limit. Default limit is %d MB.\n" + " --nomemlimit - Do not limit memory usage.\n", kDefaultJPEGQuality, kDefaultMemlimitMB); + exit(1); +} + +} // namespace + +int main(int argc, const char** argv) { + std::set_terminate(TerminateHandler); + + int verbose = 0; + int quality = kDefaultJPEGQuality; + int memlimit_mb = kDefaultMemlimitMB; + + int opt_idx = 1; + for(;opt_idx < argc;opt_idx++) { + if (strnlen(argv[opt_idx], 2) < 2 || argv[opt_idx][0] != '-' || argv[opt_idx][1] != '-') + break; + + if (!strcmp(argv[opt_idx], "--verbose")) { + verbose = 1; + } else if (!strcmp(argv[opt_idx], "--quality")) { + opt_idx++; + if (opt_idx >= argc) + Usage(); + quality = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--memlimit")) { + opt_idx++; + if (opt_idx >= argc) + Usage(); + memlimit_mb = atoi(argv[opt_idx]); + } else if (!strcmp(argv[opt_idx], "--nomemlimit")) { + memlimit_mb = -1; + } else if (!strcmp(argv[opt_idx], "--")) { + opt_idx++; + break; + } else { + fprintf(stderr, "Unknown commandline flag: %s\n", argv[opt_idx]); + Usage(); + } + } + + if (argc - opt_idx != 2) { + Usage(); + } + + sandbox2::file_util::fileops::FDCloser in_fd_closer( + open(argv[opt_idx], O_RDONLY)); + + if (in_fd_closer.get() < 0) { + fprintf(stderr, "Can't open input file: %s\n", argv[opt_idx]); + return 1; + } + + // auto out_temp_file = CreateNamedTempFile("/tmp/" + std::string(argv[opt_idx + 1])); + // if (!out_temp_file.ok()) { + // fprintf(stderr, "Can't create temporary output file: %s\n", + // argv[opt_idx + 1]); + // return 1; + // } + // sandbox2::file_util::fileops::FDCloser out_fd_closer( + // out_temp_file.value().second); + + // if (unlink(out_temp_file.value().first.c_str()) < 0) { + // fprintf(stderr, "Error unlinking temp out file: %s\n", + // out_temp_file.value().first.c_str()); + // return 1; + // } + + sandbox2::file_util::fileops::FDCloser out_fd_closer( + open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); + + if (out_fd_closer.get() < 0) { + fprintf(stderr, "Can't create temporary output file: %s\n", argv[opt_idx]); + return 1; + } + + // sandbox2::file_util::fileops::FDCloser out_fd_closer(open(argv[opt_idx + 1], + // O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)); + + // if (out_fd_closer.get() < 0) { + // fprintf(stderr, "Can't open output file: %s\n", argv[opt_idx + 1]); + // return 1; + // } + + guetzli::sandbox::TransactionParams params = { + in_fd_closer.get(), + out_fd_closer.get(), + verbose, + quality, + memlimit_mb + }; + + guetzli::sandbox::GuetzliTransaction transaction(std::move(params)); + auto result = transaction.Run(); + + if (result.ok()) { + if (access(argv[opt_idx + 1], F_OK) != -1) { + if (remove(argv[opt_idx + 1]) < 0) { + fprintf(stderr, "Error deleting existing output file: %s\n", + argv[opt_idx + 1]); + } + } + + std::stringstream path; + path << "/proc/self/fd/" << out_fd_closer.get(); + linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1], + AT_SYMLINK_FOLLOW); + } + else { + fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? + return 1; + } + + return 0; +} \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc new file mode 100644 index 0000000..9403783 --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -0,0 +1,160 @@ +#include "guetzli_transaction.h" + +#include + +namespace guetzli { +namespace sandbox { + +absl::Status GuetzliTransaction::Init() { + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); + + if (in_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote input fd is set to -1"); + } + if (out_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote output fd is set to -1"); + } + + return absl::OkStatus(); +} + + absl::Status GuetzliTransaction::ProcessPng(GuetzliAPi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + sapi::v::LenVal rgb_in(0); + + auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_in.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading PNG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, + rgb_in.PtrBefore(), xsize.GetValue(), + ysize.GetValue(), output->PtrBoth()); + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + + absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + ::sapi::v::Int xsize; + ::sapi::v::Int ysize; + auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), + ysize.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading JPG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, + input->PtrBefore(), output->PtrBoth()); + + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + +absl::Status GuetzliTransaction::Main() { + GuetzliApi api(sandbox()); + + sapi::v::LenVal input(0); + sapi::v::LenVal output(0); + sapi::v::Struct params; + + auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading data inside sandbox" + ); + } + + auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); + + if (!score_quality_result.ok()) { + return absl::FailedPreconditionError( + "Error calculating butteraugli score" + ); + } + + params.mutable_data()->butteraugli_target = score_quality_result.value(); + + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + + if (input.GetDataSize() >= 8 && + memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { + auto process_status = ProcessPng(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } else { + auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } + + auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), + output.PtrBefore()); + + if (!write_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error writing file inside sandbox" + ); + } + + return absl::OkStatus(); +} + +time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( + uint64_t pixels) const { + return (pixels / kMpixPixels + 5) * 60; +} + +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h new file mode 100644 index 0000000..5d6e83a --- /dev/null +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -0,0 +1,216 @@ +#pragma once + +#include +#include + +#include "guetzli_sandbox.h" +#include "sandboxed_api/transaction.h" +#include "sandboxed_api/vars.h" + +namespace guetzli { +namespace sandbox { + +constexpr int kDefaultTransactionRetryCount = 1; +constexpr uint64_t kMpixPixels = 1'000'000; + +constexpr int kBytesPerPixel = 350; +constexpr int kLowestMemusageMB = 100; // in MB + +struct TransactionParams { + int in_fd; + int out_fd; + int verbose; + int quality; + int memlimit_mb; +}; + +//Add optional time limit/retry count as a constructors arguments +//Use differenet status errors +class GuetzliTransaction : public sapi::Transaction { + public: + GuetzliTransaction(TransactionParams&& params) + : sapi::Transaction(std::make_unique()) + , params_(std::move(params)) + , in_fd_(params_.in_fd) + , out_fd_(params_.out_fd) + { + sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount); + sapi::Transaction::SetTimeLimit(0); + } + + private: + absl::Status Init() override; + absl::Status Main() final; + + absl::Status ProcessPng(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const; + + absl::Status ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const; + + // As guetzli takes roughly 1 minute of CPU per 1 MPix we need to calculate + // approximate time for transaction to complete + time_t CalculateTimeLimitFromImageSize(uint64_t pixels) const; + + const TransactionParams params_; + sapi::v::Fd in_fd_; + sapi::v::Fd out_fd_; +}; + +absl::Status GuetzliTransaction::Init() { + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); + + if (in_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote input fd is set to -1"); + } + if (out_fd_.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote output fd is set to -1"); + } + + return absl::OkStatus(); +} + + absl::Status GuetzliTransaction::ProcessPng(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + sapi::v::LenVal rgb_in(0); + + auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_in.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading PNG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, + rgb_in.PtrBefore(), xsize.GetValue(), + ysize.GetValue(), output->PtrBoth()); + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + + absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, + sapi::v::Struct* params, + sapi::v::LenVal* input, + sapi::v::LenVal* output) const { + sapi::v::Int xsize; + sapi::v::Int ysize; + auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), + ysize.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading JPG data from input file" + ); + } + + double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); + if (params_.memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb + || params_.memlimit_mb < kLowestMemusageMB)) { + return absl::FailedPreconditionError( + "Memory limit would be exceeded" + ); + } + + auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, + input->PtrBefore(), output->PtrBoth()); + + if (!result.value_or(false)) { + return absl::FailedPreconditionError( + "Guetzli processing failed" + ); + } + + return absl::OkStatus(); + } + +absl::Status GuetzliTransaction::Main() { + GuetzliApi api(sandbox()); + + sapi::v::LenVal input(0); + sapi::v::LenVal output(0); + sapi::v::Struct params; + + auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); + + if (!read_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error reading data inside sandbox" + ); + } + + auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); + + if (!score_quality_result.ok()) { + return absl::FailedPreconditionError( + "Error calculating butteraugli score" + ); + } + + params.mutable_data()->butteraugli_target = score_quality_result.value(); + + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + + if (input.GetDataSize() >= 8 && + memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { + auto process_status = ProcessPng(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } else { + auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); + + if (!process_status.ok()) { + return process_status; + } + } + + auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), + output.PtrBefore()); + + if (!write_result.value_or(false)) { + return absl::FailedPreconditionError( + "Error writing file inside sandbox" + ); + } + + return absl::OkStatus(); +} + +time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( + uint64_t pixels) const { + return (pixels / kMpixPixels + 5) * 60; +} + +} // namespace sandbox +} // namespace guetzli diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel new file mode 100644 index 0000000..b7c7517 --- /dev/null +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -0,0 +1,22 @@ +# cc_test( +# name = "transaction_tests", +# srcs = ["guetzli_transaction_test.cc"], +# visibility=["//visibility:public"], +# includes = ["."], +# deps = [ +# "//:guetzli_sapi", +# "@googletest//:gtest_main" +# ], +# ) + +# cc_test( +# name = "sapi_lib_tests", +# srcs = ["guetzli_sapi_test.cc"], +# visibility=["//visibility:public"], +# includes=[".."], +# deps = [ +# "//:guetzli_sapi", +# "@googletest//:gtest_main" +# ], +# data = glob(["testdata/*"]) +# ) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc new file mode 100644 index 0000000..73e54d6 --- /dev/null +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -0,0 +1,165 @@ +#include "gtest/gtest.h" +#include "guetzli_sandbox.h" +#include "sandboxed_api/sandbox2/util/fileops.h" +#include "sandboxed_api/vars.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace guetzli { +namespace sandbox { +namespace tests { + +namespace { + +constexpr const char* IN_PNG_FILENAME = "bees.png"; +constexpr const char* IN_JPG_FILENAME = "landscape.jpg"; + +constexpr int IN_PNG_FILE_SIZE = 177'424; +constexpr int IN_JPG_FILE_SIZE = 14'418; + +constexpr int DEFAULT_QUALITY_TARGET = 95; + +constexpr const char* RELATIVE_PATH_TO_TESTDATA = + "/guetzli/guetzli-sandboxed/tests/testdata/"; + +std::string GetPathToInputFile(const char* filename) { + return std::string(getenv("TEST_SRCDIR")) + + std::string(RELATIVE_PATH_TO_TESTDATA) + + std::string(filename); +} + +std::string ReadFromFile(const std::string& filename) { + std::ifstream stream(filename, std::ios::binary); + + if (!stream.is_open()) { + return ""; + } + + std::stringstream result; + result << stream.rdbuf(); + return result.str(); +} + +} // namespace + +class GuetzliSapiTest : public ::testing::Test { +protected: + void SetUp() override { + sandbox_ = std::make_unique(); + sandbox_->Init().IgnoreError(); + api_ = std::make_unique(sandbox_.get()); + } + + std::unique_ptr sandbox_; + std::unique_ptr api_; +}; + +TEST_F(GuetzliSapiTest, ReadDataFromFd) { + std::string input_file_path = GetPathToInputFile(IN_PNG_FILENAME); + int fd = open(input_file_path.c_str(), O_RDONLY); + ASSERT_TRUE(fd != -1) << "Error opening input file"; + sapi::v::Fd remote_fd(fd); + auto send_fd_status = sandbox_->TransferToSandboxee(&remote_fd); + ASSERT_TRUE(send_fd_status.ok()) << "Error sending fd to sandboxee"; + ASSERT_TRUE(remote_fd.GetRemoteFd() != -1) << "Error opening remote fd"; + sapi::v::LenVal data(0); + auto read_status = + api_->ReadDataFromFd(remote_fd.GetRemoteFd(), data.PtrBoth()); + ASSERT_TRUE(read_status.value_or(false)) << "Error reading data from fd"; + ASSERT_EQ(data.GetDataSize(), IN_PNG_FILE_SIZE) << "Wrong size of file"; +} + +// TEST_F(GuetzliSapiTest, WriteDataToFd) { + +// } + +TEST_F(GuetzliSapiTest, ReadPng) { + std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); + ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + sapi::v::LenVal rgb_out(0); + + auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_out.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; + ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; +} + +TEST_F(GuetzliSapiTest, ReadJpeg) { + std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); + ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + + auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, + xsize.PtrBoth(), ysize.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; + ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; +} + +// This test can take up to few minutes depending on your hardware +TEST_F(GuetzliSapiTest, ProcessRGB) { + std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); + ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + sapi::v::LenVal rgb_out(0); + + auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), + ysize.PtrBoth(), rgb_out.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; + ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; + auto quality = + api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); + ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; + sapi::v::Struct params; + sapi::v::LenVal out_data(0); + params.mutable_data()->butteraugli_target = quality.value(); + + status = api_->ProcessRGBData(params.PtrBefore(), 0, rgb_out.PtrBefore(), + xsize.GetValue(), ysize.GetValue(), out_data.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing png file"; + ASSERT_EQ(out_data.GetDataSize(), 38'625); + //ADD COMPARSION WITH REFERENCE OUTPUT +} + +// This test can take up to few minutes depending on your hardware +TEST_F(GuetzliSapiTest, ProcessJpeg) { + std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); + ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; + sapi::v::LenVal in_data(data.data(), data.size()); + sapi::v::Int xsize, ysize; + + auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, + xsize.PtrBoth(), ysize.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; + ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; + ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; + + auto quality = + api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); + ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; + sapi::v::Struct params; + params.mutable_data()->butteraugli_target = quality.value(); + sapi::v::LenVal out_data(0); + + status = api_->ProcessJPEGString(params.PtrBefore(), 0, in_data.PtrBefore(), + out_data.PtrBoth()); + ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg file"; + ASSERT_EQ(out_data.GetDataSize(), 10'816); + //ADD COMPARSION WITH REFERENCE OUTPUT +} + +} // namespace tests +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc new file mode 100644 index 0000000..8471d1e --- /dev/null +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -0,0 +1,2 @@ +#include "gtest/gtest.h" +#include "guetzli_transaction.h" diff --git a/oss-internship-2020/guetzli/tests/testdata/bees.png b/oss-internship-2020/guetzli/tests/testdata/bees.png new file mode 100644 index 0000000000000000000000000000000000000000..11640c7488fb114127f869572d7ffec132e77746 GIT binary patch literal 177424 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%0D2&3f5zx%5uXh+B#w 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

slNF9=bRxAin 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�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 literal 0 HcmV?d00001 From d41a3d6d16bccb057aa78ffc68a9a65219c9ce7e Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Tue, 11 Aug 2020 15:48:48 -0700 Subject: [PATCH 03/10] First version of guetzli sandbox --- oss-internship-2020/guetzli/.bazelrc | 2 + oss-internship-2020/guetzli/BUILD.bazel | 31 +- oss-internship-2020/guetzli/WORKSPACE | 31 +- .../guetzli/external/guetzli.BUILD | 18 + .../guetzli/guetzli_entry_points.cc | 237 +- .../guetzli/guetzli_entry_points.h | 31 +- .../guetzli/guetzli_sandboxed.cc | 44 +- .../guetzli/guetzli_transaction.cc | 180 +- .../guetzli/guetzli_transaction.h | 190 +- .../guetzli/third_party/butteraugli/BUILD | 24 + .../guetzli/third_party/butteraugli/LICENSE | 201 ++ .../guetzli/third_party/butteraugli/README.md | 68 + .../guetzli/third_party/butteraugli/WORKSPACE | 25 + .../butteraugli/butteraugli/Makefile | 7 + .../butteraugli/butteraugli/butteraugli.cc | 1994 +++++++++++++++++ .../butteraugli/butteraugli/butteraugli.h | 619 +++++ .../butteraugli/butteraugli_main.cc | 457 ++++ .../third_party/butteraugli/jpeg.BUILD | 89 + .../guetzli/third_party/butteraugli/png.BUILD | 33 + .../third_party/butteraugli/zlib.BUILD | 36 + 20 files changed, 3835 insertions(+), 482 deletions(-) create mode 100644 oss-internship-2020/guetzli/.bazelrc create mode 100644 oss-internship-2020/guetzli/external/guetzli.BUILD create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/BUILD create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/LICENSE create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/README.md create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD create mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD diff --git a/oss-internship-2020/guetzli/.bazelrc b/oss-internship-2020/guetzli/.bazelrc new file mode 100644 index 0000000..a68e070 --- /dev/null +++ b/oss-internship-2020/guetzli/.bazelrc @@ -0,0 +1,2 @@ +# Build in C++17 mode without a custom CROSSTOOL +build --cxxopt=-std=c++17 diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel index 345f879..6e3332a 100644 --- a/oss-internship-2020/guetzli/BUILD.bazel +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -1,9 +1,3 @@ -load("@rules_cc//cc:defs.bzl", "cc_proto_library") -load("@rules_proto//proto:defs.bzl", "proto_library") -load( - "@com_google_sandboxed_api//sandboxed_api/bazel:proto.bzl", - "sapi_proto_library", -) load( "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", "sapi_library", @@ -17,23 +11,17 @@ cc_library( "@guetzli//:guetzli_lib", "@com_google_sandboxed_api//sandboxed_api:lenval_core", "@com_google_sandboxed_api//sandboxed_api:vars", - #"@com_google_sandboxed_api//sandboxed_api/sandbox2/util:temp_file", visibility error "@png_archive//:png" ], - visibility = ["//visibility:public"] ) sapi_library( name = "guetzli_sapi", - #srcs = ["guetzli_transaction.cc"], // Error when try to place definitions insde .cc file + srcs = ["guetzli_transaction.cc"], hdrs = ["guetzli_sandbox.h", "guetzli_transaction.h"], functions = [ - "ProcessJPEGString", - "ProcessRGBData", - "ButteraugliScoreQuality", - "ReadPng", - "ReadJpegData", - "ReadDataFromFd", + "ProcessJpeg", + "ProcessRgb", "WriteDataToFd" ], input_files = ["guetzli_entry_points.h"], @@ -43,23 +31,10 @@ sapi_library( namespace = "guetzli::sandbox" ) -# cc_library( -# name = "guetzli_sapi_transaction", -# #srcs = ["guetzli_transaction.cc"], -# hdrs = ["guetzli_transaction.h"], -# deps = [ -# ":guetzli_sapi" -# ], -# visibility = ["//visibility:public"] -# ) - cc_binary( name="guetzli_sandboxed", srcs=["guetzli_sandboxed.cc"], - includes = ["."], - visibility= [ "//visibility:public" ], deps = [ - #":guetzli_sapi_transaction" ":guetzli_sapi" ] ) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/WORKSPACE b/oss-internship-2020/guetzli/WORKSPACE index 17887de..f7ec64c 100644 --- a/oss-internship-2020/guetzli/WORKSPACE +++ b/oss-internship-2020/guetzli/WORKSPACE @@ -15,6 +15,7 @@ workspace(name = "guetzli_sandboxed") load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") # Include the Sandboxed API dependency if it does not already exist in this @@ -46,22 +47,19 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") protobuf_deps() -maybe( - git_repository, +local_repository( + name = "butteraugli", + path = "third_party/butteraugli/" +) + +http_archive( name = "guetzli", - remote = "https://github.com/google/guetzli.git", - branch = "master" + build_file = "guetzli.BUILD", + sha256 = "39632357e49db83d9560bf0de560ad833352f36d23b109b0e995b01a37bddb57", + strip_prefix = "guetzli-master", + url = "https://github.com/google/guetzli/archive/master.zip" ) -maybe( - git_repository, - name = "googletest", - remote = "https://github.com/google/googletest", - tag = "release-1.10.0" -) - -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - http_archive( name = "png_archive", build_file = "png.BUILD", @@ -76,4 +74,11 @@ http_archive( sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", strip_prefix = "zlib-1.2.10", url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", +) + +maybe( + git_repository, + name = "googletest", + remote = "https://github.com/google/googletest", + tag = "release-1.10.0" ) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/external/guetzli.BUILD b/oss-internship-2020/guetzli/external/guetzli.BUILD new file mode 100644 index 0000000..dec29a1 --- /dev/null +++ b/oss-internship-2020/guetzli/external/guetzli.BUILD @@ -0,0 +1,18 @@ +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "guetzli_lib", + srcs = glob( + [ + "guetzli/*.h", + "guetzli/*.cc", + "guetzli/*.inc", + ], + exclude = ["guetzli/guetzli.cc"], + ), + copts = [ "-Wno-sign-compare" ], + visibility= [ "//visibility:public" ], + deps = [ + "@butteraugli//:butteraugli_lib", + ], +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc index 446150c..a815673 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.cc +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -3,18 +3,25 @@ #include "guetzli_entry_points.h" #include "png.h" #include "sandboxed_api/sandbox2/util/fileops.h" +#include "sandboxed_api/util/statusor.h" #include #include +#include #include #include #include namespace { -inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { - return (static_cast(val) * static_cast(alpha) + 128) / 255; -} +constexpr int kBytesPerPixel = 350; +constexpr int kLowestMemusageMB = 100; // in MB + +struct GuetzliInitData { + std::string in_data; + guetzli::Params params; + guetzli::ProcessStats stats; +}; template void CopyMemoryToLenVal(const T* data, size_t size, @@ -26,68 +33,63 @@ void CopyMemoryToLenVal(const T* data, size_t size, out_data->data = new_out; } -} // namespace - -extern "C" bool ProcessJPEGString(const guetzli::Params* params, - int verbose, - sapi::LenValStruct* in_data, - sapi::LenValStruct* out_data) -{ - std::string in_data_temp(static_cast(in_data->data), - in_data->size); - - guetzli::ProcessStats stats; - if (verbose > 0) { - stats.debug_output_file = stderr; - } - - std::string temp_out = ""; - auto result = guetzli::Process(*params, &stats, in_data_temp, &temp_out); - - if (result) { - CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); - } - - return result; -} - -extern "C" bool ProcessRGBData(const guetzli::Params* params, - int verbose, - sapi::LenValStruct* rgb, - int w, int h, - sapi::LenValStruct* out_data) -{ - std::vector in_data_temp; - in_data_temp.reserve(rgb->size); - - auto* rgb_data = static_cast(rgb->data); - std::copy(rgb_data, rgb_data + rgb->size, std::back_inserter(in_data_temp)); - - guetzli::ProcessStats stats; - if (verbose > 0) { - stats.debug_output_file = stderr; - } - - std::string temp_out = ""; - auto result = - guetzli::Process(*params, &stats, in_data_temp, w, h, &temp_out); - - //TODO: Move shared part of the code to another function - if (result) { - CopyMemoryToLenVal(temp_out.data(), temp_out.size(), out_data); - } - - return result; -} - -extern "C" bool ReadPng(sapi::LenValStruct* in_data, - int* xsize, int* ysize, - sapi::LenValStruct* rgb_out) -{ - std::string data(static_cast(in_data->data), in_data->size); - std::vector rgb; +sapi::StatusOr ReadFromFd(int fd) { + struct stat file_data; + auto status = fstat(fd, &file_data); - png_structp png_ptr = + if (status < 0) { + return absl::FailedPreconditionError( + "Error reading input from fd" + ); + } + + auto fsize = file_data.st_size; + + std::unique_ptr buf(new char[fsize]); + status = read(fd, buf.get(), fsize); + + if (status < 0) { + lseek(fd, 0, SEEK_SET); + return absl::FailedPreconditionError( + "Error reading input from fd" + ); + } + + return std::string(buf.get(), fsize); +} + +sapi::StatusOr PrepareDataForProcessing( + const ProcessingParams* processing_params) { + auto input_status = ReadFromFd(processing_params->remote_fd); + + if (!input_status.ok()) { + return input_status.status(); + } + + guetzli::Params guetzli_params; + guetzli_params.butteraugli_target = static_cast( + guetzli::ButteraugliScoreForQuality(processing_params->quality)); + + guetzli::ProcessStats stats; + + if (processing_params->verbose) { + stats.debug_output_file = stderr; + } + + return GuetzliInitData{ + std::move(input_status.value()), + guetzli_params, + stats + }; +} + +inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { + return (static_cast(val) * static_cast(alpha) + 128) / 255; +} + +bool ReadPNG(const std::string& data, int* xsize, int* ysize, + std::vector* rgb) { + png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) { return false; @@ -129,7 +131,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, *xsize = png_get_image_width(png_ptr, info_ptr); *ysize = png_get_image_height(png_ptr, info_ptr); - rgb.resize(3 * (*xsize) * (*ysize)); + rgb->resize(3 * (*xsize) * (*ysize)); const int components = png_get_channels(png_ptr, info_ptr); switch (components) { @@ -137,7 +139,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, // GRAYSCALE for (int y = 0; y < *ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; for (int x = 0; x < *xsize; ++x) { const uint8_t gray = row_in[x]; row_out[3 * x + 0] = gray; @@ -151,7 +153,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, // GRAYSCALE + ALPHA for (int y = 0; y < *ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; for (int x = 0; x < *xsize; ++x) { const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); row_out[3 * x + 0] = gray; @@ -165,7 +167,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, // RGB for (int y = 0; y < *ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; memcpy(row_out, row_in, 3 * (*xsize)); } break; @@ -174,7 +176,7 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, // RGBA for (int y = 0; y < *ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (*xsize)]; + uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; for (int x = 0; x < *xsize; ++x) { const uint8_t alpha = row_in[4 * x + 3]; row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); @@ -189,53 +191,84 @@ extern "C" bool ReadPng(sapi::LenValStruct* in_data, return false; } png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - - CopyMemoryToLenVal(rgb.data(), rgb.size(), rgb_out); - return true; } -extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, - int mode, - int* xsize, int* ysize) -{ - std::string data(static_cast(in_data->data), in_data->size); - guetzli::JPEGData jpg; - - auto result = guetzli::ReadJpeg(data, - static_cast(mode), &jpg); - - if (result) { - *xsize = jpg.width; - *ysize = jpg.height; - } - - return result; +bool CheckMemoryLimitExceeded(int memlimit_mb, int xsize, int ysize) { + double pixels = static_cast(xsize) * ysize; + return memlimit_mb != -1 + && (pixels * kBytesPerPixel / (1 << 20) > memlimit_mb + || memlimit_mb < kLowestMemusageMB); } -extern "C" double ButteraugliScoreQuality(double quality) { - return guetzli::ButteraugliScoreForQuality(quality); +} // namespace + +extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, + sapi::LenValStruct* output) { + auto processing_data_status = PrepareDataForProcessing(processing_params); + + if (!processing_data_status.status().ok()) { + fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str()); + return false; + } + + guetzli::JPEGData jpg_header; + if (!guetzli::ReadJpeg(processing_data_status.value().in_data, + guetzli::JPEG_READ_HEADER, &jpg_header)) { + fprintf(stderr, "Error reading JPG data from input file\n"); + return false; + } + + if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, + jpg_header.width, jpg_header.height)) { + fprintf(stderr, "Memory limit would be exceeded.\n"); + return false; + } + + std::string out_data; + if (!guetzli::Process(processing_data_status.value().params, + &processing_data_status.value().stats, + processing_data_status.value().in_data, + &out_data)) { + fprintf(stderr, "Guezli processing failed\n"); + return false; + } + + CopyMemoryToLenVal(out_data.data(), out_data.size(), output); + return true; } -extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data) { - struct stat file_data; - auto status = fstat(fd, &file_data); - - if (status < 0) { +extern "C" bool ProcessRgb(const ProcessingParams* processing_params, + sapi::LenValStruct* output) { + auto processing_data_status = PrepareDataForProcessing(processing_params); + + if (!processing_data_status.status().ok()) { + fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str()); return false; } - - auto fsize = file_data.st_size; - std::unique_ptr buf(new char[fsize]); - status = read(fd, buf.get(), fsize); + int xsize, ysize; + std::vector rgb; - if (status < 0) { + if (!ReadPNG(processing_data_status.value().in_data, &xsize, &ysize, &rgb)) { + fprintf(stderr, "Error reading PNG data from input file\n"); return false; } - - CopyMemoryToLenVal(buf.get(), fsize, out_data); + if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, xsize, ysize)) { + fprintf(stderr, "Memory limit would be exceeded.\n"); + return false; + } + + std::string out_data; + if (!guetzli::Process(processing_data_status.value().params, + &processing_data_status.value().stats, + rgb, xsize, ysize, &out_data)) { + fprintf(stderr, "Guetzli processing failed\n"); + return false; + } + + CopyMemoryToLenVal(out_data.data(), out_data.size(), output); return true; } diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.h b/oss-internship-2020/guetzli/guetzli_entry_points.h index 6fd0f11..90b0d97 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.h +++ b/oss-internship-2020/guetzli/guetzli_entry_points.h @@ -4,26 +4,15 @@ #include "sandboxed_api/lenval_core.h" #include "sandboxed_api/vars.h" -extern "C" bool ProcessJPEGString(const guetzli::Params* params, - int verbose, - sapi::LenValStruct* in_data, - sapi::LenValStruct* out_data); - -extern "C" bool ProcessRGBData(const guetzli::Params* params, - int verbose, - sapi::LenValStruct* rgb, - int w, int h, - sapi::LenValStruct* out_data); - -extern "C" bool ReadPng(sapi::LenValStruct* in_data, - int* xsize, int* ysize, - sapi::LenValStruct* rgb_out); - -extern "C" bool ReadJpegData(sapi::LenValStruct* in_data, - int mode, int* xsize, int* ysize); - -extern "C" double ButteraugliScoreQuality(double quality); - -extern "C" bool ReadDataFromFd(int fd, sapi::LenValStruct* out_data); +struct ProcessingParams { + int remote_fd = -1; + int verbose = 0; + int quality = 0; + int memlimit_mb = 0; +}; +extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, + sapi::LenValStruct* output); +extern "C" bool ProcessRgb(const ProcessingParams* processing_params, + sapi::LenValStruct* output); extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data); \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc index a62f249..3393b8f 100644 --- a/oss-internship-2020/guetzli/guetzli_sandboxed.cc +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -14,17 +14,6 @@ namespace { constexpr int kDefaultJPEGQuality = 95; constexpr int kDefaultMemlimitMB = 6000; // in MB -//constexpr absl::string_view kMktempSuffix = "XXXXXX"; - -// sapi::StatusOr> CreateNamedTempFile( -// absl::string_view prefix) { -// std::string name_template = absl::StrCat(prefix, kMktempSuffix); -// int fd = mkstemp(&name_template[0]); -// if (fd < 0) { -// return absl::UnknownError("Error creating temp file"); -// } -// return std::pair{std::move(name_template), fd}; -// } void TerminateHandler() { fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" @@ -96,21 +85,6 @@ int main(int argc, const char** argv) { return 1; } - // auto out_temp_file = CreateNamedTempFile("/tmp/" + std::string(argv[opt_idx + 1])); - // if (!out_temp_file.ok()) { - // fprintf(stderr, "Can't create temporary output file: %s\n", - // argv[opt_idx + 1]); - // return 1; - // } - // sandbox2::file_util::fileops::FDCloser out_fd_closer( - // out_temp_file.value().second); - - // if (unlink(out_temp_file.value().first.c_str()) < 0) { - // fprintf(stderr, "Error unlinking temp out file: %s\n", - // out_temp_file.value().first.c_str()); - // return 1; - // } - sandbox2::file_util::fileops::FDCloser out_fd_closer( open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); @@ -119,14 +93,6 @@ int main(int argc, const char** argv) { return 1; } - // sandbox2::file_util::fileops::FDCloser out_fd_closer(open(argv[opt_idx + 1], - // O_RDWR | O_CREAT, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP)); - - // if (out_fd_closer.get() < 0) { - // fprintf(stderr, "Can't open output file: %s\n", argv[opt_idx + 1]); - // return 1; - // } - guetzli::sandbox::TransactionParams params = { in_fd_closer.get(), out_fd_closer.get(), @@ -143,13 +109,19 @@ int main(int argc, const char** argv) { if (remove(argv[opt_idx + 1]) < 0) { fprintf(stderr, "Error deleting existing output file: %s\n", argv[opt_idx + 1]); + return 1; } } std::stringstream path; path << "/proc/self/fd/" << out_fd_closer.get(); - linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1], - AT_SYMLINK_FOLLOW); + + if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1], + AT_SYMLINK_FOLLOW) < 0) { + fprintf(stderr, "Error linking %s\n", + argv[opt_idx + 1]); + return 1; + } } else { fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc index 9403783..0f7e1bd 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -1,11 +1,38 @@ #include "guetzli_transaction.h" #include +#include namespace guetzli { namespace sandbox { absl::Status GuetzliTransaction::Init() { + // Close remote fd if transaction is repeated + if (in_fd_.GetRemoteFd() != -1) { + SAPI_RETURN_IF_ERROR(in_fd_.CloseRemoteFd(sandbox()->GetRpcChannel())); + } + if (out_fd_.GetRemoteFd() != -1) { + SAPI_RETURN_IF_ERROR(out_fd_.CloseRemoteFd(sandbox()->GetRpcChannel())); + } + + // Reposition back to the beginning of file + if (lseek(in_fd_.GetValue(), 0, SEEK_CUR) != 0) { + if (lseek(in_fd_.GetValue(), 0, SEEK_SET) != 0) { + return absl::FailedPreconditionError( + "Error returnig cursor to the beginning" + ); + } + } + + // Choosing between jpg and png modes + sapi::StatusOr image_type = GetImageTypeFromFd(in_fd_.GetValue()); + + if (!image_type.ok()) { + return image_type.status(); + } + + image_type_ = image_type.value(); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); @@ -18,125 +45,36 @@ absl::Status GuetzliTransaction::Init() { "Error receiving remote FD: remote output fd is set to -1"); } + in_fd_.OwnLocalFd(false); // FDCloser will close local fd + out_fd_.OwnLocalFd(false); // FDCloser will close local fd + return absl::OkStatus(); } - absl::Status GuetzliTransaction::ProcessPng(GuetzliAPi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const { - sapi::v::Int xsize; - sapi::v::Int ysize; - sapi::v::LenVal rgb_in(0); - - auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), - ysize.PtrBoth(), rgb_in.PtrBoth()); - - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading PNG data from input file" - ); - } - - double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); - if (params_.memlimit_mb != -1 - && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb - || params_.memlimit_mb < kLowestMemusageMB)) { - return absl::FailedPreconditionError( - "Memory limit would be exceeded" - ); - } - - auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, - rgb_in.PtrBefore(), xsize.GetValue(), - ysize.GetValue(), output->PtrBoth()); - if (!result.value_or(false)) { - return absl::FailedPreconditionError( - "Guetzli processing failed" - ); - } - - return absl::OkStatus(); - } - - absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const { - ::sapi::v::Int xsize; - ::sapi::v::Int ysize; - auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), - ysize.PtrBoth()); - - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading JPG data from input file" - ); - } - - double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); - if (params_.memlimit_mb != -1 - && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb - || params_.memlimit_mb < kLowestMemusageMB)) { - return absl::FailedPreconditionError( - "Memory limit would be exceeded" - ); - } - - auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, - input->PtrBefore(), output->PtrBoth()); - - if (!result.value_or(false)) { - return absl::FailedPreconditionError( - "Guetzli processing failed" - ); - } - - return absl::OkStatus(); - } - absl::Status GuetzliTransaction::Main() { GuetzliApi api(sandbox()); - - sapi::v::LenVal input(0); sapi::v::LenVal output(0); - sapi::v::Struct params; - - auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading data inside sandbox" - ); - } - - auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); - - if (!score_quality_result.ok()) { - return absl::FailedPreconditionError( - "Error calculating butteraugli score" - ); - } - - params.mutable_data()->butteraugli_target = score_quality_result.value(); - - static const unsigned char kPNGMagicBytes[] = { - 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + sapi::v::Struct processing_params; + *processing_params.mutable_data() = {in_fd_.GetRemoteFd(), + params_.verbose, + params_.quality, + params_.memlimit_mb }; + + auto result_status = image_type_ == ImageType::JPEG ? + api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()) : + api.ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); - if (input.GetDataSize() >= 8 && - memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { - auto process_status = ProcessPng(&api, ¶ms, &input, &output); - - if (!process_status.ok()) { - return process_status; - } - } else { - auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); - - if (!process_status.ok()) { - return process_status; - } + if (!result_status.value_or(false)) { + std::stringstream error_stream; + error_stream << "Error processing " + << (image_type_ == ImageType::JPEG ? "jpeg" : "rgb") << " data" + << std::endl; + + return absl::FailedPreconditionError( + error_stream.str() + ); } auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), @@ -156,5 +94,27 @@ time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( return (pixels / kMpixPixels + 5) * 60; } +sapi::StatusOr GuetzliTransaction::GetImageTypeFromFd(int fd) const { + static const unsigned char kPNGMagicBytes[] = { + 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', + }; + char read_buf[8]; + + if (read(fd, read_buf, 8) != 8) { + return absl::FailedPreconditionError( + "Error determining type of the input file" + ); + } + + if (lseek(fd, 0, SEEK_SET) != 0) { + return absl::FailedPreconditionError( + "Error returnig cursor to the beginning" + ); + } + + return memcmp(read_buf, kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0 ? + ImageType::PNG : ImageType::JPEG; +} + } // namespace sandbox } // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index 5d6e83a..8dbd7ec 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -10,22 +10,24 @@ namespace guetzli { namespace sandbox { -constexpr int kDefaultTransactionRetryCount = 1; +constexpr int kDefaultTransactionRetryCount = 0; constexpr uint64_t kMpixPixels = 1'000'000; -constexpr int kBytesPerPixel = 350; -constexpr int kLowestMemusageMB = 100; // in MB - -struct TransactionParams { - int in_fd; - int out_fd; - int verbose; - int quality; - int memlimit_mb; +enum class ImageType { + JPEG, + PNG }; -//Add optional time limit/retry count as a constructors arguments -//Use differenet status errors +struct TransactionParams { + int in_fd = -1; + int out_fd = -1; + int verbose = 0; + int quality = 0; + int memlimit_mb = 0; +}; + +// Instance of this transaction shouldn't be reused +// Create a new one for each processing operation class GuetzliTransaction : public sapi::Transaction { public: GuetzliTransaction(TransactionParams&& params) @@ -34,7 +36,9 @@ class GuetzliTransaction : public sapi::Transaction { , in_fd_(params_.in_fd) , out_fd_(params_.out_fd) { + //TODO: Add retry count as a parameter sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount); + //TODO: Try to use sandbox().set_wall_limit instead of infinite time limit sapi::Transaction::SetTimeLimit(0); } @@ -42,15 +46,7 @@ class GuetzliTransaction : public sapi::Transaction { absl::Status Init() override; absl::Status Main() final; - absl::Status ProcessPng(GuetzliApi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const; - - absl::Status ProcessJpeg(GuetzliApi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const; + sapi::StatusOr GetImageTypeFromFd(int fd) const; // As guetzli takes roughly 1 minute of CPU per 1 MPix we need to calculate // approximate time for transaction to complete @@ -59,158 +55,8 @@ class GuetzliTransaction : public sapi::Transaction { const TransactionParams params_; sapi::v::Fd in_fd_; sapi::v::Fd out_fd_; + ImageType image_type_ = ImageType::JPEG; }; -absl::Status GuetzliTransaction::Init() { - SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); - SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); - - if (in_fd_.GetRemoteFd() < 0) { - return absl::FailedPreconditionError( - "Error receiving remote FD: remote input fd is set to -1"); - } - if (out_fd_.GetRemoteFd() < 0) { - return absl::FailedPreconditionError( - "Error receiving remote FD: remote output fd is set to -1"); - } - - return absl::OkStatus(); -} - - absl::Status GuetzliTransaction::ProcessPng(GuetzliApi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const { - sapi::v::Int xsize; - sapi::v::Int ysize; - sapi::v::LenVal rgb_in(0); - - auto read_result = api->ReadPng(input->PtrBefore(), xsize.PtrBoth(), - ysize.PtrBoth(), rgb_in.PtrBoth()); - - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading PNG data from input file" - ); - } - - double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); - if (params_.memlimit_mb != -1 - && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb - || params_.memlimit_mb < kLowestMemusageMB)) { - return absl::FailedPreconditionError( - "Memory limit would be exceeded" - ); - } - - auto result = api->ProcessRGBData(params->PtrBefore(), params_.verbose, - rgb_in.PtrBefore(), xsize.GetValue(), - ysize.GetValue(), output->PtrBoth()); - if (!result.value_or(false)) { - return absl::FailedPreconditionError( - "Guetzli processing failed" - ); - } - - return absl::OkStatus(); - } - - absl::Status GuetzliTransaction::ProcessJpeg(GuetzliApi* api, - sapi::v::Struct* params, - sapi::v::LenVal* input, - sapi::v::LenVal* output) const { - sapi::v::Int xsize; - sapi::v::Int ysize; - auto read_result = api->ReadJpegData(input->PtrBefore(), 0, xsize.PtrBoth(), - ysize.PtrBoth()); - - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading JPG data from input file" - ); - } - - double pixels = static_cast(xsize.GetValue()) * ysize.GetValue(); - if (params_.memlimit_mb != -1 - && (pixels * kBytesPerPixel / (1 << 20) > params_.memlimit_mb - || params_.memlimit_mb < kLowestMemusageMB)) { - return absl::FailedPreconditionError( - "Memory limit would be exceeded" - ); - } - - auto result = api->ProcessJPEGString(params->PtrBefore(), params_.verbose, - input->PtrBefore(), output->PtrBoth()); - - if (!result.value_or(false)) { - return absl::FailedPreconditionError( - "Guetzli processing failed" - ); - } - - return absl::OkStatus(); - } - -absl::Status GuetzliTransaction::Main() { - GuetzliApi api(sandbox()); - - sapi::v::LenVal input(0); - sapi::v::LenVal output(0); - sapi::v::Struct params; - - auto read_result = api.ReadDataFromFd(in_fd_.GetRemoteFd(), input.PtrBoth()); - - if (!read_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error reading data inside sandbox" - ); - } - - auto score_quality_result = api.ButteraugliScoreQuality(params_.quality); - - if (!score_quality_result.ok()) { - return absl::FailedPreconditionError( - "Error calculating butteraugli score" - ); - } - - params.mutable_data()->butteraugli_target = score_quality_result.value(); - - static const unsigned char kPNGMagicBytes[] = { - 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', - }; - - if (input.GetDataSize() >= 8 && - memcmp(input.GetData(), kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0) { - auto process_status = ProcessPng(&api, ¶ms, &input, &output); - - if (!process_status.ok()) { - return process_status; - } - } else { - auto process_status = ProcessJpeg(&api, ¶ms, &input, &output); - - if (!process_status.ok()) { - return process_status; - } - } - - auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), - output.PtrBefore()); - - if (!write_result.value_or(false)) { - return absl::FailedPreconditionError( - "Error writing file inside sandbox" - ); - } - - return absl::OkStatus(); -} - -time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( - uint64_t pixels) const { - return (pixels / kMpixPixels + 5) * 60; -} - } // namespace sandbox } // namespace guetzli diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/BUILD new file mode 100755 index 0000000..f553c0b --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/BUILD @@ -0,0 +1,24 @@ +cc_library( + name = "butteraugli_lib", + srcs = [ + "butteraugli/butteraugli.cc", + "butteraugli/butteraugli.h", + ], + hdrs = [ + "butteraugli/butteraugli.h", + ], + copts = ["-Wno-sign-compare"], + visibility = ["//visibility:public"], +) + +cc_binary( + name = "butteraugli", + srcs = ["butteraugli/butteraugli_main.cc"], + copts = ["-Wno-sign-compare"], + visibility = ["//visibility:public"], + deps = [ + ":butteraugli_lib", + "@jpeg_archive//:jpeg", + "@png_archive//:png", + ], +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE b/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE new file mode 100755 index 0000000..261eeb9 --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/README.md b/oss-internship-2020/guetzli/third_party/butteraugli/README.md new file mode 100755 index 0000000..4623442 --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/README.md @@ -0,0 +1,68 @@ +# butteraugli + +> A tool for measuring perceived differences between images + +## Introduction + +Butteraugli is a project that estimates the psychovisual similarity of two +images. It gives a score for the images that is reliable in the domain of barely +noticeable differences. Butteraugli not only gives a scalar score, but also +computes a spatial map of the level of differences. + +One of the main motivations for this project is the statistical differences in +location and density of different color receptors, particularly the low density +of blue cones in the fovea. Another motivation comes from more accurate modeling +of ganglion cells, particularly the frequency space inhibition. + +## Use + +Butteraugli can work as a quality metric for lossy image and video compression. +On our small test corpus butteraugli performs better than our implementations of +the reference methods, psnrhsv-m, ssim, and our yuv-color-space variant of ssim. +One possible use is to define the quality level setting used in a jpeg +compressor, or to compare two or more compression methods at the same level of +psychovisual differences. + +Butteraugli is intended to be a research tool more than a practical tool for +choosing compression formats. We don't know how well butteraugli performs with +major deformations -- we have mostly tuned it within a small range of quality, +roughly corresponding to jpeg qualities 90 to 95. + +## Interface + +Only a C++ interface is provided. The interface takes two images and outputs a +map together with a scalar value defining the difference. The scalar value can +be compared to two reference values that divide the value space into three +experience classes: 'great', 'acceptable' and 'not acceptable'. + +## Build instructions + +Install [Bazel](http://bazel.build) by following the +[instructions](https://www.bazel.build/docs/install.html). Run `bazel build -c opt +//:butteraugli` in the directory that contains this README file to build the +[command-line utility](#cmdline-tool). If you want to use Butteraugli as a +library, depend on the `//:butteraugli_lib` target. + +Alternatively, you can use the Makefile provided in the `butteraugli` directory, +after ensuring that [libpng](http://www.libpng.org/) and +[libjpeg](http://ijg.org/) are installed. On some systems you might need to also +install corresponding `-dev` packages. + +The code is portable and also compiles on Windows after defining +`_CRT_SECURE_NO_WARNINGS` in the project settings. + +## Command-line utility {#cmdline-tool} + +Butteraugli, apart from the library, comes bundled with a comparison tool. The +comparison tool supports PNG and JPG images as inputs. To compare images, run: + +``` +butteraugli image1.{png|jpg} image2.{png|jpg} +``` + +The tool can also produce a heatmap of differences between images. The heatmap +will be output as a PNM image. To produce one, run: + +``` +butteraugli image1.{png|jpg} image2.{png|jpg} heatmap.pnm +``` diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE b/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE new file mode 100755 index 0000000..4d6ed65 --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE @@ -0,0 +1,25 @@ +workspace(name = "butteraugli") + +new_http_archive( + name = "png_archive", + url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", + sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", + strip_prefix = "libpng-1.2.57", + build_file = "png.BUILD", +) + +new_http_archive( + name = "zlib_archive", + url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", + sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", + strip_prefix = "zlib-1.2.10", + build_file = "zlib.BUILD", +) + +new_http_archive( + name = "jpeg_archive", + url = "http://www.ijg.org/files/jpegsrc.v9b.tar.gz", + sha256 = "240fd398da741669bf3c90366f58452ea59041cacc741a489b99f2f6a0bad052", + strip_prefix = "jpeg-9b", + build_file = "jpeg.BUILD", +) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile new file mode 100755 index 0000000..76b3a9b --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile @@ -0,0 +1,7 @@ +LDLIBS += -lpng -ljpeg +CXXFLAGS += -std=c++11 -I.. +LINK.o = $(LINK.cc) + +all: butteraugli.o butteraugli_main.o butteraugli + +butteraugli: butteraugli.o butteraugli_main.o diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc new file mode 100755 index 0000000..77c91cc --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc @@ -0,0 +1,1994 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) +// +// The physical architecture of butteraugli is based on the following naming +// convention: +// * Opsin - dynamics of the photosensitive chemicals in the retina +// with their immediate electrical processing +// * Xyb - hybrid opponent/trichromatic color space +// x is roughly red-subtract-green. +// y is yellow. +// b is blue. +// Xyb values are computed from Opsin mixing, not directly from rgb. +// * Mask - for visual masking +// * Hf - color modeling for spatially high-frequency features +// * Lf - color modeling for spatially low-frequency features +// * Diffmap - to cluster and build an image of error between the images +// * Blur - to hold the smoothing code + +#include "butteraugli/butteraugli.h" + +#include +#include +#include +#include +#include + +#include +#include + + +// Restricted pointers speed up Convolution(); MSVC uses a different keyword. +#ifdef _MSC_VER +#define __restrict__ __restrict +#endif + +#ifndef PROFILER_ENABLED +#define PROFILER_ENABLED 0 +#endif +#if PROFILER_ENABLED +#else +#define PROFILER_FUNC +#define PROFILER_ZONE(name) +#endif + +namespace butteraugli { + +void *CacheAligned::Allocate(const size_t bytes) { + char *const allocated = static_cast(malloc(bytes + kCacheLineSize)); + if (allocated == nullptr) { + return nullptr; + } + const uintptr_t misalignment = + reinterpret_cast(allocated) & (kCacheLineSize - 1); + // malloc is at least kPointerSize aligned, so we can store the "allocated" + // pointer immediately before the aligned memory. + assert(misalignment % kPointerSize == 0); + char *const aligned = allocated + kCacheLineSize - misalignment; + memcpy(aligned - kPointerSize, &allocated, kPointerSize); + return BUTTERAUGLI_ASSUME_ALIGNED(aligned, 64); +} + +void CacheAligned::Free(void *aligned_pointer) { + if (aligned_pointer == nullptr) { + return; + } + char *const aligned = static_cast(aligned_pointer); + assert(reinterpret_cast(aligned) % kCacheLineSize == 0); + char *allocated; + memcpy(&allocated, aligned - kPointerSize, kPointerSize); + assert(allocated <= aligned - kPointerSize); + assert(allocated >= aligned - kCacheLineSize); + free(allocated); +} + +static inline bool IsNan(const float x) { + uint32_t bits; + memcpy(&bits, &x, sizeof(bits)); + const uint32_t bitmask_exp = 0x7F800000; + return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); +} + +static inline bool IsNan(const double x) { + uint64_t bits; + memcpy(&bits, &x, sizeof(bits)); + return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || + (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); +} + +static inline void CheckImage(const ImageF &image, const char *name) { + for (size_t y = 0; y < image.ysize(); ++y) { + const float * const BUTTERAUGLI_RESTRICT row = image.Row(y); + for (size_t x = 0; x < image.xsize(); ++x) { + if (IsNan(row[x])) { + printf("Image %s @ %lu,%lu (of %lu,%lu)\n", name, x, y, image.xsize(), + image.ysize()); + exit(1); + } + } + } +} + +#if BUTTERAUGLI_ENABLE_CHECKS + +#define CHECK_NAN(x, str) \ + do { \ + if (IsNan(x)) { \ + printf("%d: %s\n", __LINE__, str); \ + abort(); \ + } \ + } while (0) + +#define CHECK_IMAGE(image, name) CheckImage(image, name) + +#else + +#define CHECK_NAN(x, str) +#define CHECK_IMAGE(image, name) + +#endif + + +// Purpose of kInternalGoodQualityThreshold: +// Normalize 'ok' image degradation to 1.0 across different versions of +// butteraugli. +static const double kInternalGoodQualityThreshold = 20.35; +static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; + +inline float DotProduct(const float u[3], const float v[3]) { + return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; +} + +std::vector ComputeKernel(float sigma) { + const float m = 2.25; // Accuracy increases when m is increased. + const float scaler = -1.0 / (2 * sigma * sigma); + const int diff = std::max(1, m * fabs(sigma)); + std::vector kernel(2 * diff + 1); + for (int i = -diff; i <= diff; ++i) { + kernel[i + diff] = exp(scaler * i * i); + } + return kernel; +} + +void ConvolveBorderColumn( + const ImageF& in, + const std::vector& kernel, + const float weight_no_border, + const float border_ratio, + const size_t x, + float* const BUTTERAUGLI_RESTRICT row_out) { + const int offset = kernel.size() / 2; + int minx = x < offset ? 0 : x - offset; + int maxx = std::min(in.xsize() - 1, x + offset); + float weight = 0.0f; + for (int j = minx; j <= maxx; ++j) { + weight += kernel[j - x + offset]; + } + // Interpolate linearly between the no-border scaling and border scaling. + weight = (1.0f - border_ratio) * weight + border_ratio * weight_no_border; + float scale = 1.0f / weight; + for (size_t y = 0; y < in.ysize(); ++y) { + const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); + float sum = 0.0f; + for (int j = minx; j <= maxx; ++j) { + sum += row_in[j] * kernel[j - x + offset]; + } + row_out[y] = sum * scale; + } +} + +// Computes a horizontal convolution and transposes the result. +ImageF Convolution(const ImageF& in, + const std::vector& kernel, + const float border_ratio) { + ImageF out(in.ysize(), in.xsize()); + const int len = kernel.size(); + const int offset = kernel.size() / 2; + float weight_no_border = 0.0f; + for (int j = 0; j < len; ++j) { + weight_no_border += kernel[j]; + } + float scale_no_border = 1.0f / weight_no_border; + const int border1 = in.xsize() <= offset ? in.xsize() : offset; + const int border2 = in.xsize() - offset; + std::vector scaled_kernel = kernel; + for (int i = 0; i < scaled_kernel.size(); ++i) { + scaled_kernel[i] *= scale_no_border; + } + // left border + for (int x = 0; x < border1; ++x) { + ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, + out.Row(x)); + } + // middle + for (size_t y = 0; y < in.ysize(); ++y) { + const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); + for (int x = border1; x < border2; ++x) { + const int d = x - offset; + float* const BUTTERAUGLI_RESTRICT row_out = out.Row(x); + float sum = 0.0f; + for (int j = 0; j < len; ++j) { + sum += row_in[d + j] * scaled_kernel[j]; + } + row_out[y] = sum; + } + } + // right border + for (int x = border2; x < in.xsize(); ++x) { + ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, + out.Row(x)); + } + return out; +} + +// A blur somewhat similar to a 2D Gaussian blur. +// See: https://en.wikipedia.org/wiki/Gaussian_blur +ImageF Blur(const ImageF& in, float sigma, float border_ratio) { + std::vector kernel = ComputeKernel(sigma); + return Convolution(Convolution(in, kernel, border_ratio), + kernel, border_ratio); +} + +// Clamping linear interpolator. +inline double InterpolateClampNegative(const double *array, + int size, double ix) { + if (ix < 0) { + ix = 0; + } + int baseix = static_cast(ix); + double res; + if (baseix >= size - 1) { + res = array[size - 1]; + } else { + double mix = ix - baseix; + int nextix = baseix + 1; + res = array[baseix] + mix * (array[nextix] - array[baseix]); + } + return res; +} + +double GammaMinArg() { + double out0, out1, out2; + OpsinAbsorbance(0.0, 0.0, 0.0, &out0, &out1, &out2); + return std::min(out0, std::min(out1, out2)); +} + +double GammaMaxArg() { + double out0, out1, out2; + OpsinAbsorbance(255.0, 255.0, 255.0, &out0, &out1, &out2); + return std::max(out0, std::max(out1, out2)); +} + +double SimpleGamma(double v) { + static const double kGamma = 0.372322653176; + static const double limit = 37.8000499603; + double bright = v - limit; + if (bright >= 0) { + static const double mul = 0.0950819040934; + v -= bright * mul; + } + { + static const double limit2 = 74.6154406429; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.01; + v -= bright2 * mul; + } + } + { + static const double limit2 = 82.8505938033; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.0316722592629; + v -= bright2 * mul; + } + } + { + static const double limit2 = 92.8505938033; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.221249885752; + v -= bright2 * mul; + } + } + { + static const double limit2 = 102.8505938033; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.0402547853939; + v -= bright2 * mul; + } + } + { + static const double limit2 = 112.8505938033; + double bright2 = v - limit2; + if (bright2 >= 0) { + static const double mul = 0.021471798711500003; + v -= bright2 * mul; + } + } + static const double offset = 0.106544447664; + static const double scale = 10.7950943969; + double retval = scale * (offset + pow(v, kGamma)); + return retval; +} + +static inline double Gamma(double v) { + //return SimpleGamma(v); + return GammaPolynomial(v); +} + +std::vector OpsinDynamicsImage(const std::vector& rgb) { + PROFILER_FUNC; + std::vector xyb(3); + std::vector blurred(3); + const double kSigma = 1.2; + for (int i = 0; i < 3; ++i) { + xyb[i] = ImageF(rgb[i].xsize(), rgb[i].ysize()); + blurred[i] = Blur(rgb[i], kSigma, 0.0f); + } + for (size_t y = 0; y < rgb[0].ysize(); ++y) { + const float* const BUTTERAUGLI_RESTRICT row_r = rgb[0].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_g = rgb[1].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_b = rgb[2].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_blurred_r = blurred[0].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_blurred_g = blurred[1].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_blurred_b = blurred[2].Row(y); + float* const BUTTERAUGLI_RESTRICT row_out_x = xyb[0].Row(y); + float* const BUTTERAUGLI_RESTRICT row_out_y = xyb[1].Row(y); + float* const BUTTERAUGLI_RESTRICT row_out_b = xyb[2].Row(y); + for (size_t x = 0; x < rgb[0].xsize(); ++x) { + float sensitivity[3]; + { + // Calculate sensitivity based on the smoothed image gamma derivative. + float pre_mixed0, pre_mixed1, pre_mixed2; + OpsinAbsorbance(row_blurred_r[x], row_blurred_g[x], row_blurred_b[x], + &pre_mixed0, &pre_mixed1, &pre_mixed2); + // TODO: use new polynomial to compute Gamma(x)/x derivative. + sensitivity[0] = Gamma(pre_mixed0) / pre_mixed0; + sensitivity[1] = Gamma(pre_mixed1) / pre_mixed1; + sensitivity[2] = Gamma(pre_mixed2) / pre_mixed2; + } + float cur_mixed0, cur_mixed1, cur_mixed2; + OpsinAbsorbance(row_r[x], row_g[x], row_b[x], + &cur_mixed0, &cur_mixed1, &cur_mixed2); + cur_mixed0 *= sensitivity[0]; + cur_mixed1 *= sensitivity[1]; + cur_mixed2 *= sensitivity[2]; + RgbToXyb(cur_mixed0, cur_mixed1, cur_mixed2, + &row_out_x[x], &row_out_y[x], &row_out_b[x]); + } + } + return xyb; +} + +// Make area around zero less important (remove it). +static BUTTERAUGLI_INLINE float RemoveRangeAroundZero(float w, float x) { + return x > w ? x - w : x < -w ? x + w : 0.0f; +} + +// Make area around zero more important (2x it until the limit). +static BUTTERAUGLI_INLINE float AmplifyRangeAroundZero(float w, float x) { + return x > w ? x + w : x < -w ? x - w : 2.0f * x; +} + +// XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. +// Vals space can be converted to L2-norm space (Euclidean and normalized) +// through visual masking. +template +BUTTERAUGLI_INLINE void XybLowFreqToVals(const V &x, const V &y, const V &b_arg, + V *BUTTERAUGLI_RESTRICT valx, + V *BUTTERAUGLI_RESTRICT valy, + V *BUTTERAUGLI_RESTRICT valb) { + static const double xmuli = 5.57547552483; + static const double ymuli = 1.20828034498; + static const double bmuli = 6.08319517575; + static const double y_to_b_muli = -0.628811683685; + + const V xmul(xmuli); + const V ymul(ymuli); + const V bmul(bmuli); + const V y_to_b_mul(y_to_b_muli); + const V b = b_arg + y_to_b_mul * y; + *valb = b * bmul; + *valx = x * xmul; + *valy = y * ymul; +} + +static ImageF SuppressInBrightAreas(size_t xsize, size_t ysize, + double mul, double mul2, double reg, + const ImageF& hf, + const ImageF& brightness) { + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* const rowhf = hf.Row(y); + const float* const rowbr = brightness.Row(y); + float* const rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + float v = rowhf[x]; + float scaler = mul * reg / (reg + rowbr[x]); + rownew[x] = scaler * v; + } + } + return inew; +} + + +static float SuppressHfInBrightAreas(float hf, float brightness, + float mul, float reg) { + float scaler = mul * reg / (reg + brightness); + return scaler * hf; +} + +static float SuppressUhfInBrightAreas(float hf, float brightness, + float mul, float reg) { + float scaler = mul * reg / (reg + brightness); + return scaler * hf; +} + +static float MaximumClamp(float v, float maxval) { + static const double kMul = 0.688059627878; + if (v >= maxval) { + v -= maxval; + v *= kMul; + v += maxval; + } else if (v < -maxval) { + v += maxval; + v *= kMul; + v -= maxval; + } + return v; +} + +static ImageF MaximumClamping(size_t xsize, size_t ysize, const ImageF& ix, + double yw) { + static const double kMul = 0.688059627878; + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* const rowx = ix.Row(y); + float* const rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + double v = rowx[x]; + if (v >= yw) { + v -= yw; + v *= kMul; + v += yw; + } else if (v < -yw) { + v += yw; + v *= kMul; + v -= yw; + } + rownew[x] = v; + } + } + return inew; +} + +static ImageF SuppressXByY(size_t xsize, size_t ysize, + const ImageF& ix, const ImageF& iy, + const double yw) { + static const double s = 0.745954517135; + ImageF inew(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* const rowx = ix.Row(y); + const float* const rowy = iy.Row(y); + float* const rownew = inew.Row(y); + for (size_t x = 0; x < xsize; ++x) { + const double xval = rowx[x]; + const double yval = rowy[x]; + const double scaler = s + (yw * (1.0 - s)) / (yw + yval * yval); + rownew[x] = scaler * xval; + } + } + return inew; +} + +static void SeparateFrequencies( + size_t xsize, size_t ysize, + const std::vector& xyb, + PsychoImage &ps) { + PROFILER_FUNC; + ps.lf.resize(3); // XYB + ps.mf.resize(3); // XYB + ps.hf.resize(2); // XY + ps.uhf.resize(2); // XY + // Extract lf ... + static const double kSigmaLf = 7.46953768697; + static const double kSigmaHf = 3.734768843485; + static const double kSigmaUhf = 1.8673844217425; + // At borders we move some more of the energy to the high frequency + // parts, because there can be unfortunate continuations in tiling + // background color etc. So we want to represent the borders with + // some more accuracy. + static double border_lf = -0.00457628248637; + static double border_mf = -0.271277366628; + static double border_hf = 0.147068973249; + for (int i = 0; i < 3; ++i) { + ps.lf[i] = Blur(xyb[i], kSigmaLf, border_lf); + // ... and keep everything else in mf. + ps.mf[i] = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + ps.mf[i].Row(y)[x] = xyb[i].Row(y)[x] - ps.lf[i].Row(y)[x]; + } + } + if (i == 2) { + ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); + break; + } + // Divide mf into mf and hf. + ps.hf[i] = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[i].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] = row_mf[x]; + } + } + ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); + static const double w0 = 0.120079806822; + static const double w1 = 0.03430529365; + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[0].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] -= row_mf[x]; + row_mf[x] = RemoveRangeAroundZero(w0, row_mf[x]); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[1].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_hf[x] -= row_mf[x]; + row_mf[x] = AmplifyRangeAroundZero(w1, row_mf[x]); + } + } + } + } + // Suppress red-green by intensity change in the high freq channels. + static const double suppress = 2.96534974403; + ps.hf[0] = SuppressXByY(xsize, ysize, ps.hf[0], ps.hf[1], suppress); + + for (int i = 0; i < 2; ++i) { + // Divide hf into hf and uhf. + ps.uhf[i] = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[i].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] = row_hf[x]; + } + } + ps.hf[i] = Blur(ps.hf[i], kSigmaUhf, border_hf); + static const double kRemoveHfRange = 0.0287615200377; + static const double kMaxclampHf = 78.8223237675; + static const double kMaxclampUhf = 5.8907152736; + static const float kMulSuppressHf = 1.10684769012; + static const float kMulRegHf = 0.478741530298; + static const float kRegHf = 2000 * kMulRegHf; + static const float kMulSuppressUhf = 1.76905001176; + static const float kMulRegUhf = 0.310148420674; + static const float kRegUhf = 2000 * kMulRegUhf; + + if (i == 0) { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[0].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] -= row_hf[x]; + row_hf[x] = RemoveRangeAroundZero(kRemoveHfRange, row_hf[x]); + } + } + } else { + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[1].Row(y); + float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); + float* BUTTERAUGLI_RESTRICT const row_lf = ps.lf[1].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row_uhf[x] -= row_hf[x]; + row_hf[x] = MaximumClamp(row_hf[x], kMaxclampHf); + row_uhf[x] = MaximumClamp(row_uhf[x], kMaxclampUhf); + row_uhf[x] = SuppressUhfInBrightAreas(row_uhf[x], row_lf[x], + kMulSuppressUhf, kRegUhf); + row_hf[x] = SuppressHfInBrightAreas(row_hf[x], row_lf[x], + kMulSuppressHf, kRegHf); + + } + } + } + } + // Modify range around zero code only concerns the high frequency + // planes and only the X and Y channels. + // Convert low freq xyb to vals space so that we can do a simple squared sum + // diff on the low frequencies later. + for (size_t y = 0; y < ysize; ++y) { + float* BUTTERAUGLI_RESTRICT const row_x = ps.lf[0].Row(y); + float* BUTTERAUGLI_RESTRICT const row_y = ps.lf[1].Row(y); + float* BUTTERAUGLI_RESTRICT const row_b = ps.lf[2].Row(y); + for (size_t x = 0; x < xsize; ++x) { + float valx, valy, valb; + XybLowFreqToVals(row_x[x], row_y[x], row_b[x], &valx, &valy, &valb); + row_x[x] = valx; + row_y[x] = valy; + row_b[x] = valb; + } + } +} + +static void SameNoiseLevels(const ImageF& i0, const ImageF& i1, + const double kSigma, + const double w, + const double maxclamp, + ImageF* BUTTERAUGLI_RESTRICT diffmap) { + ImageF blurred(i0.xsize(), i0.ysize()); + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); + const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); + float* BUTTERAUGLI_RESTRICT const to = blurred.Row(y); + for (size_t x = 0; x < i0.xsize(); ++x) { + double v0 = fabs(row0[x]); + double v1 = fabs(row1[x]); + if (v0 > maxclamp) v0 = maxclamp; + if (v1 > maxclamp) v1 = maxclamp; + to[x] = v0 - v1; + } + + } + blurred = Blur(blurred, kSigma, 0.0); + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT const row = blurred.Row(y); + float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); + for (size_t x = 0; x < i0.xsize(); ++x) { + double diff = row[x]; + row_diff[x] += w * diff * diff; + } + } +} + +static void L2Diff(const ImageF& i0, const ImageF& i1, const double w, + ImageF* BUTTERAUGLI_RESTRICT diffmap) { + if (w == 0) { + return; + } + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); + const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); + float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); + for (size_t x = 0; x < i0.xsize(); ++x) { + double diff = row0[x] - row1[x]; + row_diff[x] += w * diff * diff; + } + } +} + +// i0 is the original image. +// i1 is the deformed copy. +static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, + double w_0gt1, + double w_0lt1, + ImageF* BUTTERAUGLI_RESTRICT diffmap) { + if (w_0gt1 == 0 && w_0lt1 == 0) { + return; + } + w_0gt1 *= 0.8; + w_0lt1 *= 0.8; + for (size_t y = 0; y < i0.ysize(); ++y) { + const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); + const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); + float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); + for (size_t x = 0; x < i0.xsize(); ++x) { + // Primary symmetric quadratic objective. + double diff = row0[x] - row1[x]; + row_diff[x] += w_0gt1 * diff * diff; + + // Secondary half-open quadratic objectives. + const double fabs0 = fabs(row0[x]); + const double too_small = 0.4 * fabs0; + const double too_big = 1.0 * fabs0; + + if (row0[x] < 0) { + if (row1[x] > -too_small) { + double v = row1[x] + too_small; + row_diff[x] += w_0lt1 * v * v; + } else if (row1[x] < -too_big) { + double v = -row1[x] - too_big; + row_diff[x] += w_0lt1 * v * v; + } + } else { + if (row1[x] < too_small) { + double v = too_small - row1[x]; + row_diff[x] += w_0lt1 * v * v; + } else if (row1[x] > too_big) { + double v = row1[x] - too_big; + row_diff[x] += w_0lt1 * v * v; + } + } + } + } +} + +// Making a cluster of local errors to be more impactful than +// just a single error. +ImageF CalculateDiffmap(const ImageF& diffmap_in) { + PROFILER_FUNC; + // Take square root. + ImageF diffmap(diffmap_in.xsize(), diffmap_in.ysize()); + static const float kInitialSlope = 100.0f; + for (size_t y = 0; y < diffmap.ysize(); ++y) { + const float* const BUTTERAUGLI_RESTRICT row_in = diffmap_in.Row(y); + float* const BUTTERAUGLI_RESTRICT row_out = diffmap.Row(y); + for (size_t x = 0; x < diffmap.xsize(); ++x) { + const float orig_val = row_in[x]; + // TODO(b/29974893): Until that is fixed do not call sqrt on very small + // numbers. + row_out[x] = (orig_val < (1.0f / (kInitialSlope * kInitialSlope)) + ? kInitialSlope * orig_val + : std::sqrt(orig_val)); + } + } + { + static const double kSigma = 1.72547472444; + static const double mul1 = 0.458794906198; + static const float scale = 1.0f / (1.0f + mul1); + static const double border_ratio = 1.0; // 2.01209066992; + ImageF blurred = Blur(diffmap, kSigma, border_ratio); + for (int y = 0; y < diffmap.ysize(); ++y) { + const float* const BUTTERAUGLI_RESTRICT row_blurred = blurred.Row(y); + float* const BUTTERAUGLI_RESTRICT row = diffmap.Row(y); + for (int x = 0; x < diffmap.xsize(); ++x) { + row[x] += mul1 * row_blurred[x]; + row[x] *= scale; + } + } + } + return diffmap; +} + +void MaskPsychoImage(const PsychoImage& pi0, const PsychoImage& pi1, + const size_t xsize, const size_t ysize, + std::vector* BUTTERAUGLI_RESTRICT mask, + std::vector* BUTTERAUGLI_RESTRICT mask_dc) { + std::vector mask_xyb0 = CreatePlanes(xsize, ysize, 3); + std::vector mask_xyb1 = CreatePlanes(xsize, ysize, 3); + static const double muls[4] = { + 0, + 1.64178305129, + 0.831081703362, + 3.23680933546, + }; + for (int i = 0; i < 2; ++i) { + double a = muls[2 * i]; + double b = muls[2 * i + 1]; + for (size_t y = 0; y < ysize; ++y) { + const float* const BUTTERAUGLI_RESTRICT row_hf0 = pi0.hf[i].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_hf1 = pi1.hf[i].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_uhf0 = pi0.uhf[i].Row(y); + const float* const BUTTERAUGLI_RESTRICT row_uhf1 = pi1.uhf[i].Row(y); + float* const BUTTERAUGLI_RESTRICT row0 = mask_xyb0[i].Row(y); + float* const BUTTERAUGLI_RESTRICT row1 = mask_xyb1[i].Row(y); + for (size_t x = 0; x < xsize; ++x) { + row0[x] = a * row_uhf0[x] + b * row_hf0[x]; + row1[x] = a * row_uhf1[x] + b * row_hf1[x]; + } + } + } + Mask(mask_xyb0, mask_xyb1, mask, mask_dc); +} + +ButteraugliComparator::ButteraugliComparator(const std::vector& rgb0) + : xsize_(rgb0[0].xsize()), + ysize_(rgb0[0].ysize()), + num_pixels_(xsize_ * ysize_) { + if (xsize_ < 8 || ysize_ < 8) return; + std::vector xyb0 = OpsinDynamicsImage(rgb0); + SeparateFrequencies(xsize_, ysize_, xyb0, pi0_); +} + +void ButteraugliComparator::Mask( + std::vector* BUTTERAUGLI_RESTRICT mask, + std::vector* BUTTERAUGLI_RESTRICT mask_dc) const { + MaskPsychoImage(pi0_, pi0_, xsize_, ysize_, mask, mask_dc); +} + +void ButteraugliComparator::Diffmap(const std::vector& rgb1, + ImageF &result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) return; + DiffmapOpsinDynamicsImage(OpsinDynamicsImage(rgb1), result); +} + +void ButteraugliComparator::DiffmapOpsinDynamicsImage( + const std::vector& xyb1, + ImageF &result) const { + PROFILER_FUNC; + if (xsize_ < 8 || ysize_ < 8) return; + PsychoImage pi1; + SeparateFrequencies(xsize_, ysize_, xyb1, pi1); + result = ImageF(xsize_, ysize_); + DiffmapPsychoImage(pi1, result); +} + +void ButteraugliComparator::DiffmapPsychoImage(const PsychoImage& pi1, + ImageF& result) const { + PROFILER_FUNC; + const float hf_asymmetry_ = 0.8f; + if (xsize_ < 8 || ysize_ < 8) { + return; + } + std::vector block_diff_dc(3); + std::vector block_diff_ac(3); + for (int c = 0; c < 3; ++c) { + block_diff_dc[c] = ImageF(xsize_, ysize_, 0.0); + block_diff_ac[c] = ImageF(xsize_, ysize_, 0.0); + } + + static const double wUhfMalta = 5.1409625726; + static const double norm1Uhf = 58.5001247061; + MaltaDiffMap(pi0_.uhf[1], pi1.uhf[1], + wUhfMalta * hf_asymmetry_, + wUhfMalta / hf_asymmetry_, + norm1Uhf, + &block_diff_ac[1]); + + static const double wUhfMaltaX = 4.91743441556; + static const double norm1UhfX = 687196.39002; + MaltaDiffMap(pi0_.uhf[0], pi1.uhf[0], + wUhfMaltaX * hf_asymmetry_, + wUhfMaltaX / hf_asymmetry_, + norm1UhfX, + &block_diff_ac[0]); + + static const double wHfMalta = 153.671655716; + static const double norm1Hf = 83150785.9592; + MaltaDiffMapLF(pi0_.hf[1], pi1.hf[1], + wHfMalta * sqrt(hf_asymmetry_), + wHfMalta / sqrt(hf_asymmetry_), + norm1Hf, + &block_diff_ac[1]); + + static const double wHfMaltaX = 668.358918152; + static const double norm1HfX = 0.882954368025; + MaltaDiffMapLF(pi0_.hf[0], pi1.hf[0], + wHfMaltaX * sqrt(hf_asymmetry_), + wHfMaltaX / sqrt(hf_asymmetry_), + norm1HfX, + &block_diff_ac[0]); + + static const double wMfMalta = 6841.81248144; + static const double norm1Mf = 0.0135134962487; + MaltaDiffMapLF(pi0_.mf[1], pi1.mf[1], wMfMalta, wMfMalta, norm1Mf, + &block_diff_ac[1]); + + static const double wMfMaltaX = 813.901703816; + static const double norm1MfX = 16792.9322251; + MaltaDiffMapLF(pi0_.mf[0], pi1.mf[0], wMfMaltaX, wMfMaltaX, norm1MfX, + &block_diff_ac[0]); + + static const double wmul[9] = { + 0, + 32.4449876135, + 0, + 0, + 0, + 0, + 1.01370836411, + 0, + 1.74566011615, + }; + + static const double maxclamp = 85.7047444518; + static const double kSigmaHfX = 10.6666499623; + static const double w = 884.809801415; + SameNoiseLevels(pi0_.hf[1], pi1.hf[1], kSigmaHfX, w, maxclamp, + &block_diff_ac[1]); + + for (int c = 0; c < 3; ++c) { + if (c < 2) { + L2DiffAsymmetric(pi0_.hf[c], pi1.hf[c], + wmul[c] * hf_asymmetry_, + wmul[c] / hf_asymmetry_, + &block_diff_ac[c]); + } + L2Diff(pi0_.mf[c], pi1.mf[c], wmul[3 + c], &block_diff_ac[c]); + L2Diff(pi0_.lf[c], pi1.lf[c], wmul[6 + c], &block_diff_dc[c]); + } + + std::vector mask_xyb; + std::vector mask_xyb_dc; + MaskPsychoImage(pi0_, pi1, xsize_, ysize_, &mask_xyb, &mask_xyb_dc); + + result = CalculateDiffmap( + CombineChannels(mask_xyb, mask_xyb_dc, block_diff_dc, block_diff_ac)); +} + +// Allows PaddedMaltaUnit to call either function via overloading. +struct MaltaTagLF {}; +struct MaltaTag {}; + +static float MaltaUnit(MaltaTagLF, const float* BUTTERAUGLI_RESTRICT d, + const int xs) { + const int xs3 = 3 * xs; + float retval = 0; + { + // x grows, y constant + float sum = + d[-4] + + d[-2] + + d[0] + + d[2] + + d[4]; + retval += sum * sum; + } + { + // y grows, x constant + float sum = + d[-xs3 - xs] + + d[-xs - xs] + + d[0] + + d[xs + xs] + + d[xs3 + xs]; + retval += sum * sum; + } + { + // both grow + float sum = + d[-xs3 - 3] + + d[-xs - xs - 2] + + d[0] + + d[xs + xs + 2] + + d[xs3 + 3]; + retval += sum * sum; + } + { + // y grows, x shrinks + float sum = + d[-xs3 + 3] + + d[-xs - xs + 2] + + d[0] + + d[xs + xs - 2] + + d[xs3 - 3]; + retval += sum * sum; + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + float sum = + d[-xs3 - xs + 1] + + d[-xs - xs + 1] + + d[0] + + d[xs + xs - 1] + + d[xs3 + xs - 1]; + retval += sum * sum; + } + { + // y grows -4 to 4, x grows -1 -> 1 + float sum = + d[-xs3 - xs - 1] + + d[-xs - xs - 1] + + d[0] + + d[xs + xs + 1] + + d[xs3 + xs + 1]; + retval += sum * sum; + } + { + // x grows -4 to 4, y grows -1 to 1 + float sum = + d[-4 - xs] + + d[-2 - xs] + + d[0] + + d[2 + xs] + + d[4 + xs]; + retval += sum * sum; + } + { + // x grows -4 to 4, y shrinks 1 to -1 + float sum = + d[-4 + xs] + + d[-2 + xs] + + d[0] + + d[2 - xs] + + d[4 - xs]; + retval += sum * sum; + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7______*__ + 8_________ */ + float sum = + d[-xs3 - 2] + + d[-xs - xs - 1] + + d[0] + + d[xs + xs + 1] + + d[xs3 + 2]; + retval += sum * sum; + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7__*______ + 8_________ */ + float sum = + d[-xs3 + 2] + + d[-xs - xs + 1] + + d[0] + + d[xs + xs - 1] + + d[xs3 - 2]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__*______ + 4____0____ + 5______*__ + 6_______*_ + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 3] + + d[-xs - 2] + + d[0] + + d[xs + 2] + + d[xs + xs + 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_______*_ + 3______*__ + 4____0____ + 5__*______ + 6_*_______ + 7_________ + 8_________ */ + float sum = + d[-xs - xs + 3] + + d[-xs + 2] + + d[0] + + d[xs - 2] + + d[xs + xs - 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2________* + 3______*__ + 4____0____ + 5__*______ + 6*________ + 7_________ + 8_________ */ + + float sum = + d[xs + xs - 4] + + d[xs - 2] + + d[0] + + d[-xs + 2] + + d[-xs - xs + 4]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2*________ + 3__*______ + 4____0____ + 5______*__ + 6________* + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 4] + + d[-xs - 2] + + d[0] + + d[xs + 2] + + d[xs + xs + 4]; + retval += sum * sum; + } + { + /* 0__*______ + 1_________ + 2___*_____ + 3_________ + 4____0____ + 5_________ + 6_____*___ + 7_________ + 8______*__ */ + float sum = + d[-xs3 - xs - 2] + + d[-xs - xs - 1] + + d[0] + + d[xs + xs + 1] + + d[xs3 + xs + 2]; + retval += sum * sum; + } + { + /* 0______*__ + 1_________ + 2_____*___ + 3_________ + 4____0____ + 5_________ + 6___*_____ + 7_________ + 8__*______ */ + float sum = + d[-xs3 - xs + 2] + + d[-xs - xs + 1] + + d[0] + + d[xs + xs - 1] + + d[xs3 + xs - 2]; + retval += sum * sum; + } + return retval; +} + +static float MaltaUnit(MaltaTag, const float* BUTTERAUGLI_RESTRICT d, + const int xs) { + const int xs3 = 3 * xs; + float retval = 0; + { + // x grows, y constant + float sum = + d[-4] + + d[-3] + + d[-2] + + d[-1] + + d[0] + + d[1] + + d[2] + + d[3] + + d[4]; + retval += sum * sum; + } + { + // y grows, x constant + float sum = + d[-xs3 - xs] + + d[-xs3] + + d[-xs - xs] + + d[-xs] + + d[0] + + d[xs] + + d[xs + xs] + + d[xs3] + + d[xs3 + xs]; + retval += sum * sum; + } + { + // both grow + float sum = + d[-xs3 - 3] + + d[-xs - xs - 2] + + d[-xs - 1] + + d[0] + + d[xs + 1] + + d[xs + xs + 2] + + d[xs3 + 3]; + retval += sum * sum; + } + { + // y grows, x shrinks + float sum = + d[-xs3 + 3] + + d[-xs - xs + 2] + + d[-xs + 1] + + d[0] + + d[xs - 1] + + d[xs + xs - 2] + + d[xs3 - 3]; + retval += sum * sum; + } + { + // y grows -4 to 4, x shrinks 1 -> -1 + float sum = + d[-xs3 - xs + 1] + + d[-xs3 + 1] + + d[-xs - xs + 1] + + d[-xs] + + d[0] + + d[xs] + + d[xs + xs - 1] + + d[xs3 - 1] + + d[xs3 + xs - 1]; + retval += sum * sum; + } + { + // y grows -4 to 4, x grows -1 -> 1 + float sum = + d[-xs3 - xs - 1] + + d[-xs3 - 1] + + d[-xs - xs - 1] + + d[-xs] + + d[0] + + d[xs] + + d[xs + xs + 1] + + d[xs3 + 1] + + d[xs3 + xs + 1]; + retval += sum * sum; + } + { + // x grows -4 to 4, y grows -1 to 1 + float sum = + d[-4 - xs] + + d[-3 - xs] + + d[-2 - xs] + + d[-1] + + d[0] + + d[1] + + d[2 + xs] + + d[3 + xs] + + d[4 + xs]; + retval += sum * sum; + } + { + // x grows -4 to 4, y shrinks 1 to -1 + float sum = + d[-4 + xs] + + d[-3 + xs] + + d[-2 + xs] + + d[-1] + + d[0] + + d[1] + + d[2 - xs] + + d[3 - xs] + + d[4 - xs]; + retval += sum * sum; + } + { + /* 0_________ + 1__*______ + 2___*_____ + 3___*_____ + 4____0____ + 5_____*___ + 6_____*___ + 7______*__ + 8_________ */ + float sum = + d[-xs3 - 2] + + d[-xs - xs - 1] + + d[-xs - 1] + + d[0] + + d[xs + 1] + + d[xs + xs + 1] + + d[xs3 + 2]; + retval += sum * sum; + } + { + /* 0_________ + 1______*__ + 2_____*___ + 3_____*___ + 4____0____ + 5___*_____ + 6___*_____ + 7__*______ + 8_________ */ + float sum = + d[-xs3 + 2] + + d[-xs - xs + 1] + + d[-xs + 1] + + d[0] + + d[xs - 1] + + d[xs + xs - 1] + + d[xs3 - 2]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_*_______ + 3__**_____ + 4____0____ + 5_____**__ + 6_______*_ + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 3] + + d[-xs - 2] + + d[-xs - 1] + + d[0] + + d[xs + 1] + + d[xs + 2] + + d[xs + xs + 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_______*_ + 3_____**__ + 4____0____ + 5__**_____ + 6_*_______ + 7_________ + 8_________ */ + float sum = + d[-xs - xs + 3] + + d[-xs + 2] + + d[-xs + 1] + + d[0] + + d[xs - 1] + + d[xs - 2] + + d[xs + xs - 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2_________ + 3______**_ + 4____0*___ + 5__**_____ + 6**_______ + 7_________ + 8_________ */ + + float sum = + d[xs + xs - 4] + + d[xs + xs - 3] + + d[xs - 2] + + d[xs - 1] + + d[0] + + d[1] + + d[-xs + 2] + + d[-xs + 3]; + retval += sum * sum; + } + { + /* 0_________ + 1_________ + 2**_______ + 3__**_____ + 4____0*___ + 5______**_ + 6_________ + 7_________ + 8_________ */ + float sum = + d[-xs - xs - 4] + + d[-xs - xs - 3] + + d[-xs - 2] + + d[-xs - 1] + + d[0] + + d[1] + + d[xs + 2] + + d[xs + 3]; + retval += sum * sum; + } + { + /* 0__*______ + 1__*______ + 2___*_____ + 3___*_____ + 4____0____ + 5____*____ + 6_____*___ + 7_____*___ + 8_________ */ + float sum = + d[-xs3 - xs - 2] + + d[-xs3 - 2] + + d[-xs - xs - 1] + + d[-xs - 1] + + d[0] + + d[xs] + + d[xs + xs + 1] + + d[xs3 + 1]; + retval += sum * sum; + } + { + /* 0______*__ + 1______*__ + 2_____*___ + 3_____*___ + 4____0____ + 5____*____ + 6___*_____ + 7___*_____ + 8_________ */ + float sum = + d[-xs3 - xs + 2] + + d[-xs3 + 2] + + d[-xs - xs + 1] + + d[-xs + 1] + + d[0] + + d[xs] + + d[xs + xs - 1] + + d[xs3 - 1]; + retval += sum * sum; + } + return retval; +} + +// Returns MaltaUnit. "fastMode" avoids bounds-checks when x0 and y0 are known +// to be far enough from the image borders. +template +static BUTTERAUGLI_INLINE float PaddedMaltaUnit( + float* const BUTTERAUGLI_RESTRICT diffs, const size_t x0, const size_t y0, + const size_t xsize_, const size_t ysize_) { + int ix0 = y0 * xsize_ + x0; + const float* BUTTERAUGLI_RESTRICT d = &diffs[ix0]; + if (fastMode || + (x0 >= 4 && y0 >= 4 && x0 < (xsize_ - 4) && y0 < (ysize_ - 4))) { + return MaltaUnit(Tag(), d, xsize_); + } + + float borderimage[9 * 9]; + for (int dy = 0; dy < 9; ++dy) { + int y = y0 + dy - 4; + if (y < 0 || y >= ysize_) { + for (int dx = 0; dx < 9; ++dx) { + borderimage[dy * 9 + dx] = 0.0f; + } + } else { + for (int dx = 0; dx < 9; ++dx) { + int x = x0 + dx - 4; + if (x < 0 || x >= xsize_) { + borderimage[dy * 9 + dx] = 0.0f; + } else { + borderimage[dy * 9 + dx] = diffs[y * xsize_ + x]; + } + } + } + } + return MaltaUnit(Tag(), &borderimage[4 * 9 + 4], 9); +} + +template +static void MaltaDiffMapImpl(const ImageF& lum0, const ImageF& lum1, + const size_t xsize_, const size_t ysize_, + const double w_0gt1, + const double w_0lt1, + double norm1, + const double len, const double mulli, + ImageF* block_diff_ac) { + const float kWeight0 = 0.5; + const float kWeight1 = 0.33; + + const double w_pre0gt1 = mulli * sqrt(kWeight0 * w_0gt1) / (len * 2 + 1); + const double w_pre0lt1 = mulli * sqrt(kWeight1 * w_0lt1) / (len * 2 + 1); + const float norm2_0gt1 = w_pre0gt1 * norm1; + const float norm2_0lt1 = w_pre0lt1 * norm1; + + std::vector diffs(ysize_ * xsize_); + for (size_t y = 0, ix = 0; y < ysize_; ++y) { + const float* BUTTERAUGLI_RESTRICT const row0 = lum0.Row(y); + const float* BUTTERAUGLI_RESTRICT const row1 = lum1.Row(y); + for (size_t x = 0; x < xsize_; ++x, ++ix) { + const float absval = 0.5 * std::abs(row0[x]) + 0.5 * std::abs(row1[x]); + const float diff = row0[x] - row1[x]; + const float scaler = norm2_0gt1 / (static_cast(norm1) + absval); + + // Primary symmetric quadratic objective. + diffs[ix] = scaler * diff; + + const float scaler2 = norm2_0lt1 / (static_cast(norm1) + absval); + const double fabs0 = fabs(row0[x]); + + // Secondary half-open quadratic objectives. + const double too_small = 0.55 * fabs0; + const double too_big = 1.05 * fabs0; + + if (row0[x] < 0) { + if (row1[x] > -too_small) { + double impact = scaler2 * (row1[x] + too_small); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } else if (row1[x] < -too_big) { + double impact = scaler2 * (-row1[x] - too_big); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } + } else { + if (row1[x] < too_small) { + double impact = scaler2 * (too_small - row1[x]); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } else if (row1[x] > too_big) { + double impact = scaler2 * (row1[x] - too_big); + if (diff < 0) { + diffs[ix] -= impact; + } else { + diffs[ix] += impact; + } + } + } + } + } + + size_t y0 = 0; + // Top + for (; y0 < 4; ++y0) { + float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } + + // Middle + for (; y0 < ysize_ - 4; ++y0) { + float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); + size_t x0 = 0; + for (; x0 < 4; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + for (; x0 < xsize_ - 4; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + + for (; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } + + // Bottom + for (; y0 < ysize_; ++y0) { + float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); + for (size_t x0 = 0; x0 < xsize_; ++x0) { + row_diff[x0] += + PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); + } + } +} + +void ButteraugliComparator::MaltaDiffMap( + const ImageF& lum0, const ImageF& lum1, + const double w_0gt1, + const double w_0lt1, + const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.354191303559; + MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, w_0gt1, w_0lt1, + norm1, len, + mulli, block_diff_ac); +} + +void ButteraugliComparator::MaltaDiffMapLF( + const ImageF& lum0, const ImageF& lum1, + const double w_0gt1, + const double w_0lt1, + const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { + PROFILER_FUNC; + const double len = 3.75; + static const double mulli = 0.405371989604; + MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, + w_0gt1, w_0lt1, + norm1, len, + mulli, block_diff_ac); +} + +ImageF ButteraugliComparator::CombineChannels( + const std::vector& mask_xyb, + const std::vector& mask_xyb_dc, + const std::vector& block_diff_dc, + const std::vector& block_diff_ac) const { + PROFILER_FUNC; + ImageF result(xsize_, ysize_); + for (size_t y = 0; y < ysize_; ++y) { + float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); + for (size_t x = 0; x < xsize_; ++x) { + float mask[3]; + float dc_mask[3]; + float diff_dc[3]; + float diff_ac[3]; + for (int i = 0; i < 3; ++i) { + mask[i] = mask_xyb[i].Row(y)[x]; + dc_mask[i] = mask_xyb_dc[i].Row(y)[x]; + diff_dc[i] = block_diff_dc[i].Row(y)[x]; + diff_ac[i] = block_diff_ac[i].Row(y)[x]; + } + row_out[x] = (DotProduct(diff_dc, dc_mask) + DotProduct(diff_ac, mask)); + } + } + return result; +} + +double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { + PROFILER_FUNC; + float retval = 0.0f; + for (size_t y = 0; y < diffmap.ysize(); ++y) { + const float * const BUTTERAUGLI_RESTRICT row = diffmap.Row(y); + for (size_t x = 0; x < diffmap.xsize(); ++x) { + retval = std::max(retval, row[x]); + } + } + return retval; +} + +#include + +// ===== Functions used by Mask only ===== +static std::array MakeMask( + double extmul, double extoff, + double mul, double offset, + double scaler) { + std::array lut; + for (int i = 0; i < lut.size(); ++i) { + const double c = mul / ((0.01 * scaler * i) + offset); + lut[i] = kGlobalScale * (1.0 + extmul * (c + extoff)); + if (lut[i] < 1e-5) { + lut[i] = 1e-5; + } + assert(lut[i] >= 0.0); + lut[i] *= lut[i]; + } + return lut; +} + +double MaskX(double delta) { + static const double extmul = 2.59885507073; + static const double extoff = 3.08805636789; + static const double offset = 0.315424196682; + static const double scaler = 16.2770141832; + static const double mul = 5.62939030582; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskY(double delta) { + static const double extmul = 0.9613705131; + static const double extoff = -0.581933100068; + static const double offset = 1.00846207765; + static const double scaler = 2.2342321176; + static const double mul = 6.64307621174; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcX(double delta) { + static const double extmul = 10.0470705878; + static const double extoff = 3.18472654033; + static const double offset = 0.0551512255218; + static const double scaler = 70.0; + static const double mul = 0.373092999662; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +double MaskDcY(double delta) { + static const double extmul = 0.0115640939227; + static const double extoff = 45.9483175519; + static const double offset = 0.0142290066313; + static const double scaler = 5.0; + static const double mul = 2.52611324247; + static const std::array lut = + MakeMask(extmul, extoff, mul, offset, scaler); + return InterpolateClampNegative(lut.data(), lut.size(), delta); +} + +ImageF DiffPrecompute(const ImageF& xyb0, const ImageF& xyb1) { + PROFILER_FUNC; + const size_t xsize = xyb0.xsize(); + const size_t ysize = xyb0.ysize(); + ImageF result(xsize, ysize); + size_t x2, y2; + for (size_t y = 0; y < ysize; ++y) { + if (y + 1 < ysize) { + y2 = y + 1; + } else if (y > 0) { + y2 = y - 1; + } else { + y2 = y; + } + const float* const BUTTERAUGLI_RESTRICT row0_in = xyb0.Row(y); + const float* const BUTTERAUGLI_RESTRICT row1_in = xyb1.Row(y); + const float* const BUTTERAUGLI_RESTRICT row0_in2 = xyb0.Row(y2); + const float* const BUTTERAUGLI_RESTRICT row1_in2 = xyb1.Row(y2); + float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); + for (size_t x = 0; x < xsize; ++x) { + if (x + 1 < xsize) { + x2 = x + 1; + } else if (x > 0) { + x2 = x - 1; + } else { + x2 = x; + } + double sup0 = (fabs(row0_in[x] - row0_in[x2]) + + fabs(row0_in[x] - row0_in2[x])); + double sup1 = (fabs(row1_in[x] - row1_in[x2]) + + fabs(row1_in[x] - row1_in2[x])); + static const double mul0 = 0.918416534734; + row_out[x] = mul0 * std::min(sup0, sup1); + static const double cutoff = 55.0184555849; + if (row_out[x] >= cutoff) { + row_out[x] = cutoff; + } + } + } + return result; +} + +void Mask(const std::vector& xyb0, + const std::vector& xyb1, + std::vector* BUTTERAUGLI_RESTRICT mask, + std::vector* BUTTERAUGLI_RESTRICT mask_dc) { + PROFILER_FUNC; + const size_t xsize = xyb0[0].xsize(); + const size_t ysize = xyb0[0].ysize(); + mask->resize(3); + *mask_dc = CreatePlanes(xsize, ysize, 3); + double muls[2] = { + 0.207017089891, + 0.267138152891, + }; + double normalizer = { + 1.0 / (muls[0] + muls[1]), + }; + static const double r0 = 2.3770330432; + static const double r1 = 9.04353323561; + static const double r2 = 9.24456601467; + static const double border_ratio = -0.0724948220913; + + { + // X component + ImageF diff = DiffPrecompute(xyb0[0], xyb1[0]); + ImageF blurred = Blur(diff, r2, border_ratio); + (*mask)[0] = ImageF(xsize, ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + (*mask)[0].Row(y)[x] = blurred.Row(y)[x]; + } + } + } + { + // Y component + (*mask)[1] = ImageF(xsize, ysize); + ImageF diff = DiffPrecompute(xyb0[1], xyb1[1]); + ImageF blurred1 = Blur(diff, r0, border_ratio); + ImageF blurred2 = Blur(diff, r1, border_ratio); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + const double val = normalizer * ( + muls[0] * blurred1.Row(y)[x] + + muls[1] * blurred2.Row(y)[x]); + (*mask)[1].Row(y)[x] = val; + } + } + } + // B component + (*mask)[2] = ImageF(xsize, ysize); + static const double mul[2] = { + 16.6963293877, + 2.1364621982, + }; + static const double w00 = 36.4671237619; + static const double w11 = 2.1887170895; + static const double w_ytob_hf = std::max( + 0.086624184478, + 0.0); + static const double w_ytob_lf = 21.6804277046; + static const double p1_to_p0 = 0.0513061271723; + + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + const double s0 = (*mask)[0].Row(y)[x]; + const double s1 = (*mask)[1].Row(y)[x]; + const double p1 = mul[1] * w11 * s1; + const double p0 = mul[0] * w00 * s0 + p1_to_p0 * p1; + + (*mask)[0].Row(y)[x] = MaskX(p0); + (*mask)[1].Row(y)[x] = MaskY(p1); + (*mask)[2].Row(y)[x] = w_ytob_hf * MaskY(p1); + (*mask_dc)[0].Row(y)[x] = MaskDcX(p0); + (*mask_dc)[1].Row(y)[x] = MaskDcY(p1); + (*mask_dc)[2].Row(y)[x] = w_ytob_lf * MaskDcY(p1); + } + } +} + +void ButteraugliDiffmap(const std::vector &rgb0_image, + const std::vector &rgb1_image, + ImageF &result_image) { + const size_t xsize = rgb0_image[0].xsize(); + const size_t ysize = rgb0_image[0].ysize(); + static const int kMax = 8; + if (xsize < kMax || ysize < kMax) { + // Butteraugli values for small (where xsize or ysize is smaller + // than 8 pixels) images are non-sensical, but most likely it is + // less disruptive to try to compute something than just give up. + // Temporarily extend the borders of the image to fit 8 x 8 size. + int xborder = xsize < kMax ? (kMax - xsize) / 2 : 0; + int yborder = ysize < kMax ? (kMax - ysize) / 2 : 0; + size_t xscaled = std::max(kMax, xsize); + size_t yscaled = std::max(kMax, ysize); + std::vector scaled0 = CreatePlanes(xscaled, yscaled, 3); + std::vector scaled1 = CreatePlanes(xscaled, yscaled, 3); + for (int i = 0; i < 3; ++i) { + for (int y = 0; y < yscaled; ++y) { + for (int x = 0; x < xscaled; ++x) { + size_t x2 = std::min(xsize - 1, std::max(0, x - xborder)); + size_t y2 = std::min(ysize - 1, std::max(0, y - yborder)); + scaled0[i].Row(y)[x] = rgb0_image[i].Row(y2)[x2]; + scaled1[i].Row(y)[x] = rgb1_image[i].Row(y2)[x2]; + } + } + } + ImageF diffmap_scaled; + ButteraugliDiffmap(scaled0, scaled1, diffmap_scaled); + result_image = ImageF(xsize, ysize); + for (int y = 0; y < ysize; ++y) { + for (int x = 0; x < xsize; ++x) { + result_image.Row(y)[x] = diffmap_scaled.Row(y + yborder)[x + xborder]; + } + } + return; + } + ButteraugliComparator butteraugli(rgb0_image); + butteraugli.Diffmap(rgb1_image, result_image); +} + +bool ButteraugliInterface(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap, + double &diffvalue) { + const size_t xsize = rgb0[0].xsize(); + const size_t ysize = rgb0[0].ysize(); + if (xsize < 1 || ysize < 1) { + return false; // No image. + } + for (int i = 1; i < 3; i++) { + if (rgb0[i].xsize() != xsize || rgb0[i].ysize() != ysize || + rgb1[i].xsize() != xsize || rgb1[i].ysize() != ysize) { + return false; // Image planes must have same dimensions. + } + } + ButteraugliDiffmap(rgb0, rgb1, diffmap); + diffvalue = ButteraugliScoreFromDiffmap(diffmap); + return true; +} + +bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, + const std::vector > &rgb, std::vector &quant) { + if (xsize < 16 || ysize < 16) { + return false; // Butteraugli is undefined for small images. + } + size_t size = xsize * ysize; + + std::vector rgb_planes = PlanesFromPacked(xsize, ysize, rgb); + std::vector scale_xyb; + std::vector scale_xyb_dc; + Mask(rgb_planes, rgb_planes, &scale_xyb, &scale_xyb_dc); + quant.reserve(size); + + // Mask gives us values in 3 color channels, but for now we take only + // the intensity channel. + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + quant.push_back(scale_xyb[1].Row(y)[x]); + } + } + return true; +} + +double ButteraugliFuzzyClass(double score) { + static const double fuzzy_width_up = 6.07887388532; + static const double fuzzy_width_down = 5.50793514384; + static const double m0 = 2.0; + static const double scaler = 0.840253347958; + double val; + if (score < 1.0) { + // val in [scaler .. 2.0] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_down)); + val -= 1.0; // from [1 .. 2] to [0 .. 1] + val *= 2.0 - scaler; // from [0 .. 1] to [0 .. 2.0 - scaler] + val += scaler; // from [0 .. 2.0 - scaler] to [scaler .. 2.0] + } else { + // val in [0 .. scaler] + val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_up)); + val *= scaler; + } + return val; +} + +double ButteraugliFuzzyInverse(double seek) { + double pos = 0; + for (double range = 1.0; range >= 1e-10; range *= 0.5) { + double cur = ButteraugliFuzzyClass(pos); + if (cur < seek) { + pos -= range; + } else { + pos += range; + } + } + return pos; +} + +namespace { + +void ScoreToRgb(double score, double good_threshold, double bad_threshold, + uint8_t rgb[3]) { + double heatmap[12][3] = { + {0, 0, 0}, + {0, 0, 1}, + {0, 1, 1}, + {0, 1, 0}, // Good level + {1, 1, 0}, + {1, 0, 0}, // Bad level + {1, 0, 1}, + {0.5, 0.5, 1.0}, + {1.0, 0.5, 0.5}, // Pastel colors for the very bad quality range. + {1.0, 1.0, 0.5}, + { + 1, 1, 1, + }, + { + 1, 1, 1, + }, + }; + if (score < good_threshold) { + score = (score / good_threshold) * 0.3; + } else if (score < bad_threshold) { + score = 0.3 + + (score - good_threshold) / (bad_threshold - good_threshold) * 0.15; + } else { + score = 0.45 + (score - bad_threshold) / (bad_threshold * 12) * 0.5; + } + static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); + score = std::min(std::max(score * (kTableSize - 1), 0.0), + kTableSize - 2); + int ix = static_cast(score); + double mix = score - ix; + for (int i = 0; i < 3; ++i) { + double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; + rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); + } +} + +} // namespace + +void CreateHeatMapImage(const std::vector& distmap, + double good_threshold, double bad_threshold, + size_t xsize, size_t ysize, + std::vector* heatmap) { + heatmap->resize(3 * xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + int px = xsize * y + x; + double d = distmap[px]; + uint8_t* rgb = &(*heatmap)[3 * px]; + ScoreToRgb(d, good_threshold, bad_threshold, rgb); + } + } +} + +} // namespace butteraugli diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h new file mode 100755 index 0000000..2f5d938 --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h @@ -0,0 +1,619 @@ +// Copyright 2016 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Disclaimer: This is not an official Google product. +// +// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) + +#ifndef BUTTERAUGLI_BUTTERAUGLI_H_ +#define BUTTERAUGLI_BUTTERAUGLI_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUTTERAUGLI_ENABLE_CHECKS 0 + +// This is the main interface to butteraugli image similarity +// analysis function. + +namespace butteraugli { + +template +class Image; + +using Image8 = Image; +using ImageF = Image; + +// ButteraugliInterface defines the public interface for butteraugli. +// +// It calculates the difference between rgb0 and rgb1. +// +// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains +// the red image for c == 0, green for c == 1, blue for c == 2. Location index +// px is calculated as y * xsize + x. +// +// Value of pixels of images rgb0 and rgb1 need to be represented as raw +// intensity. Most image formats store gamma corrected intensity in pixel +// values. This gamma correction has to be removed, by applying the following +// function: +// butteraugli_val = 255.0 * pow(png_val / 255.0, gamma); +// A typical value of gamma is 2.2. It is usually stored in the image header. +// Take care not to confuse that value with its inverse. The gamma value should +// be always greater than one. +// Butteraugli does not work as intended if the caller does not perform +// gamma correction. +// +// diffmap will contain an image of the size xsize * ysize, containing +// localized differences for values px (indexed with the px the same as rgb0 +// and rgb1). diffvalue will give a global score of similarity. +// +// A diffvalue smaller than kButteraugliGood indicates that images can be +// observed as the same image. +// diffvalue larger than kButteraugliBad indicates that a difference between +// the images can be observed. +// A diffvalue between kButteraugliGood and kButteraugliBad indicates that +// a subtle difference can be observed between the images. +// +// Returns true on success. + +bool ButteraugliInterface(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap, + double &diffvalue); + +const double kButteraugliQuantLow = 0.26; +const double kButteraugliQuantHigh = 1.454; + +// Converts the butteraugli score into fuzzy class values that are continuous +// at the class boundary. The class boundary location is based on human +// raters, but the slope is arbitrary. Particularly, it does not reflect +// the expectation value of probabilities of the human raters. It is just +// expected that a smoother class boundary will allow for higher-level +// optimization algorithms to work faster. +// +// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the +// scoring is fuzzy, a butteraugli score of 0.96 would return a class of +// around 1.9. +double ButteraugliFuzzyClass(double score); + +// Input values should be in range 0 (bad) to 2 (good). Use +// kButteraugliNormalization as normalization. +double ButteraugliFuzzyInverse(double seek); + +// Returns a map which can be used for adaptive quantization. Values can +// typically range from kButteraugliQuantLow to kButteraugliQuantHigh. Low +// values require coarse quantization (e.g. near random noise), high values +// require fine quantization (e.g. in smooth bright areas). +bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, + const std::vector > &rgb, std::vector &quant); + +// Implementation details, don't use anything below or your code will +// break in the future. + +#ifdef _MSC_VER +#define BUTTERAUGLI_RESTRICT __restrict +#else +#define BUTTERAUGLI_RESTRICT __restrict__ +#endif + +#ifdef _MSC_VER +#define BUTTERAUGLI_INLINE __forceinline +#else +#define BUTTERAUGLI_INLINE inline +#endif + +#ifdef __clang__ +// Early versions of Clang did not support __builtin_assume_aligned. +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) +#elif defined(__GNUC__) +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1 +#else +#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0 +#endif + +// Returns a void* pointer which the compiler then assumes is N-byte aligned. +// Example: float* PIK_RESTRICT aligned = (float*)PIK_ASSUME_ALIGNED(in, 32); +// +// The assignment semantics are required by GCC/Clang. ICC provides an in-place +// __assume_aligned, whereas MSVC's __assume appears unsuitable. +#if BUTTERAUGLI_HAS_ASSUME_ALIGNED +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) +#else +#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr) +#endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED + +// Functions that depend on the cache line size. +class CacheAligned { + public: + static constexpr size_t kPointerSize = sizeof(void *); + static constexpr size_t kCacheLineSize = 64; + + // The aligned-return annotation is only allowed on function declarations. + static void *Allocate(const size_t bytes); + static void Free(void *aligned_pointer); +}; + +template +using CacheAlignedUniquePtrT = std::unique_ptr; + +using CacheAlignedUniquePtr = CacheAlignedUniquePtrT; + +template +static inline CacheAlignedUniquePtrT Allocate(const size_t entries) { + return CacheAlignedUniquePtrT( + static_cast( + CacheAligned::Allocate(entries * sizeof(T))), + CacheAligned::Free); +} + +// Returns the smallest integer not less than "amount" that is divisible by +// "multiple", which must be a power of two. +template +static inline size_t Align(const size_t amount) { + static_assert(multiple != 0 && ((multiple & (multiple - 1)) == 0), + "Align<> argument must be a power of two"); + return (amount + multiple - 1) & ~(multiple - 1); +} + +// Single channel, contiguous (cache-aligned) rows separated by padding. +// T must be POD. +// +// Rationale: vectorization benefits from aligned operands - unaligned loads and +// especially stores are expensive when the address crosses cache line +// boundaries. Introducing padding after each row ensures the start of a row is +// aligned, and that row loops can process entire vectors (writes to the padding +// are allowed and ignored). +// +// We prefer a planar representation, where channels are stored as separate +// 2D arrays, because that simplifies vectorization (repeating the same +// operation on multiple adjacent components) without the complexity of a +// hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients can easily iterate +// over all components in a row and Image requires no knowledge of the pixel +// format beyond the component type "T". The downside is that we duplicate the +// xsize/ysize members for each channel. +// +// This image layout could also be achieved with a vector and a row accessor +// function, but a class wrapper with support for "deleter" allows wrapping +// existing memory allocated by clients without copying the pixels. It also +// provides convenient accessors for xsize/ysize, which shortens function +// argument lists. Supports move-construction so it can be stored in containers. +template +class Image { + // Returns cache-aligned row stride, being careful to avoid 2K aliasing. + static size_t BytesPerRow(const size_t xsize) { + // Allow reading one extra AVX-2 vector on the right margin. + const size_t row_size = xsize * sizeof(T) + 32; + const size_t align = CacheAligned::kCacheLineSize; + size_t bytes_per_row = (row_size + align - 1) & ~(align - 1); + // During the lengthy window before writes are committed to memory, CPUs + // guard against read after write hazards by checking the address, but + // only the lower 11 bits. We avoid a false dependency between writes to + // consecutive rows by ensuring their sizes are not multiples of 2 KiB. + if (bytes_per_row % 2048 == 0) { + bytes_per_row += align; + } + return bytes_per_row; + } + + public: + using T = ComponentType; + + Image() : xsize_(0), ysize_(0), bytes_per_row_(0), + bytes_(static_cast(nullptr), Ignore) {} + + Image(const size_t xsize, const size_t ysize) + : xsize_(xsize), + ysize_(ysize), + bytes_per_row_(BytesPerRow(xsize)), + bytes_(Allocate(bytes_per_row_ * ysize)) {} + + Image(const size_t xsize, const size_t ysize, T val) + : xsize_(xsize), + ysize_(ysize), + bytes_per_row_(BytesPerRow(xsize)), + bytes_(Allocate(bytes_per_row_ * ysize)) { + for (size_t y = 0; y < ysize_; ++y) { + T* const BUTTERAUGLI_RESTRICT row = Row(y); + for (int x = 0; x < xsize_; ++x) { + row[x] = val; + } + } + } + + Image(const size_t xsize, const size_t ysize, + uint8_t * const BUTTERAUGLI_RESTRICT bytes, + const size_t bytes_per_row) + : xsize_(xsize), + ysize_(ysize), + bytes_per_row_(bytes_per_row), + bytes_(bytes, Ignore) {} + + // Move constructor (required for returning Image from function) + Image(Image &&other) + : xsize_(other.xsize_), + ysize_(other.ysize_), + bytes_per_row_(other.bytes_per_row_), + bytes_(std::move(other.bytes_)) {} + + // Move assignment (required for std::vector) + Image &operator=(Image &&other) { + xsize_ = other.xsize_; + ysize_ = other.ysize_; + bytes_per_row_ = other.bytes_per_row_; + bytes_ = std::move(other.bytes_); + return *this; + } + + void Swap(Image &other) { + std::swap(xsize_, other.xsize_); + std::swap(ysize_, other.ysize_); + std::swap(bytes_per_row_, other.bytes_per_row_); + std::swap(bytes_, other.bytes_); + } + + // How many pixels. + size_t xsize() const { return xsize_; } + size_t ysize() const { return ysize_; } + + T *const BUTTERAUGLI_RESTRICT Row(const size_t y) { +#ifdef BUTTERAUGLI_ENABLE_CHECKS + if (y >= ysize_) { + printf("Row %zu out of bounds (ysize=%zu)\n", y, ysize_); + abort(); + } +#endif + void *row = bytes_.get() + y * bytes_per_row_; + return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); + } + + const T *const BUTTERAUGLI_RESTRICT Row(const size_t y) const { +#ifdef BUTTERAUGLI_ENABLE_CHECKS + if (y >= ysize_) { + printf("Const row %zu out of bounds (ysize=%zu)\n", y, ysize_); + abort(); + } +#endif + void *row = bytes_.get() + y * bytes_per_row_; + return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); + } + + // Raw access to byte contents, for interfacing with other libraries. + // Unsigned char instead of char to avoid surprises (sign extension). + uint8_t * const BUTTERAUGLI_RESTRICT bytes() { return bytes_.get(); } + const uint8_t * const BUTTERAUGLI_RESTRICT bytes() const { + return bytes_.get(); + } + size_t bytes_per_row() const { return bytes_per_row_; } + + // Returns number of pixels (some of which are padding) per row. Useful for + // computing other rows via pointer arithmetic. + intptr_t PixelsPerRow() const { + static_assert(CacheAligned::kCacheLineSize % sizeof(T) == 0, + "Padding must be divisible by the pixel size."); + return static_cast(bytes_per_row_ / sizeof(T)); + } + + private: + // Deleter used when bytes are not owned. + static void Ignore(void *ptr) {} + + // (Members are non-const to enable assignment during move-assignment.) + size_t xsize_; // original intended pixels, not including any padding. + size_t ysize_; + size_t bytes_per_row_; // [bytes] including padding. + CacheAlignedUniquePtr bytes_; +}; + +// Returns newly allocated planes of the given dimensions. +template +static inline std::vector> CreatePlanes(const size_t xsize, + const size_t ysize, + const size_t num_planes) { + std::vector> planes; + planes.reserve(num_planes); + for (size_t i = 0; i < num_planes; ++i) { + planes.emplace_back(xsize, ysize); + } + return planes; +} + +// Returns a new image with the same dimensions and pixel values. +template +static inline Image CopyPixels(const Image &other) { + Image copy(other.xsize(), other.ysize()); + const void *BUTTERAUGLI_RESTRICT from = other.bytes(); + void *BUTTERAUGLI_RESTRICT to = copy.bytes(); + memcpy(to, from, other.ysize() * other.bytes_per_row()); + return copy; +} + +// Returns new planes with the same dimensions and pixel values. +template +static inline std::vector> CopyPlanes( + const std::vector> &planes) { + std::vector> copy; + copy.reserve(planes.size()); + for (const Image &plane : planes) { + copy.push_back(CopyPixels(plane)); + } + return copy; +} + +// Compacts a padded image into a preallocated packed vector. +template +static inline void CopyToPacked(const Image &from, std::vector *to) { + const size_t xsize = from.xsize(); + const size_t ysize = from.ysize(); +#if BUTTERAUGLI_ENABLE_CHECKS + if (to->size() < xsize * ysize) { + printf("%zu x %zu exceeds %zu capacity\n", xsize, ysize, to->size()); + abort(); + } +#endif + for (size_t y = 0; y < ysize; ++y) { + const float* const BUTTERAUGLI_RESTRICT row_from = from.Row(y); + float* const BUTTERAUGLI_RESTRICT row_to = to->data() + y * xsize; + memcpy(row_to, row_from, xsize * sizeof(T)); + } +} + +// Expands a packed vector into a preallocated padded image. +template +static inline void CopyFromPacked(const std::vector &from, Image *to) { + const size_t xsize = to->xsize(); + const size_t ysize = to->ysize(); + assert(from.size() == xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + const float* const BUTTERAUGLI_RESTRICT row_from = + from.data() + y * xsize; + float* const BUTTERAUGLI_RESTRICT row_to = to->Row(y); + memcpy(row_to, row_from, xsize * sizeof(T)); + } +} + +template +static inline std::vector> PlanesFromPacked( + const size_t xsize, const size_t ysize, + const std::vector> &packed) { + std::vector> planes; + planes.reserve(packed.size()); + for (const std::vector &p : packed) { + planes.push_back(Image(xsize, ysize)); + CopyFromPacked(p, &planes.back()); + } + return planes; +} + +template +static inline std::vector> PackedFromPlanes( + const std::vector> &planes) { + assert(!planes.empty()); + const size_t num_pixels = planes[0].xsize() * planes[0].ysize(); + std::vector> packed; + packed.reserve(planes.size()); + for (const Image &image : planes) { + packed.push_back(std::vector(num_pixels)); + CopyToPacked(image, &packed.back()); + } + return packed; +} + +struct PsychoImage { + std::vector uhf; + std::vector hf; + std::vector mf; + std::vector lf; +}; + +class ButteraugliComparator { + public: + ButteraugliComparator(const std::vector& rgb0); + + // Computes the butteraugli map between the original image given in the + // constructor and the distorted image give here. + void Diffmap(const std::vector& rgb1, ImageF& result) const; + + // Same as above, but OpsinDynamicsImage() was already applied. + void DiffmapOpsinDynamicsImage(const std::vector& xyb1, + ImageF& result) const; + + // Same as above, but the frequency decomposition was already applied. + void DiffmapPsychoImage(const PsychoImage& ps1, ImageF &result) const; + + void Mask(std::vector* BUTTERAUGLI_RESTRICT mask, + std::vector* BUTTERAUGLI_RESTRICT mask_dc) const; + + private: + void MaltaDiffMapLF(const ImageF& y0, + const ImageF& y1, + double w_0gt1, + double w_0lt1, + double normalization, + ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; + + void MaltaDiffMap(const ImageF& y0, + const ImageF& y1, + double w_0gt1, + double w_0lt1, + double normalization, + ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; + + ImageF CombineChannels(const std::vector& scale_xyb, + const std::vector& scale_xyb_dc, + const std::vector& block_diff_dc, + const std::vector& block_diff_ac) const; + + const size_t xsize_; + const size_t ysize_; + const size_t num_pixels_; + PsychoImage pi0_; +}; + +void ButteraugliDiffmap(const std::vector &rgb0, + const std::vector &rgb1, + ImageF &diffmap); + +double ButteraugliScoreFromDiffmap(const ImageF& distmap); + +// Generate rgb-representation of the distance between two images. +void CreateHeatMapImage(const std::vector &distmap, + double good_threshold, double bad_threshold, + size_t xsize, size_t ysize, + std::vector *heatmap); + +// Compute values of local frequency and dc masking based on the activity +// in the two images. +void Mask(const std::vector& xyb0, + const std::vector& xyb1, + std::vector* BUTTERAUGLI_RESTRICT mask, + std::vector* BUTTERAUGLI_RESTRICT mask_dc); + +template +BUTTERAUGLI_INLINE void RgbToXyb(const V &r, const V &g, const V &b, + V *BUTTERAUGLI_RESTRICT valx, + V *BUTTERAUGLI_RESTRICT valy, + V *BUTTERAUGLI_RESTRICT valb) { + *valx = r - g; + *valy = r + g; + *valb = b; +} + +template +BUTTERAUGLI_INLINE void OpsinAbsorbance(const V &in0, const V &in1, + const V &in2, + V *BUTTERAUGLI_RESTRICT out0, + V *BUTTERAUGLI_RESTRICT out1, + V *BUTTERAUGLI_RESTRICT out2) { + // https://en.wikipedia.org/wiki/Photopsin absorbance modeling. + static const double mixi0 = 0.254462330846; + static const double mixi1 = 0.488238255095; + static const double mixi2 = 0.0635278003854; + static const double mixi3 = 1.01681026909; + static const double mixi4 = 0.195214015766; + static const double mixi5 = 0.568019861857; + static const double mixi6 = 0.0860755536007; + static const double mixi7 = 1.1510118369; + static const double mixi8 = 0.07374607900105684; + static const double mixi9 = 0.06142425304154509; + static const double mixi10 = 0.24416850520714256; + static const double mixi11 = 1.20481945273; + + const V mix0(mixi0); + const V mix1(mixi1); + const V mix2(mixi2); + const V mix3(mixi3); + const V mix4(mixi4); + const V mix5(mixi5); + const V mix6(mixi6); + const V mix7(mixi7); + const V mix8(mixi8); + const V mix9(mixi9); + const V mix10(mixi10); + const V mix11(mixi11); + + *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; + *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; + *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; +} + +std::vector OpsinDynamicsImage(const std::vector& rgb); + +ImageF Blur(const ImageF& in, float sigma, float border_ratio); + +double SimpleGamma(double v); + +double GammaMinArg(); +double GammaMaxArg(); + +// Polynomial evaluation via Clenshaw's scheme (similar to Horner's). +// Template enables compile-time unrolling of the recursion, but must reside +// outside of a class due to the specialization. +template +static inline void ClenshawRecursion(const double x, const double *coefficients, + double *b1, double *b2) { + const double x_b1 = x * (*b1); + const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX]; + *b2 = *b1; + *b1 = t; + + ClenshawRecursion(x, coefficients, b1, b2); +} + +// Base case +template <> +inline void ClenshawRecursion<0>(const double x, const double *coefficients, + double *b1, double *b2) { + const double x_b1 = x * (*b1); + // The final iteration differs - no 2 * x_b1 here. + *b1 = x_b1 - (*b2) + coefficients[0]; +} + +// Rational polynomial := dividing two polynomial evaluations. These are easier +// to find than minimax polynomials. +struct RationalPolynomial { + template + static double EvaluatePolynomial(const double x, + const double (&coefficients)[N]) { + double b1 = 0.0; + double b2 = 0.0; + ClenshawRecursion(x, coefficients, &b1, &b2); + return b1; + } + + // Evaluates the polynomial at x (in [min_value, max_value]). + inline double operator()(const double x) const { + // First normalize to [0, 1]. + const double x01 = (x - min_value) / (max_value - min_value); + // And then to [-1, 1] domain of Chebyshev polynomials. + const double xc = 2.0 * x01 - 1.0; + + const double yp = EvaluatePolynomial(xc, p); + const double yq = EvaluatePolynomial(xc, q); + if (yq == 0.0) return 0.0; + return static_cast(yp / yq); + } + + // Domain of the polynomials; they are undefined elsewhere. + double min_value; + double max_value; + + // Coefficients of T_n (Chebyshev polynomials of the first kind). + // Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability. + double p[5 + 1]; + double q[5 + 1]; +}; + +static inline double GammaPolynomial(double value) { + static const RationalPolynomial r = { + 0.971783, 590.188894, + { + 98.7821300963361, 164.273222212631, 92.948112871376, + 33.8165311212688, 6.91626704983562, 0.556380877028234 + }, + { + 1, 1.64339473427892, 0.89392405219969, 0.298947051776379, + 0.0507146002577288, 0.00226495093949756 + }}; + return r(value); +} + +} // namespace butteraugli + +#endif // BUTTERAUGLI_BUTTERAUGLI_H_ diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc new file mode 100755 index 0000000..f38af1d --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include "butteraugli/butteraugli.h" + +extern "C" { +#include "png.h" +#include "jpeglib.h" +} + +namespace butteraugli { +namespace { + +// "rgb": cleared and filled with same-sized image planes (one per channel); +// either RGB, or RGBA if the PNG contains an alpha channel. +bool ReadPNG(FILE* f, std::vector* rgb) { + png_structp png_ptr = + png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png_ptr) { + return false; + } + + png_infop info_ptr = png_create_info_struct(png_ptr); + if (!info_ptr) { + png_destroy_read_struct(&png_ptr, NULL, NULL); + return false; + } + + if (setjmp(png_jmpbuf(png_ptr)) != 0) { + // Ok we are here because of the setjmp. + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return false; + } + + rewind(f); + png_init_io(png_ptr, f); + + // The png_transforms flags are as follows: + // packing == convert 1,2,4 bit images, + // strip == 16 -> 8 bits / channel, + // shift == use sBIT dynamics, and + // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. + const unsigned int png_transforms = + PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; + + png_read_png(png_ptr, info_ptr, png_transforms, NULL); + + png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); + + const int xsize = png_get_image_width(png_ptr, info_ptr); + const int ysize = png_get_image_height(png_ptr, info_ptr); + const int components = png_get_channels(png_ptr, info_ptr); + + *rgb = CreatePlanes(xsize, ysize, 3); + + switch (components) { + case 1: { + // GRAYSCALE + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); + + for (int x = 0; x < xsize; ++x) { + const uint8_t gray = row[x]; + row0[x] = row1[x] = row2[x] = gray; + } + } + break; + } + case 2: { + // GRAYSCALE_ALPHA + rgb->push_back(Image8(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); + + for (int x = 0; x < xsize; ++x) { + const uint8_t gray = row[2 * x + 0]; + const uint8_t alpha = row[2 * x + 1]; + row0[x] = gray; + row1[x] = gray; + row2[x] = gray; + row3[x] = alpha; + } + } + break; + } + case 3: { + // RGB + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); + + for (int x = 0; x < xsize; ++x) { + row0[x] = row[3 * x + 0]; + row1[x] = row[3 * x + 1]; + row2[x] = row[3 * x + 2]; + } + } + break; + } + case 4: { + // RGBA + rgb->push_back(Image8(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); + uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); + + for (int x = 0; x < xsize; ++x) { + row0[x] = row[4 * x + 0]; + row1[x] = row[4 * x + 1]; + row2[x] = row[4 * x + 2]; + row3[x] = row[4 * x + 3]; + } + } + break; + } + default: + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return false; + } + png_destroy_read_struct(&png_ptr, &info_ptr, NULL); + return true; +} + +const double* NewSrgbToLinearTable() { + double* table = new double[256]; + for (int i = 0; i < 256; ++i) { + const double srgb = i / 255.0; + table[i] = + 255.0 * (srgb <= 0.04045 ? srgb / 12.92 + : std::pow((srgb + 0.055) / 1.055, 2.4)); + } + return table; +} + +void jpeg_catch_error(j_common_ptr cinfo) { + (*cinfo->err->output_message) (cinfo); + jmp_buf* jpeg_jmpbuf = (jmp_buf*) cinfo->client_data; + jpeg_destroy(cinfo); + longjmp(*jpeg_jmpbuf, 1); +} + +// "rgb": cleared and filled with same-sized image planes (one per channel); +// either RGB, or RGBA if the PNG contains an alpha channel. +bool ReadJPEG(FILE* f, std::vector* rgb) { + rewind(f); + + struct jpeg_decompress_struct cinfo; + struct jpeg_error_mgr jerr; + cinfo.err = jpeg_std_error(&jerr); + jmp_buf jpeg_jmpbuf; + cinfo.client_data = &jpeg_jmpbuf; + jerr.error_exit = jpeg_catch_error; + if (setjmp(jpeg_jmpbuf)) { + return false; + } + + jpeg_create_decompress(&cinfo); + + jpeg_stdio_src(&cinfo, f); + jpeg_read_header(&cinfo, TRUE); + jpeg_start_decompress(&cinfo); + + int row_stride = cinfo.output_width * cinfo.output_components; + JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) + ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); + + const size_t xsize = cinfo.output_width; + const size_t ysize = cinfo.output_height; + + *rgb = CreatePlanes(xsize, ysize, 3); + + switch (cinfo.out_color_space) { + case JCS_GRAYSCALE: + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + + const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = + (*rgb)[0].Row(cinfo.output_scanline - 1); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = + (*rgb)[1].Row(cinfo.output_scanline - 1); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = + (*rgb)[2].Row(cinfo.output_scanline - 1); + + for (int x = 0; x < xsize; x++) { + const uint8_t gray = row[x]; + row0[x] = row1[x] = row2[x] = gray; + } + } + break; + + case JCS_RGB: + while (cinfo.output_scanline < cinfo.output_height) { + jpeg_read_scanlines(&cinfo, buffer, 1); + + const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; + uint8_t* const BUTTERAUGLI_RESTRICT row0 = + (*rgb)[0].Row(cinfo.output_scanline - 1); + uint8_t* const BUTTERAUGLI_RESTRICT row1 = + (*rgb)[1].Row(cinfo.output_scanline - 1); + uint8_t* const BUTTERAUGLI_RESTRICT row2 = + (*rgb)[2].Row(cinfo.output_scanline - 1); + + for (int x = 0; x < xsize; x++) { + row0[x] = row[3 * x + 0]; + row1[x] = row[3 * x + 1]; + row2[x] = row[3 * x + 2]; + } + } + break; + + default: + jpeg_destroy_decompress(&cinfo); + return false; + } + + jpeg_finish_decompress(&cinfo); + jpeg_destroy_decompress(&cinfo); + return true; +} + +// Translate R, G, B channels from sRGB to linear space. If an alpha channel +// is present, overlay the image over a black or white background. Overlaying +// is done in the sRGB space; while technically incorrect, this is aligned with +// many other software (web browsers, WebP near lossless). +void FromSrgbToLinear(const std::vector& rgb, + std::vector& linear, int background) { + const size_t xsize = rgb[0].xsize(); + const size_t ysize = rgb[0].ysize(); + static const double* const kSrgbToLinearTable = NewSrgbToLinearTable(); + + if (rgb.size() == 3) { // RGB + for (int c = 0; c < 3; c++) { + linear.push_back(ImageF(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); + float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); + for (size_t x = 0; x < xsize; x++) { + const int value = row_rgb[x]; + row_linear[x] = kSrgbToLinearTable[value]; + } + } + } + } else { // RGBA + for (int c = 0; c < 3; c++) { + linear.push_back(ImageF(xsize, ysize)); + for (int y = 0; y < ysize; ++y) { + const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); + float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); + const uint8_t* const BUTTERAUGLI_RESTRICT row_alpha = rgb[3].Row(y); + for (size_t x = 0; x < xsize; x++) { + int value; + if (row_alpha[x] == 255) { + value = row_rgb[x]; + } else if (row_alpha[x] == 0) { + value = background; + } else { + const int fg_weight = row_alpha[x]; + const int bg_weight = 255 - fg_weight; + value = + (row_rgb[x] * fg_weight + background * bg_weight + 127) / 255; + } + row_linear[x] = kSrgbToLinearTable[value]; + } + } + } + } +} + +std::vector ReadImageOrDie(const char* filename) { + std::vector rgb; + FILE* f = fopen(filename, "rb"); + if (!f) { + fprintf(stderr, "Cannot open %s\n", filename); + exit(1); + } + unsigned char magic[2]; + if (fread(magic, 1, 2, f) != 2) { + fprintf(stderr, "Cannot read from %s\n", filename); + exit(1); + } + if (magic[0] == 0xFF && magic[1] == 0xD8) { + if (!ReadJPEG(f, &rgb)) { + fprintf(stderr, "File %s is a malformed JPEG.\n", filename); + exit(1); + } + } else { + if (!ReadPNG(f, &rgb)) { + fprintf(stderr, "File %s is neither a valid JPEG nor a valid PNG.\n", + filename); + exit(1); + } + } + fclose(f); + return rgb; +} + +static void ScoreToRgb(double score, double good_threshold, + double bad_threshold, uint8_t rgb[3]) { + double heatmap[12][3] = { + { 0, 0, 0 }, + { 0, 0, 1 }, + { 0, 1, 1 }, + { 0, 1, 0 }, // Good level + { 1, 1, 0 }, + { 1, 0, 0 }, // Bad level + { 1, 0, 1 }, + { 0.5, 0.5, 1.0 }, + { 1.0, 0.5, 0.5 }, // Pastel colors for the very bad quality range. + { 1.0, 1.0, 0.5 }, + { 1, 1, 1, }, + { 1, 1, 1, }, + }; + if (score < good_threshold) { + score = (score / good_threshold) * 0.3; + } else if (score < bad_threshold) { + score = 0.3 + (score - good_threshold) / + (bad_threshold - good_threshold) * 0.15; + } else { + score = 0.45 + (score - bad_threshold) / + (bad_threshold * 12) * 0.5; + } + static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); + score = std::min(std::max( + score * (kTableSize - 1), 0.0), kTableSize - 2); + int ix = static_cast(score); + double mix = score - ix; + for (int i = 0; i < 3; ++i) { + double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; + rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); + } +} + +void CreateHeatMapImage(const ImageF& distmap, double good_threshold, + double bad_threshold, size_t xsize, size_t ysize, + std::vector* heatmap) { + heatmap->resize(3 * xsize * ysize); + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + int px = xsize * y + x; + double d = distmap.Row(y)[x]; + uint8_t* rgb = &(*heatmap)[3 * px]; + ScoreToRgb(d, good_threshold, bad_threshold, rgb); + } + } +} + +// main() function, within butteraugli namespace for convenience. +int Run(int argc, char* argv[]) { + if (argc != 3 && argc != 4) { + fprintf(stderr, + "Usage: %s {image1.(png|jpg|jpeg)} {image2.(png|jpg|jpeg)} " + "[heatmap.ppm]\n", + argv[0]); + return 1; + } + + std::vector rgb1 = ReadImageOrDie(argv[1]); + std::vector rgb2 = ReadImageOrDie(argv[2]); + + if (rgb1.size() != rgb2.size()) { + fprintf(stderr, "Different number of channels: %lu vs %lu\n", rgb1.size(), + rgb2.size()); + exit(1); + } + + for (size_t c = 0; c < rgb1.size(); ++c) { + if (rgb1[c].xsize() != rgb2[c].xsize() || + rgb1[c].ysize() != rgb2[c].ysize()) { + fprintf( + stderr, "The images are not equal in size: (%lu,%lu) vs (%lu,%lu)\n", + rgb1[c].xsize(), rgb2[c].xsize(), rgb1[c].ysize(), rgb2[c].ysize()); + return 1; + } + } + + // TODO: Figure out if it is a good idea to fetch the gamma from the image + // instead of applying sRGB conversion. + std::vector linear1, linear2; + // Overlay the image over a black background. + FromSrgbToLinear(rgb1, linear1, 0); + FromSrgbToLinear(rgb2, linear2, 0); + ImageF diff_map, diff_map_on_white; + double diff_value; + if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map, + diff_value)) { + fprintf(stderr, "Butteraugli comparison failed\n"); + return 1; + } + ImageF* diff_map_ptr = &diff_map; + if (rgb1.size() == 4 || rgb2.size() == 4) { + // If the alpha channel is present, overlay the image over a white + // background as well. + FromSrgbToLinear(rgb1, linear1, 255); + FromSrgbToLinear(rgb2, linear2, 255); + double diff_value_on_white; + if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map_on_white, + diff_value_on_white)) { + fprintf(stderr, "Butteraugli comparison failed\n"); + return 1; + } + if (diff_value_on_white > diff_value) { + diff_value = diff_value_on_white; + diff_map_ptr = &diff_map_on_white; + } + } + printf("%lf\n", diff_value); + + if (argc == 4) { + const double good_quality = ::butteraugli::ButteraugliFuzzyInverse(1.5); + const double bad_quality = ::butteraugli::ButteraugliFuzzyInverse(0.5); + std::vector rgb; + CreateHeatMapImage(*diff_map_ptr, good_quality, bad_quality, + rgb1[0].xsize(), rgb2[0].ysize(), &rgb); + FILE* const fmap = fopen(argv[3], "wb"); + if (fmap == NULL) { + fprintf(stderr, "Cannot open %s\n", argv[3]); + perror("fopen"); + return 1; + } + bool ok = true; + if (fprintf(fmap, "P6\n%lu %lu\n255\n", + rgb1[0].xsize(), rgb1[0].ysize()) < 0){ + perror("fprintf"); + ok = false; + } + if (ok && fwrite(rgb.data(), 1, rgb.size(), fmap) != rgb.size()) { + perror("fwrite"); + ok = false; + } + if (fclose(fmap) != 0) { + perror("fclose"); + ok = false; + } + if (!ok) return 1; + } + + return 0; +} + +} // namespace +} // namespace butteraugli + +int main(int argc, char** argv) { return butteraugli::Run(argc, argv); } diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD new file mode 100755 index 0000000..92c9ddc --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD @@ -0,0 +1,89 @@ +# Description: +# The Independent JPEG Group's JPEG runtime library. + +licenses(["notice"]) # custom notice-style license, see LICENSE + +cc_library( + name = "jpeg", + srcs = [ + "cderror.h", + "cdjpeg.h", + "jaricom.c", + "jcapimin.c", + "jcapistd.c", + "jcarith.c", + "jccoefct.c", + "jccolor.c", + "jcdctmgr.c", + "jchuff.c", + "jcinit.c", + "jcmainct.c", + "jcmarker.c", + "jcmaster.c", + "jcomapi.c", + "jconfig.h", + "jcparam.c", + "jcprepct.c", + "jcsample.c", + "jctrans.c", + "jdapimin.c", + "jdapistd.c", + "jdarith.c", + "jdatadst.c", + "jdatasrc.c", + "jdcoefct.c", + "jdcolor.c", + "jdct.h", + "jddctmgr.c", + "jdhuff.c", + "jdinput.c", + "jdmainct.c", + "jdmarker.c", + "jdmaster.c", + "jdmerge.c", + "jdpostct.c", + "jdsample.c", + "jdtrans.c", + "jerror.c", + "jfdctflt.c", + "jfdctfst.c", + "jfdctint.c", + "jidctflt.c", + "jidctfst.c", + "jidctint.c", + "jinclude.h", + "jmemmgr.c", + "jmemnobs.c", + "jmemsys.h", + "jmorecfg.h", + "jquant1.c", + "jquant2.c", + "jutils.c", + "jversion.h", + "transupp.h", + ], + hdrs = [ + "jerror.h", + "jpegint.h", + "jpeglib.h", + ], + includes = ["."], + visibility = ["//visibility:public"], +) + +genrule( + name = "configure", + outs = ["jconfig.h"], + cmd = "cat <$@\n" + + "#define HAVE_PROTOTYPES 1\n" + + "#define HAVE_UNSIGNED_CHAR 1\n" + + "#define HAVE_UNSIGNED_SHORT 1\n" + + "#define HAVE_STDDEF_H 1\n" + + "#define HAVE_STDLIB_H 1\n" + + "#ifdef WIN32\n" + + "#define INLINE __inline\n" + + "#else\n" + + "#define INLINE __inline__\n" + + "#endif\n" + + "EOF\n", +) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD new file mode 100755 index 0000000..9ff982b --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD @@ -0,0 +1,33 @@ +# Description: +# libpng is the official PNG reference library. + +licenses(["notice"]) # BSD/MIT-like license + +cc_library( + name = "png", + srcs = [ + "png.c", + "pngerror.c", + "pngget.c", + "pngmem.c", + "pngpread.c", + "pngread.c", + "pngrio.c", + "pngrtran.c", + "pngrutil.c", + "pngset.c", + "pngtrans.c", + "pngwio.c", + "pngwrite.c", + "pngwtran.c", + "pngwutil.c", + ], + hdrs = [ + "png.h", + "pngconf.h", + ], + includes = ["."], + linkopts = ["-lm"], + visibility = ["//visibility:public"], + deps = ["@zlib_archive//:zlib"], +) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD new file mode 100755 index 0000000..edb77fd --- /dev/null +++ b/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD @@ -0,0 +1,36 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) # BSD/MIT-like license (for zlib) + +cc_library( + name = "zlib", + srcs = [ + "adler32.c", + "compress.c", + "crc32.c", + "crc32.h", + "deflate.c", + "deflate.h", + "gzclose.c", + "gzguts.h", + "gzlib.c", + "gzread.c", + "gzwrite.c", + "infback.c", + "inffast.c", + "inffast.h", + "inffixed.h", + "inflate.c", + "inflate.h", + "inftrees.c", + "inftrees.h", + "trees.c", + "trees.h", + "uncompr.c", + "zconf.h", + "zutil.c", + "zutil.h", + ], + hdrs = ["zlib.h"], + includes = ["."], +) From 6f7110dd4fe54d68827e371ca7c72cac90265347 Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Wed, 12 Aug 2020 03:45:08 -0700 Subject: [PATCH 04/10] Added tests for sandboxed library and transaction --- oss-internship-2020/guetzli/tests/BUILD.bazel | 43 ++--- .../guetzli/tests/guetzli_sapi_test.cc | 168 ++++++++---------- .../guetzli/tests/guetzli_transaction_test.cc | 129 ++++++++++++++ .../guetzli/tests/testdata/bees_reference.jpg | Bin 0 -> 38625 bytes .../testdata/{landscape.jpg => nature.jpg} | Bin .../tests/testdata/nature_reference.jpg | Bin 0 -> 10816 bytes 6 files changed, 223 insertions(+), 117 deletions(-) create mode 100644 oss-internship-2020/guetzli/tests/testdata/bees_reference.jpg rename oss-internship-2020/guetzli/tests/testdata/{landscape.jpg => nature.jpg} (100%) create mode 100644 oss-internship-2020/guetzli/tests/testdata/nature_reference.jpg diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel index b7c7517..fa8b4ae 100644 --- a/oss-internship-2020/guetzli/tests/BUILD.bazel +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -1,22 +1,23 @@ -# cc_test( -# name = "transaction_tests", -# srcs = ["guetzli_transaction_test.cc"], -# visibility=["//visibility:public"], -# includes = ["."], -# deps = [ -# "//:guetzli_sapi", -# "@googletest//:gtest_main" -# ], -# ) +cc_test( + name = "transaction_tests", + srcs = ["guetzli_transaction_test.cc"], + visibility=["//visibility:public"], + deps = [ + "//:guetzli_sapi", + "@googletest//:gtest_main" + ], + size = "large", + data = glob(["testdata/*"]) +) -# cc_test( -# name = "sapi_lib_tests", -# srcs = ["guetzli_sapi_test.cc"], -# visibility=["//visibility:public"], -# includes=[".."], -# deps = [ -# "//:guetzli_sapi", -# "@googletest//:gtest_main" -# ], -# data = glob(["testdata/*"]) -# ) \ No newline at end of file +cc_test( + name = "sapi_lib_tests", + srcs = ["guetzli_sapi_test.cc"], + visibility=["//visibility:public"], + deps = [ + "//:guetzli_sapi", + "@googletest//:gtest_main" + ], + size = "large", + data = glob(["testdata/*"]) +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc index 73e54d6..2de4cb3 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -10,6 +10,7 @@ #include #include #include +#include namespace guetzli { namespace sandbox { @@ -18,15 +19,18 @@ namespace tests { namespace { constexpr const char* IN_PNG_FILENAME = "bees.png"; -constexpr const char* IN_JPG_FILENAME = "landscape.jpg"; +constexpr const char* IN_JPG_FILENAME = "nature.jpg"; +constexpr const char* PNG_REFERENCE_FILENAME = "bees_reference.jpg"; +constexpr const char* JPG_REFERENCE_FILENAME = "nature_reference.jpg"; -constexpr int IN_PNG_FILE_SIZE = 177'424; -constexpr int IN_JPG_FILE_SIZE = 14'418; +constexpr int PNG_EXPECTED_SIZE = 38'625; +constexpr int JPG_EXPECTED_SIZE = 10'816; constexpr int DEFAULT_QUALITY_TARGET = 95; +constexpr int DEFAULT_MEMLIMIT_MB = 6000; constexpr const char* RELATIVE_PATH_TO_TESTDATA = - "/guetzli/guetzli-sandboxed/tests/testdata/"; + "/guetzli_sandboxed/tests/testdata/"; std::string GetPathToInputFile(const char* filename) { return std::string(getenv("TEST_SRCDIR")) @@ -46,6 +50,18 @@ std::string ReadFromFile(const std::string& filename) { return result.str(); } +template +bool CompareBytesInLenValAndContainer(const sapi::v::LenVal& lenval, + const Container& container) { + return std::equal( + lenval.GetData(), lenval.GetData() + lenval.GetDataSize(), + container.begin(), + [](const uint8_t lhs, const auto rhs) { + return lhs == static_cast(rhs); + } + ); +} + } // namespace class GuetzliSapiTest : public ::testing::Test { @@ -60,106 +76,66 @@ protected: std::unique_ptr api_; }; -TEST_F(GuetzliSapiTest, ReadDataFromFd) { - std::string input_file_path = GetPathToInputFile(IN_PNG_FILENAME); - int fd = open(input_file_path.c_str(), O_RDONLY); - ASSERT_TRUE(fd != -1) << "Error opening input file"; - sapi::v::Fd remote_fd(fd); - auto send_fd_status = sandbox_->TransferToSandboxee(&remote_fd); - ASSERT_TRUE(send_fd_status.ok()) << "Error sending fd to sandboxee"; - ASSERT_TRUE(remote_fd.GetRemoteFd() != -1) << "Error opening remote fd"; - sapi::v::LenVal data(0); - auto read_status = - api_->ReadDataFromFd(remote_fd.GetRemoteFd(), data.PtrBoth()); - ASSERT_TRUE(read_status.value_or(false)) << "Error reading data from fd"; - ASSERT_EQ(data.GetDataSize(), IN_PNG_FILE_SIZE) << "Wrong size of file"; -} - -// TEST_F(GuetzliSapiTest, WriteDataToFd) { - -// } - -TEST_F(GuetzliSapiTest, ReadPng) { - std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); - ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; - sapi::v::LenVal in_data(data.data(), data.size()); - sapi::v::Int xsize, ysize; - sapi::v::LenVal rgb_out(0); - - auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), - ysize.PtrBoth(), rgb_out.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; - ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; - ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; -} - -TEST_F(GuetzliSapiTest, ReadJpeg) { - std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); - ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; - sapi::v::LenVal in_data(data.data(), data.size()); - sapi::v::Int xsize, ysize; - - auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, - xsize.PtrBoth(), ysize.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; - ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; - ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; -} - // This test can take up to few minutes depending on your hardware TEST_F(GuetzliSapiTest, ProcessRGB) { - std::string data = ReadFromFile(GetPathToInputFile(IN_PNG_FILENAME)); - ASSERT_EQ(data.size(), IN_PNG_FILE_SIZE) << "Error reading input file"; - sapi::v::LenVal in_data(data.data(), data.size()); - sapi::v::Int xsize, ysize; - sapi::v::LenVal rgb_out(0); - - auto status = api_->ReadPng(in_data.PtrBefore(), xsize.PtrBoth(), - ysize.PtrBoth(), rgb_out.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing png data"; - ASSERT_EQ(xsize.GetValue(), 444) << "Error parsing width"; - ASSERT_EQ(ysize.GetValue(), 258) << "Error parsing height"; - auto quality = - api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); - ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; - sapi::v::Struct params; - sapi::v::LenVal out_data(0); - params.mutable_data()->butteraugli_target = quality.value(); - - status = api_->ProcessRGBData(params.PtrBefore(), 0, rgb_out.PtrBefore(), - xsize.GetValue(), ysize.GetValue(), out_data.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing png file"; - ASSERT_EQ(out_data.GetDataSize(), 38'625); - //ADD COMPARSION WITH REFERENCE OUTPUT + sapi::v::Fd in_fd(open(GetPathToInputFile(IN_PNG_FILENAME).c_str(), + O_RDONLY)); + ASSERT_TRUE(in_fd.GetValue() != -1) << "Error opening input file"; + ASSERT_EQ(api_->sandbox()->TransferToSandboxee(&in_fd), absl::OkStatus()) + << "Error transfering fd to sandbox"; + ASSERT_TRUE(in_fd.GetRemoteFd() != -1) << "Error opening remote fd"; + sapi::v::Struct processing_params; + *processing_params.mutable_data() = {in_fd.GetRemoteFd(), + 0, + DEFAULT_QUALITY_TARGET, + DEFAULT_MEMLIMIT_MB + }; + sapi::v::LenVal output(0); + auto processing_result = api_->ProcessRgb(processing_params.PtrBefore(), + output.PtrBoth()); + ASSERT_TRUE(processing_result.value_or(false)) << "Error processing rgb data"; + ASSERT_EQ(output.GetDataSize(), PNG_EXPECTED_SIZE) + << "Incorrect result data size"; + std::string reference_data = + ReadFromFile(GetPathToInputFile(PNG_REFERENCE_FILENAME)); + ASSERT_EQ(output.GetDataSize(), reference_data.size()) + << "Incorrect result data size"; + ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) + << "Processed data doesn't match reference output"; } // This test can take up to few minutes depending on your hardware TEST_F(GuetzliSapiTest, ProcessJpeg) { - std::string data = ReadFromFile(GetPathToInputFile(IN_JPG_FILENAME)); - ASSERT_EQ(data.size(), IN_JPG_FILE_SIZE) << "Error reading input file"; - sapi::v::LenVal in_data(data.data(), data.size()); - sapi::v::Int xsize, ysize; - - auto status = api_->ReadJpegData(in_data.PtrBefore(), 0, - xsize.PtrBoth(), ysize.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg data"; - ASSERT_EQ(xsize.GetValue(), 180) << "Error parsing width"; - ASSERT_EQ(ysize.GetValue(), 180) << "Error parsing height"; - - auto quality = - api_->ButteraugliScoreQuality(static_cast(DEFAULT_QUALITY_TARGET)); - ASSERT_TRUE(quality.ok()) << "Error calculating butteraugli quality"; - sapi::v::Struct params; - params.mutable_data()->butteraugli_target = quality.value(); - sapi::v::LenVal out_data(0); - - status = api_->ProcessJPEGString(params.PtrBefore(), 0, in_data.PtrBefore(), - out_data.PtrBoth()); - ASSERT_TRUE(status.value_or(false)) << "Error processing jpeg file"; - ASSERT_EQ(out_data.GetDataSize(), 10'816); - //ADD COMPARSION WITH REFERENCE OUTPUT + sapi::v::Fd in_fd(open(GetPathToInputFile(IN_JPG_FILENAME).c_str(), + O_RDONLY)); + ASSERT_TRUE(in_fd.GetValue() != -1) << "Error opening input file"; + ASSERT_EQ(api_->sandbox()->TransferToSandboxee(&in_fd), absl::OkStatus()) + << "Error transfering fd to sandbox"; + ASSERT_TRUE(in_fd.GetRemoteFd() != -1) << "Error opening remote fd"; + sapi::v::Struct processing_params; + *processing_params.mutable_data() = {in_fd.GetRemoteFd(), + 0, + DEFAULT_QUALITY_TARGET, + DEFAULT_MEMLIMIT_MB + }; + sapi::v::LenVal output(0); + auto processing_result = api_->ProcessJpeg(processing_params.PtrBefore(), + output.PtrBoth()); + ASSERT_TRUE(processing_result.value_or(false)) << "Error processing jpg data"; + ASSERT_EQ(output.GetDataSize(), JPG_EXPECTED_SIZE) + << "Incorrect result data size"; + std::string reference_data = + ReadFromFile(GetPathToInputFile(JPG_REFERENCE_FILENAME)); + ASSERT_EQ(output.GetDataSize(), reference_data.size()) + << "Incorrect result data size"; + ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) + << "Processed data doesn't match reference output"; } +// TEST_F(GuetzliSapiTest, WriteDataToFd) { +// sapi::v::Fd fd(open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); +// } + } // namespace tests } // namespace sandbox } // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc index 8471d1e..b9e3a22 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -1,2 +1,131 @@ #include "gtest/gtest.h" #include "guetzli_transaction.h" +#include "sandboxed_api/sandbox2/util/fileops.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace guetzli { +namespace sandbox { +namespace tests { + +namespace { + +constexpr const char* IN_PNG_FILENAME = "bees.png"; +constexpr const char* IN_JPG_FILENAME = "nature.jpg"; +constexpr const char* PNG_REFERENCE_FILENAME = "bees_reference.jpg"; +constexpr const char* JPG_REFERENCE_FILENAME = "nature_reference.jpg"; + +constexpr int PNG_EXPECTED_SIZE = 38'625; +constexpr int JPG_EXPECTED_SIZE = 10'816; + +constexpr int DEFAULT_QUALITY_TARGET = 95; +constexpr int DEFAULT_MEMLIMIT_MB = 6000; + +constexpr const char* RELATIVE_PATH_TO_TESTDATA = + "/guetzli_sandboxed/tests/testdata/"; + +std::string GetPathToInputFile(const char* filename) { + return std::string(getenv("TEST_SRCDIR")) + + std::string(RELATIVE_PATH_TO_TESTDATA) + + std::string(filename); +} + +std::string ReadFromFile(const std::string& filename) { + std::ifstream stream(filename, std::ios::binary); + + if (!stream.is_open()) { + return ""; + } + + std::stringstream result; + result << stream.rdbuf(); + return result.str(); +} + +} // namespace + +TEST(GuetzliTransactionTest, TestTransactionJpg) { + sandbox2::file_util::fileops::FDCloser in_fd_closer( + open(GetPathToInputFile(IN_JPG_FILENAME).c_str(), O_RDONLY)); + ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input jpg file"; + sandbox2::file_util::fileops::FDCloser out_fd_closer( + open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); + ASSERT_TRUE(out_fd_closer.get() != -1) << "Error creating temp output file"; + TransactionParams params = { + in_fd_closer.get(), + out_fd_closer.get(), + 0, + DEFAULT_QUALITY_TARGET, + DEFAULT_MEMLIMIT_MB + }; + { + GuetzliTransaction transaction(std::move(params)); + auto result = transaction.Run(); + + ASSERT_TRUE(result.ok()) << result.ToString(); + } + ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) + << "Local output fd closed"; + auto reference_data = ReadFromFile(GetPathToInputFile(JPG_REFERENCE_FILENAME)); + auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); + ASSERT_EQ(reference_data.size(), output_size) + << "Different sizes of reference and returned data"; + ASSERT_EQ(lseek(out_fd_closer.get(), 0, SEEK_SET), 0) + << "Error repositioning out file"; + + std::unique_ptr buf(new char[output_size]); + auto status = read(out_fd_closer.get(), buf.get(), output_size); + ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; + + ASSERT_TRUE( + std::equal(buf.get(), buf.get() + output_size, reference_data.begin())) + << "Returned data doesn't match reference"; +} + +TEST(GuetzliTransactionTest, TestTransactionPng) { + sandbox2::file_util::fileops::FDCloser in_fd_closer( + open(GetPathToInputFile(IN_PNG_FILENAME).c_str(), O_RDONLY)); + ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input png file"; + sandbox2::file_util::fileops::FDCloser out_fd_closer( + open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); + ASSERT_TRUE(out_fd_closer.get() != -1) << "Error creating temp output file"; + TransactionParams params = { + in_fd_closer.get(), + out_fd_closer.get(), + 0, + DEFAULT_QUALITY_TARGET, + DEFAULT_MEMLIMIT_MB + }; + { + GuetzliTransaction transaction(std::move(params)); + auto result = transaction.Run(); + + ASSERT_TRUE(result.ok()) << result.ToString(); + } + ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) + << "Local output fd closed"; + auto reference_data = ReadFromFile(GetPathToInputFile(PNG_REFERENCE_FILENAME)); + auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); + ASSERT_EQ(reference_data.size(), output_size) + << "Different sizes of reference and returned data"; + ASSERT_EQ(lseek(out_fd_closer.get(), 0, SEEK_SET), 0) + << "Error repositioning out file"; + + std::unique_ptr buf(new char[output_size]); + auto status = read(out_fd_closer.get(), buf.get(), output_size); + ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; + + ASSERT_TRUE( + std::equal(buf.get(), buf.get() + output_size, reference_data.begin())) + << "Returned data doesn't match refernce"; +} + +} // namespace tests +} // namespace sandbox +} // namespace guetzli \ No newline at end of file diff --git a/oss-internship-2020/guetzli/tests/testdata/bees_reference.jpg b/oss-internship-2020/guetzli/tests/testdata/bees_reference.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24700673fecdef2b805ee1a2529631fd923b5621 GIT binary patch literal 38625 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 XWVO%er7V6NB4LbF?W=O^%+LSXDy^Ji literal 0 HcmV?d00001 diff --git a/oss-internship-2020/guetzli/tests/testdata/landscape.jpg b/oss-internship-2020/guetzli/tests/testdata/nature.jpg similarity index 100% rename from oss-internship-2020/guetzli/tests/testdata/landscape.jpg rename to oss-internship-2020/guetzli/tests/testdata/nature.jpg diff --git a/oss-internship-2020/guetzli/tests/testdata/nature_reference.jpg b/oss-internship-2020/guetzli/tests/testdata/nature_reference.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b4f1c42fa4459d2bee59df5f6ccc50019062781c GIT binary patch literal 10816 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 jl;qmYi+>0s Date: Wed, 12 Aug 2020 14:09:40 -0700 Subject: [PATCH 05/10] Codestyle update --- oss-internship-2020/guetzli/BUILD.bazel | 14 ++++ oss-internship-2020/guetzli/README.md | 0 .../guetzli/guetzli_entry_points.cc | 36 +++++--- .../guetzli/guetzli_entry_points.h | 21 ++++- oss-internship-2020/guetzli/guetzli_sandbox.h | 26 +++++- .../guetzli/guetzli_sandboxed.cc | 25 ++++-- .../guetzli/guetzli_transaction.cc | 28 +++++-- .../guetzli/guetzli_transaction.h | 38 ++++++--- oss-internship-2020/guetzli/tests/BUILD.bazel | 14 ++++ .../guetzli/tests/guetzli_sapi_test.cc | 84 +++++++++++-------- .../guetzli/tests/guetzli_transaction_test.cc | 68 +++++++++------ 11 files changed, 253 insertions(+), 101 deletions(-) create mode 100644 oss-internship-2020/guetzli/README.md diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel index 6e3332a..fe5bfb8 100644 --- a/oss-internship-2020/guetzli/BUILD.bazel +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -1,3 +1,17 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + load( "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", "sapi_library", diff --git a/oss-internship-2020/guetzli/README.md b/oss-internship-2020/guetzli/README.md new file mode 100644 index 0000000..e69de29 diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc index a815673..ec0743b 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.cc +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -1,3 +1,25 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include +#include +#include + #include "guetzli/jpeg_data_reader.h" #include "guetzli/quality.h" #include "guetzli_entry_points.h" @@ -5,17 +27,10 @@ #include "sandboxed_api/sandbox2/util/fileops.h" #include "sandboxed_api/util/statusor.h" -#include -#include -#include -#include -#include -#include - namespace { constexpr int kBytesPerPixel = 350; -constexpr int kLowestMemusageMB = 100; // in MB +constexpr int kLowestMemusageMB = 100; struct GuetzliInitData { std::string in_data; @@ -26,7 +41,7 @@ struct GuetzliInitData { template void CopyMemoryToLenVal(const T* data, size_t size, sapi::LenValStruct* out_data) { - free(out_data->data); // Not sure about this + free(out_data->data); // Not sure about this out_data->size = size; T* new_out = static_cast(malloc(size)); memcpy(new_out, data, size); @@ -49,7 +64,6 @@ sapi::StatusOr ReadFromFd(int fd) { status = read(fd, buf.get(), fsize); if (status < 0) { - lseek(fd, 0, SEEK_SET); return absl::FailedPreconditionError( "Error reading input from fd" ); @@ -275,4 +289,4 @@ extern "C" bool ProcessRgb(const ProcessingParams* processing_params, extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data) { return sandbox2::file_util::fileops::WriteToFD(fd, static_cast(data->data), data->size); -} \ No newline at end of file +} diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.h b/oss-internship-2020/guetzli/guetzli_entry_points.h index 90b0d97..b7fd2c5 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.h +++ b/oss-internship-2020/guetzli/guetzli_entry_points.h @@ -1,4 +1,19 @@ -#pragma once +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GUETZLI_SANDBOXED_GUETZLI_ENTRY_POINTS_H_ +#define GUETZLI_SANDBOXED_GUETZLI_ENTRY_POINTS_H_ #include "guetzli/processor.h" #include "sandboxed_api/lenval_core.h" @@ -15,4 +30,6 @@ extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, sapi::LenValStruct* output); extern "C" bool ProcessRgb(const ProcessingParams* processing_params, sapi::LenValStruct* output); -extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data); \ No newline at end of file +extern "C" bool WriteDataToFd(int fd, sapi::LenValStruct* data); + +#endif // GUETZLI_SANDBOXED_GUETZLI_ENTRY_POINTS_H_ diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h index 3fa2b10..e55875a 100644 --- a/oss-internship-2020/guetzli/guetzli_sandbox.h +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -1,13 +1,29 @@ -#pragma once +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ +#define GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ #include #include -#include "guetzli_sapi.sapi.h" #include "sandboxed_api/sandbox2/policy.h" #include "sandboxed_api/sandbox2/policybuilder.h" #include "sandboxed_api/util/flag.h" +#include "guetzli_sapi.sapi.h" + namespace guetzli { namespace sandbox { @@ -32,5 +48,7 @@ class GuetzliSapiSandbox : public GuetzliSandbox { } }; -} // namespace sandbox -} // namespace guetzli \ No newline at end of file +} // namespace sandbox +} // namespace guetzli + +#endif // GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc index 3393b8f..cb6543c 100644 --- a/oss-internship-2020/guetzli/guetzli_sandboxed.cc +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -1,19 +1,34 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include #include #include #include #include -#include #include -#include "guetzli_transaction.h" #include "sandboxed_api/sandbox2/util/fileops.h" #include "sandboxed_api/util/statusor.h" +#include "guetzli_transaction.h" + namespace { constexpr int kDefaultJPEGQuality = 95; -constexpr int kDefaultMemlimitMB = 6000; // in MB +constexpr int kDefaultMemlimitMB = 6000; void TerminateHandler() { fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" @@ -124,9 +139,9 @@ int main(int argc, const char** argv) { } } else { - fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? + fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? return 1; } return 0; -} \ No newline at end of file +} diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc index 0f7e1bd..2613bdf 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -1,3 +1,17 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + #include "guetzli_transaction.h" #include @@ -45,8 +59,8 @@ absl::Status GuetzliTransaction::Init() { "Error receiving remote FD: remote output fd is set to -1"); } - in_fd_.OwnLocalFd(false); // FDCloser will close local fd - out_fd_.OwnLocalFd(false); // FDCloser will close local fd + in_fd_.OwnLocalFd(false); // FDCloser will close local fd + out_fd_.OwnLocalFd(false); // FDCloser will close local fd return absl::OkStatus(); } @@ -62,14 +76,14 @@ absl::Status GuetzliTransaction::Main() { params_.memlimit_mb }; - auto result_status = image_type_ == ImageType::JPEG ? + auto result_status = image_type_ == ImageType::kJpeg ? api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()) : api.ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); if (!result_status.value_or(false)) { std::stringstream error_stream; error_stream << "Error processing " - << (image_type_ == ImageType::JPEG ? "jpeg" : "rgb") << " data" + << (image_type_ == ImageType::kJpeg ? "jpeg" : "rgb") << " data" << std::endl; return absl::FailedPreconditionError( @@ -113,8 +127,8 @@ sapi::StatusOr GuetzliTransaction::GetImageTypeFromFd(int fd) const { } return memcmp(read_buf, kPNGMagicBytes, sizeof(kPNGMagicBytes)) == 0 ? - ImageType::PNG : ImageType::JPEG; + ImageType::kPng : ImageType::kJpeg; } -} // namespace sandbox -} // namespace guetzli \ No newline at end of file +} // namespace sandbox +} // namespace guetzli diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index 8dbd7ec..2db681d 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -1,21 +1,34 @@ -#pragma once +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ +#define GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ #include #include -#include "guetzli_sandbox.h" #include "sandboxed_api/transaction.h" #include "sandboxed_api/vars.h" +#include "guetzli_sandbox.h" + namespace guetzli { namespace sandbox { -constexpr int kDefaultTransactionRetryCount = 0; -constexpr uint64_t kMpixPixels = 1'000'000; - enum class ImageType { - JPEG, - PNG + kJpeg, + kPng }; struct TransactionParams { @@ -55,8 +68,13 @@ class GuetzliTransaction : public sapi::Transaction { const TransactionParams params_; sapi::v::Fd in_fd_; sapi::v::Fd out_fd_; - ImageType image_type_ = ImageType::JPEG; + ImageType image_type_ = ImageType::kJpeg; + + static const int kDefaultTransactionRetryCount = 0; + static const uint64_t kMpixPixels = 1'000'000; }; -} // namespace sandbox -} // namespace guetzli +} // namespace sandbox +} // namespace guetzli + +#endif // GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel index fa8b4ae..34f3f76 100644 --- a/oss-internship-2020/guetzli/tests/BUILD.bazel +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -1,3 +1,17 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + cc_test( name = "transaction_tests", srcs = ["guetzli_transaction_test.cc"], diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc index 2de4cb3..71439d7 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -1,16 +1,32 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +#include +#include +#include +#include + #include "gtest/gtest.h" -#include "guetzli_sandbox.h" #include "sandboxed_api/sandbox2/util/fileops.h" #include "sandboxed_api/vars.h" -#include -#include -#include -#include -#include -#include -#include -#include +#include "guetzli_sandbox.h" namespace guetzli { namespace sandbox { @@ -18,23 +34,23 @@ namespace tests { namespace { -constexpr const char* IN_PNG_FILENAME = "bees.png"; -constexpr const char* IN_JPG_FILENAME = "nature.jpg"; -constexpr const char* PNG_REFERENCE_FILENAME = "bees_reference.jpg"; -constexpr const char* JPG_REFERENCE_FILENAME = "nature_reference.jpg"; +constexpr const char* kInPngFilename = "bees.png"; +constexpr const char* kInJpegFilename = "nature.jpg"; +constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; +constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; -constexpr int PNG_EXPECTED_SIZE = 38'625; -constexpr int JPG_EXPECTED_SIZE = 10'816; +constexpr int kPngExpectedSize = 38'625; +constexpr int kJpegExpectedSize = 10'816; -constexpr int DEFAULT_QUALITY_TARGET = 95; -constexpr int DEFAULT_MEMLIMIT_MB = 6000; +constexpr int kDefaultQualityTarget = 95; +constexpr int kDefaultMemlimitMb = 6000; -constexpr const char* RELATIVE_PATH_TO_TESTDATA = +constexpr const char* kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; std::string GetPathToInputFile(const char* filename) { return std::string(getenv("TEST_SRCDIR")) - + std::string(RELATIVE_PATH_TO_TESTDATA) + + std::string(kRelativePathToTestdata) + std::string(filename); } @@ -78,7 +94,7 @@ protected: // This test can take up to few minutes depending on your hardware TEST_F(GuetzliSapiTest, ProcessRGB) { - sapi::v::Fd in_fd(open(GetPathToInputFile(IN_PNG_FILENAME).c_str(), + sapi::v::Fd in_fd(open(GetPathToInputFile(kInPngFilename).c_str(), O_RDONLY)); ASSERT_TRUE(in_fd.GetValue() != -1) << "Error opening input file"; ASSERT_EQ(api_->sandbox()->TransferToSandboxee(&in_fd), absl::OkStatus()) @@ -87,17 +103,17 @@ TEST_F(GuetzliSapiTest, ProcessRGB) { sapi::v::Struct processing_params; *processing_params.mutable_data() = {in_fd.GetRemoteFd(), 0, - DEFAULT_QUALITY_TARGET, - DEFAULT_MEMLIMIT_MB + kDefaultQualityTarget, + kDefaultMemlimitMb }; sapi::v::LenVal output(0); auto processing_result = api_->ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing rgb data"; - ASSERT_EQ(output.GetDataSize(), PNG_EXPECTED_SIZE) + ASSERT_EQ(output.GetDataSize(), kPngExpectedSize) << "Incorrect result data size"; std::string reference_data = - ReadFromFile(GetPathToInputFile(PNG_REFERENCE_FILENAME)); + ReadFromFile(GetPathToInputFile(kPngReferenceFilename)); ASSERT_EQ(output.GetDataSize(), reference_data.size()) << "Incorrect result data size"; ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) @@ -106,7 +122,7 @@ TEST_F(GuetzliSapiTest, ProcessRGB) { // This test can take up to few minutes depending on your hardware TEST_F(GuetzliSapiTest, ProcessJpeg) { - sapi::v::Fd in_fd(open(GetPathToInputFile(IN_JPG_FILENAME).c_str(), + sapi::v::Fd in_fd(open(GetPathToInputFile(kInJpegFilename).c_str(), O_RDONLY)); ASSERT_TRUE(in_fd.GetValue() != -1) << "Error opening input file"; ASSERT_EQ(api_->sandbox()->TransferToSandboxee(&in_fd), absl::OkStatus()) @@ -115,27 +131,23 @@ TEST_F(GuetzliSapiTest, ProcessJpeg) { sapi::v::Struct processing_params; *processing_params.mutable_data() = {in_fd.GetRemoteFd(), 0, - DEFAULT_QUALITY_TARGET, - DEFAULT_MEMLIMIT_MB + kDefaultQualityTarget, + kDefaultMemlimitMb }; sapi::v::LenVal output(0); auto processing_result = api_->ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing jpg data"; - ASSERT_EQ(output.GetDataSize(), JPG_EXPECTED_SIZE) + ASSERT_EQ(output.GetDataSize(), kJpegExpectedSize) << "Incorrect result data size"; std::string reference_data = - ReadFromFile(GetPathToInputFile(JPG_REFERENCE_FILENAME)); + ReadFromFile(GetPathToInputFile(kJpegReferenceFIlename)); ASSERT_EQ(output.GetDataSize(), reference_data.size()) << "Incorrect result data size"; ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) << "Processed data doesn't match reference output"; } -// TEST_F(GuetzliSapiTest, WriteDataToFd) { -// sapi::v::Fd fd(open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); -// } - -} // namespace tests -} // namespace sandbox -} // namespace guetzli \ No newline at end of file +} // namespace tests +} // namespace sandbox +} // namespace guetzli diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc index b9e3a22..da76dde 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -1,14 +1,30 @@ -#include "gtest/gtest.h" -#include "guetzli_transaction.h" -#include "sandboxed_api/sandbox2/util/fileops.h" +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. #include #include #include #include + #include -#include #include +#include + +#include "gtest/gtest.h" +#include "sandboxed_api/sandbox2/util/fileops.h" + +#include "guetzli_transaction.h" namespace guetzli { namespace sandbox { @@ -16,23 +32,23 @@ namespace tests { namespace { -constexpr const char* IN_PNG_FILENAME = "bees.png"; -constexpr const char* IN_JPG_FILENAME = "nature.jpg"; -constexpr const char* PNG_REFERENCE_FILENAME = "bees_reference.jpg"; -constexpr const char* JPG_REFERENCE_FILENAME = "nature_reference.jpg"; +constexpr const char* kInPngFilename = "bees.png"; +constexpr const char* kInJpegFilename = "nature.jpg"; +constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; +constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; -constexpr int PNG_EXPECTED_SIZE = 38'625; -constexpr int JPG_EXPECTED_SIZE = 10'816; +constexpr int kPngExpectedSize = 38'625; +constexpr int kJpegExpectedSize = 10'816; -constexpr int DEFAULT_QUALITY_TARGET = 95; -constexpr int DEFAULT_MEMLIMIT_MB = 6000; +constexpr int kDefaultQualityTarget = 95; +constexpr int kDefaultMemlimitMb = 6000; -constexpr const char* RELATIVE_PATH_TO_TESTDATA = +constexpr const char* kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; std::string GetPathToInputFile(const char* filename) { return std::string(getenv("TEST_SRCDIR")) - + std::string(RELATIVE_PATH_TO_TESTDATA) + + std::string(kRelativePathToTestdata) + std::string(filename); } @@ -48,11 +64,11 @@ std::string ReadFromFile(const std::string& filename) { return result.str(); } -} // namespace +} // namespace TEST(GuetzliTransactionTest, TestTransactionJpg) { sandbox2::file_util::fileops::FDCloser in_fd_closer( - open(GetPathToInputFile(IN_JPG_FILENAME).c_str(), O_RDONLY)); + open(GetPathToInputFile(kInJpegFilename).c_str(), O_RDONLY)); ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input jpg file"; sandbox2::file_util::fileops::FDCloser out_fd_closer( open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); @@ -61,8 +77,8 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { in_fd_closer.get(), out_fd_closer.get(), 0, - DEFAULT_QUALITY_TARGET, - DEFAULT_MEMLIMIT_MB + kDefaultQualityTarget, + kDefaultMemlimitMb }; { GuetzliTransaction transaction(std::move(params)); @@ -72,7 +88,7 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { } ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) << "Local output fd closed"; - auto reference_data = ReadFromFile(GetPathToInputFile(JPG_REFERENCE_FILENAME)); + auto reference_data = ReadFromFile(GetPathToInputFile(kJpegReferenceFIlename)); auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; @@ -90,7 +106,7 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { TEST(GuetzliTransactionTest, TestTransactionPng) { sandbox2::file_util::fileops::FDCloser in_fd_closer( - open(GetPathToInputFile(IN_PNG_FILENAME).c_str(), O_RDONLY)); + open(GetPathToInputFile(kInPngFilename).c_str(), O_RDONLY)); ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input png file"; sandbox2::file_util::fileops::FDCloser out_fd_closer( open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); @@ -99,8 +115,8 @@ TEST(GuetzliTransactionTest, TestTransactionPng) { in_fd_closer.get(), out_fd_closer.get(), 0, - DEFAULT_QUALITY_TARGET, - DEFAULT_MEMLIMIT_MB + kDefaultQualityTarget, + kDefaultMemlimitMb }; { GuetzliTransaction transaction(std::move(params)); @@ -110,7 +126,7 @@ TEST(GuetzliTransactionTest, TestTransactionPng) { } ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) << "Local output fd closed"; - auto reference_data = ReadFromFile(GetPathToInputFile(PNG_REFERENCE_FILENAME)); + auto reference_data = ReadFromFile(GetPathToInputFile(kPngReferenceFilename)); auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; @@ -126,6 +142,6 @@ TEST(GuetzliTransactionTest, TestTransactionPng) { << "Returned data doesn't match refernce"; } -} // namespace tests -} // namespace sandbox -} // namespace guetzli \ No newline at end of file +} // namespace tests +} // namespace sandbox +} // namespace guetzli From b4aca053002bae11c629caa69421d6ef59a2919d Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Mon, 17 Aug 2020 13:29:07 -0700 Subject: [PATCH 06/10] Update after code review --- oss-internship-2020/guetzli/BUILD.bazel | 14 +- oss-internship-2020/guetzli/README.md | 29 + oss-internship-2020/guetzli/WORKSPACE | 47 +- .../guetzli/external/butteraugli.BUILD | 28 + .../guetzli/external/guetzli.BUILD | 18 +- .../butteraugli => external}/jpeg.BUILD | 1 + .../guetzli/guetzli_entry_points.cc | 155 +- oss-internship-2020/guetzli/guetzli_sandbox.h | 7 +- .../guetzli/guetzli_sandboxed.cc | 55 +- .../guetzli/guetzli_transaction.cc | 105 +- .../guetzli/guetzli_transaction.h | 22 +- oss-internship-2020/guetzli/tests/BUILD.bazel | 4 +- .../guetzli/tests/guetzli_sapi_test.cc | 33 +- .../guetzli/tests/guetzli_transaction_test.cc | 80 +- .../guetzli/third_party/butteraugli/BUILD | 24 - .../guetzli/third_party/butteraugli/LICENSE | 201 -- .../guetzli/third_party/butteraugli/README.md | 68 - .../guetzli/third_party/butteraugli/WORKSPACE | 25 - .../butteraugli/butteraugli/Makefile | 7 - .../butteraugli/butteraugli/butteraugli.cc | 1994 ----------------- .../butteraugli/butteraugli/butteraugli.h | 619 ----- .../butteraugli/butteraugli_main.cc | 457 ---- .../guetzli/third_party/butteraugli/png.BUILD | 33 - .../third_party/butteraugli/zlib.BUILD | 36 - 24 files changed, 324 insertions(+), 3738 deletions(-) create mode 100644 oss-internship-2020/guetzli/external/butteraugli.BUILD rename oss-internship-2020/guetzli/{third_party/butteraugli => external}/jpeg.BUILD (99%) mode change 100755 => 100644 delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/BUILD delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/LICENSE delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/README.md delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD delete mode 100755 oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel index fe5bfb8..88a6da2 100644 --- a/oss-internship-2020/guetzli/BUILD.bazel +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +licenses(["unencumbered"]) # code authored by Google + load( "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", "sapi_library", @@ -25,7 +27,7 @@ cc_library( "@guetzli//:guetzli_lib", "@com_google_sandboxed_api//sandboxed_api:lenval_core", "@com_google_sandboxed_api//sandboxed_api:vars", - "@png_archive//:png" + "@png_archive//:png", ], ) @@ -36,19 +38,19 @@ sapi_library( functions = [ "ProcessJpeg", "ProcessRgb", - "WriteDataToFd" + "WriteDataToFd", ], input_files = ["guetzli_entry_points.h"], lib = ":guetzli_wrapper", lib_name = "Guetzli", visibility = ["//visibility:public"], - namespace = "guetzli::sandbox" + namespace = "guetzli::sandbox", ) cc_binary( name="guetzli_sandboxed", srcs=["guetzli_sandboxed.cc"], deps = [ - ":guetzli_sapi" - ] -) \ No newline at end of file + ":guetzli_sapi", + ], +) diff --git a/oss-internship-2020/guetzli/README.md b/oss-internship-2020/guetzli/README.md index e69de29..042f165 100644 --- a/oss-internship-2020/guetzli/README.md +++ b/oss-internship-2020/guetzli/README.md @@ -0,0 +1,29 @@ +# Guetzli Sandboxed +This is an example implementation of a sandbox for the [Guetzli](https://github.com/google/guetzli) library using [Sandboxed API](https://github.com/google/sandboxed-api). +Please read Guetzli's [documentation](https://github.com/google/guetzli#introduction) to learn more about it. + +## Implementation details +Because Guetzli provides C++ API and SAPI requires functions to be `extern "C"` a wrapper library has been written for the compatibility. SAPI provides a Transaction class, which is a convenient way to create a wrapper for your sandboxed API that handles internal errors. Original Guetzli has command-line utility to encode images, so fully compatible utility that uses sandboxed Guetzli is provided. + +Wrapper around Guetzli uses file descriptors to pass data to the sandbox. This approach restricts the sandbox from using open() syscall and also helps to prevent making copies of data, because you need to synchronize it between processes. + +## Build Guetzli Sandboxed +Right now Sandboxed API support only Linux systems, so you need one to build it. Guetzli sandboxed uses [Bazel](https://bazel.build/) as a build system so you need to [install it](https://docs.bazel.build/versions/3.4.0/install.html) before building. + +To build Guetzli sandboxed encoding utility you can use this command: +`bazel build //:guetzli_sandboxed` + +Than you can use it in this way: +``` +guetzli_sandboxed [--quality Q] [--verbose] original.png output.jpg +guetzli_sandboxed [--quality Q] [--verbose] original.jpg output.jpg +``` +Refer to Guetzli's [documentation](https://github.com/google/guetzli#using) to read more about usage. + +## Examples +There are two different sets of unit tests which demonstrate how to use different parts of Guetzli sandboxed: +* `tests/guetzli_sapi_test.cc` - example usage of Guetzli sandboxed API. +* `tests/guetzli_transaction_test.cc` - example usage of Guetzli transaction. + +Also, there is an example of custom security policy for your sandbox in +`guetzli_sandbox.h` diff --git a/oss-internship-2020/guetzli/WORKSPACE b/oss-internship-2020/guetzli/WORKSPACE index f7ec64c..7f2b667 100644 --- a/oss-internship-2020/guetzli/WORKSPACE +++ b/oss-internship-2020/guetzli/WORKSPACE @@ -47,38 +47,49 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") protobuf_deps() -local_repository( - name = "butteraugli", - path = "third_party/butteraugli/" -) - http_archive( name = "guetzli", build_file = "guetzli.BUILD", sha256 = "39632357e49db83d9560bf0de560ad833352f36d23b109b0e995b01a37bddb57", strip_prefix = "guetzli-master", - url = "https://github.com/google/guetzli/archive/master.zip" + url = "https://github.com/google/guetzli/archive/master.zip", ) http_archive( - name = "png_archive", - build_file = "png.BUILD", - sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", - strip_prefix = "libpng-1.2.57", - url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", + name = "butteraugli", + build_file = "butteraugli.BUILD", + sha256 = "39632357e49db83d9560bf0de560ad833352f36d23b109b0e995b01a37bddb57", + strip_prefix = "guetzli-master/third_party/butteraugli", + url = "https://github.com/google/guetzli/archive/master.zip", ) http_archive( - name = "zlib_archive", - build_file = "zlib.BUILD", - sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", - strip_prefix = "zlib-1.2.10", - url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", + name = "png_archive", + build_file = "png.BUILD", + sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", + strip_prefix = "libpng-1.2.57", + url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", +) + +http_archive( + name = "zlib_archive", + build_file = "zlib.BUILD", + sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", + strip_prefix = "zlib-1.2.10", + url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", +) + +http_archive( + name = "jpeg_archive", + url = "http://www.ijg.org/files/jpegsrc.v9b.tar.gz", + sha256 = "240fd398da741669bf3c90366f58452ea59041cacc741a489b99f2f6a0bad052", + strip_prefix = "jpeg-9b", + build_file = "jpeg.BUILD", ) maybe( git_repository, name = "googletest", remote = "https://github.com/google/googletest", - tag = "release-1.10.0" -) \ No newline at end of file + tag = "release-1.10.0", +) diff --git a/oss-internship-2020/guetzli/external/butteraugli.BUILD b/oss-internship-2020/guetzli/external/butteraugli.BUILD new file mode 100644 index 0000000..9058593 --- /dev/null +++ b/oss-internship-2020/guetzli/external/butteraugli.BUILD @@ -0,0 +1,28 @@ +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["unencumbered"]) # code authored by Google + +cc_library( + name = "butteraugli_lib", + srcs = [ + "butteraugli/butteraugli.cc", + "butteraugli/butteraugli.h", + ], + hdrs = [ + "butteraugli/butteraugli.h", + ], + copts = ["-Wno-sign-compare"], + visibility = ["//visibility:public"], +) diff --git a/oss-internship-2020/guetzli/external/guetzli.BUILD b/oss-internship-2020/guetzli/external/guetzli.BUILD index dec29a1..d2d4f32 100644 --- a/oss-internship-2020/guetzli/external/guetzli.BUILD +++ b/oss-internship-2020/guetzli/external/guetzli.BUILD @@ -1,4 +1,18 @@ -package(default_visibility = ["//visibility:public"]) +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +licenses(["unencumbered"]) # code authored by Google cc_library( name = "guetzli_lib", @@ -15,4 +29,4 @@ cc_library( deps = [ "@butteraugli//:butteraugli_lib", ], -) \ No newline at end of file +) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD b/oss-internship-2020/guetzli/external/jpeg.BUILD old mode 100755 new mode 100644 similarity index 99% rename from oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD rename to oss-internship-2020/guetzli/external/jpeg.BUILD index 92c9ddc..71eb87b --- a/oss-internship-2020/guetzli/third_party/butteraugli/jpeg.BUILD +++ b/oss-internship-2020/guetzli/external/jpeg.BUILD @@ -1,3 +1,4 @@ + # Description: # The Independent JPEG Group's JPEG runtime library. diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc index ec0743b..a36431b 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.cc +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "guetzli_entry_points.h" + #include #include @@ -22,7 +24,6 @@ #include "guetzli/jpeg_data_reader.h" #include "guetzli/quality.h" -#include "guetzli_entry_points.h" #include "png.h" #include "sandboxed_api/sandbox2/util/fileops.h" #include "sandboxed_api/util/statusor.h" @@ -38,19 +39,21 @@ struct GuetzliInitData { guetzli::ProcessStats stats; }; -template -void CopyMemoryToLenVal(const T* data, size_t size, - sapi::LenValStruct* out_data) { - free(out_data->data); // Not sure about this - out_data->size = size; - T* new_out = static_cast(malloc(size)); - memcpy(new_out, data, size); - out_data->data = new_out; +struct ImageData { + int xsize; + int ysize; + std::vector rgb; +}; + +sapi::LenValStruct CreateLenValFromData(const void* data, size_t size) { + void* new_data = malloc(size); + memcpy(new_data, data, size); + return { size, new_data }; } sapi::StatusOr ReadFromFd(int fd) { struct stat file_data; - auto status = fstat(fd, &file_data); + int status = fstat(fd, &file_data); if (status < 0) { return absl::FailedPreconditionError( @@ -58,10 +61,9 @@ sapi::StatusOr ReadFromFd(int fd) { ); } - auto fsize = file_data.st_size; - - std::unique_ptr buf(new char[fsize]); - status = read(fd, buf.get(), fsize); + std::string result; + result.resize(file_data.st_size); + status = read(fd, result.data(), result.size()); if (status < 0) { return absl::FailedPreconditionError( @@ -69,15 +71,15 @@ sapi::StatusOr ReadFromFd(int fd) { ); } - return std::string(buf.get(), fsize); + return result; } sapi::StatusOr PrepareDataForProcessing( const ProcessingParams* processing_params) { - auto input_status = ReadFromFd(processing_params->remote_fd); + sapi::StatusOr input = ReadFromFd(processing_params->remote_fd); - if (!input_status.ok()) { - return input_status.status(); + if (!input.ok()) { + return input.status(); } guetzli::Params guetzli_params; @@ -91,7 +93,7 @@ sapi::StatusOr PrepareDataForProcessing( } return GuetzliInitData{ - std::move(input_status.value()), + std::move(input.value()), guetzli_params, stats }; @@ -101,29 +103,36 @@ inline uint8_t BlendOnBlack(const uint8_t val, const uint8_t alpha) { return (static_cast(val) * static_cast(alpha) + 128) / 255; } -bool ReadPNG(const std::string& data, int* xsize, int* ysize, - std::vector* rgb) { +// Modified version of ReadPNG from original guetzli.cc +sapi::StatusOr ReadPNG(const std::string& data) { + std::vector rgb; + int xsize, ysize; png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) { - return false; + return absl::FailedPreconditionError( + "Error reading PNG data from input file"); } png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, nullptr, nullptr); - return false; + return absl::FailedPreconditionError( + "Error reading PNG data from input file"); } if (setjmp(png_jmpbuf(png_ptr)) != 0) { // Ok we are here because of the setjmp. png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - return false; + return absl::FailedPreconditionError( + "Error reading PNG data from input file"); } std::istringstream memstream(data, std::ios::in | std::ios::binary); - png_set_read_fn(png_ptr, static_cast(&memstream), [](png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead) { - std::istringstream& memstream = *static_cast(png_get_io_ptr(png_ptr)); + png_set_read_fn(png_ptr, static_cast(&memstream), + [](png_structp png_ptr, png_bytep outBytes, png_size_t byteCountToRead) { + std::istringstream& memstream = + *static_cast(png_get_io_ptr(png_ptr)); memstream.read(reinterpret_cast(outBytes), byteCountToRead); @@ -143,18 +152,18 @@ bool ReadPNG(const std::string& data, int* xsize, int* ysize, png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); - *xsize = png_get_image_width(png_ptr, info_ptr); - *ysize = png_get_image_height(png_ptr, info_ptr); - rgb->resize(3 * (*xsize) * (*ysize)); + xsize = png_get_image_width(png_ptr, info_ptr); + ysize = png_get_image_height(png_ptr, info_ptr); + rgb.resize(3 * (xsize) * (ysize)); const int components = png_get_channels(png_ptr, info_ptr); switch (components) { case 1: { // GRAYSCALE - for (int y = 0; y < *ysize; ++y) { + for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; - for (int x = 0; x < *xsize; ++x) { + uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + for (int x = 0; x < xsize; ++x) { const uint8_t gray = row_in[x]; row_out[3 * x + 0] = gray; row_out[3 * x + 1] = gray; @@ -165,10 +174,10 @@ bool ReadPNG(const std::string& data, int* xsize, int* ysize, } case 2: { // GRAYSCALE + ALPHA - for (int y = 0; y < *ysize; ++y) { + for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; - for (int x = 0; x < *xsize; ++x) { + uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + for (int x = 0; x < xsize; ++x) { const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); row_out[3 * x + 0] = gray; row_out[3 * x + 1] = gray; @@ -179,19 +188,19 @@ bool ReadPNG(const std::string& data, int* xsize, int* ysize, } case 3: { // RGB - for (int y = 0; y < *ysize; ++y) { + for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; - memcpy(row_out, row_in, 3 * (*xsize)); + uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + memcpy(row_out, row_in, 3 * (xsize)); } break; } case 4: { // RGBA - for (int y = 0; y < *ysize; ++y) { + for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(*rgb)[3 * y * (*xsize)]; - for (int x = 0; x < *xsize; ++x) { + uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + for (int x = 0; x < xsize; ++x) { const uint8_t alpha = row_in[4 * x + 3]; row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); row_out[3 * x + 1] = BlendOnBlack(row_in[4 * x + 1], alpha); @@ -202,10 +211,16 @@ bool ReadPNG(const std::string& data, int* xsize, int* ysize, } default: png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - return false; + return absl::FailedPreconditionError( + "Error reading PNG data from input file"); } png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); - return true; + + return ImageData{ + xsize, + ysize, + std::move(rgb) + }; } bool CheckMemoryLimitExceeded(int memlimit_mb, int xsize, int ysize) { @@ -219,70 +234,72 @@ bool CheckMemoryLimitExceeded(int memlimit_mb, int xsize, int ysize) { extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, sapi::LenValStruct* output) { - auto processing_data_status = PrepareDataForProcessing(processing_params); + auto processing_data = PrepareDataForProcessing(processing_params); - if (!processing_data_status.status().ok()) { - fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str()); + if (!processing_data.ok()) { + std::cerr << processing_data.status().ToString() << std::endl; return false; } guetzli::JPEGData jpg_header; - if (!guetzli::ReadJpeg(processing_data_status.value().in_data, + if (!guetzli::ReadJpeg(processing_data->in_data, guetzli::JPEG_READ_HEADER, &jpg_header)) { - fprintf(stderr, "Error reading JPG data from input file\n"); + std::cerr << "Error reading JPG data from input file" << std::endl; return false; } if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, jpg_header.width, jpg_header.height)) { - fprintf(stderr, "Memory limit would be exceeded.\n"); + std::cerr << "Memory limit would be exceeded" << std::endl; return false; } std::string out_data; - if (!guetzli::Process(processing_data_status.value().params, - &processing_data_status.value().stats, - processing_data_status.value().in_data, + if (!guetzli::Process(processing_data->params, + &processing_data->stats, + processing_data->in_data, &out_data)) { - fprintf(stderr, "Guezli processing failed\n"); + std::cerr << "Guezli processing failed" << std::endl; return false; } - CopyMemoryToLenVal(out_data.data(), out_data.size(), output); + *output = CreateLenValFromData(out_data.data(), out_data.size()); return true; } extern "C" bool ProcessRgb(const ProcessingParams* processing_params, sapi::LenValStruct* output) { - auto processing_data_status = PrepareDataForProcessing(processing_params); + auto processing_data = PrepareDataForProcessing(processing_params); - if (!processing_data_status.status().ok()) { - fprintf(stderr, "%s\n", processing_data_status.status().ToString().c_str()); + if (!processing_data.ok()) { + std::cerr << processing_data.status().ToString() << std::endl; return false; } - int xsize, ysize; - std::vector rgb; - - if (!ReadPNG(processing_data_status.value().in_data, &xsize, &ysize, &rgb)) { - fprintf(stderr, "Error reading PNG data from input file\n"); + auto png_data = ReadPNG(processing_data->in_data); + if (!png_data.ok()) { + std::cerr << "Error reading PNG data from input file" << std::endl; return false; } - if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, xsize, ysize)) { - fprintf(stderr, "Memory limit would be exceeded.\n"); + if (CheckMemoryLimitExceeded(processing_params->memlimit_mb, + png_data->xsize, png_data->ysize)) { + std::cerr << "Memory limit would be exceeded" << std::endl; return false; } std::string out_data; - if (!guetzli::Process(processing_data_status.value().params, - &processing_data_status.value().stats, - rgb, xsize, ysize, &out_data)) { - fprintf(stderr, "Guetzli processing failed\n"); + if (!guetzli::Process(processing_data->params, + &processing_data->stats, + png_data->rgb, + png_data->xsize, + png_data->ysize, + &out_data)) { + std::cerr << "Guetzli processing failed" << std::endl; return false; } - CopyMemoryToLenVal(out_data.data(), out_data.size(), output); + *output = CreateLenValFromData(out_data.data(), out_data.size()); return true; } diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h index e55875a..d1da1f8 100644 --- a/oss-internship-2020/guetzli/guetzli_sandbox.h +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -15,13 +15,8 @@ #ifndef GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ #define GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ -#include #include -#include "sandboxed_api/sandbox2/policy.h" -#include "sandboxed_api/sandbox2/policybuilder.h" -#include "sandboxed_api/util/flag.h" - #include "guetzli_sapi.sapi.h" namespace guetzli { @@ -42,7 +37,7 @@ class GuetzliSapiSandbox : public GuetzliSandbox { .AllowSyscalls({ __NR_futex, __NR_close, - __NR_recvmsg // Seems like this one needed to work with remote file descriptors + __NR_recvmsg // To work with remote fd }) .BuildOrDie(); } diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc index cb6543c..6628f3e 100644 --- a/oss-internship-2020/guetzli/guetzli_sandboxed.cc +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -30,12 +30,6 @@ namespace { constexpr int kDefaultJPEGQuality = 95; constexpr int kDefaultMemlimitMB = 6000; -void TerminateHandler() { - fprintf(stderr, "Unhandled exception. Most likely insufficient memory available.\n" - "Make sure that there is 300MB/MPix of memory available.\n"); - exit(1); -} - void Usage() { fprintf(stderr, "Guetzli JPEG compressor. Usage: \n" @@ -54,8 +48,6 @@ void Usage() { } // namespace int main(int argc, const char** argv) { - std::set_terminate(TerminateHandler); - int verbose = 0; int quality = kDefaultJPEGQuality; int memlimit_mb = kDefaultMemlimitMB; @@ -92,25 +84,9 @@ int main(int argc, const char** argv) { Usage(); } - sandbox2::file_util::fileops::FDCloser in_fd_closer( - open(argv[opt_idx], O_RDONLY)); - - if (in_fd_closer.get() < 0) { - fprintf(stderr, "Can't open input file: %s\n", argv[opt_idx]); - return 1; - } - - sandbox2::file_util::fileops::FDCloser out_fd_closer( - open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); - - if (out_fd_closer.get() < 0) { - fprintf(stderr, "Can't create temporary output file: %s\n", argv[opt_idx]); - return 1; - } - guetzli::sandbox::TransactionParams params = { - in_fd_closer.get(), - out_fd_closer.get(), + argv[opt_idx], + argv[opt_idx + 1], verbose, quality, memlimit_mb @@ -119,29 +95,10 @@ int main(int argc, const char** argv) { guetzli::sandbox::GuetzliTransaction transaction(std::move(params)); auto result = transaction.Run(); - if (result.ok()) { - if (access(argv[opt_idx + 1], F_OK) != -1) { - if (remove(argv[opt_idx + 1]) < 0) { - fprintf(stderr, "Error deleting existing output file: %s\n", - argv[opt_idx + 1]); - return 1; - } - } - - std::stringstream path; - path << "/proc/self/fd/" << out_fd_closer.get(); - - if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, argv[opt_idx + 1], - AT_SYMLINK_FOLLOW) < 0) { - fprintf(stderr, "Error linking %s\n", - argv[opt_idx + 1]); - return 1; - } - } - else { - fprintf(stderr, "%s\n", result.ToString().c_str()); // Use cerr instead ? - return 1; + if (!result.ok()) { + std::cerr << result.ToString() << std::endl; + return EXIT_FAILURE; } - return 0; + return EXIT_SUCCESS; } diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc index 2613bdf..64fac51 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -14,73 +14,49 @@ #include "guetzli_transaction.h" +#include +#include +#include +#include + #include #include namespace guetzli { namespace sandbox { -absl::Status GuetzliTransaction::Init() { - // Close remote fd if transaction is repeated - if (in_fd_.GetRemoteFd() != -1) { - SAPI_RETURN_IF_ERROR(in_fd_.CloseRemoteFd(sandbox()->GetRpcChannel())); - } - if (out_fd_.GetRemoteFd() != -1) { - SAPI_RETURN_IF_ERROR(out_fd_.CloseRemoteFd(sandbox()->GetRpcChannel())); +absl::Status GuetzliTransaction::Main() { + sapi::v::Fd in_fd(open(params_.in_file, O_RDONLY)); + + if (in_fd.GetValue() < 0) { + return absl::FailedPreconditionError( + "Error opening input file" + ); } - // Reposition back to the beginning of file - if (lseek(in_fd_.GetValue(), 0, SEEK_CUR) != 0) { - if (lseek(in_fd_.GetValue(), 0, SEEK_SET) != 0) { - return absl::FailedPreconditionError( - "Error returnig cursor to the beginning" - ); - } - } + SAPI_ASSIGN_OR_RETURN(image_type_, GetImageTypeFromFd(in_fd.GetValue())); + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd)); - // Choosing between jpg and png modes - sapi::StatusOr image_type = GetImageTypeFromFd(in_fd_.GetValue()); - - if (!image_type.ok()) { - return image_type.status(); - } - - image_type_ = image_type.value(); - - SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&in_fd_)); - SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd_)); - - if (in_fd_.GetRemoteFd() < 0) { + if (in_fd.GetRemoteFd() < 0) { return absl::FailedPreconditionError( "Error receiving remote FD: remote input fd is set to -1"); } - if (out_fd_.GetRemoteFd() < 0) { - return absl::FailedPreconditionError( - "Error receiving remote FD: remote output fd is set to -1"); - } - in_fd_.OwnLocalFd(false); // FDCloser will close local fd - out_fd_.OwnLocalFd(false); // FDCloser will close local fd - - return absl::OkStatus(); -} - -absl::Status GuetzliTransaction::Main() { GuetzliApi api(sandbox()); sapi::v::LenVal output(0); sapi::v::Struct processing_params; - *processing_params.mutable_data() = {in_fd_.GetRemoteFd(), + *processing_params.mutable_data() = {in_fd.GetRemoteFd(), params_.verbose, params_.quality, params_.memlimit_mb }; - auto result_status = image_type_ == ImageType::kJpeg ? + auto result = image_type_ == ImageType::kJpeg ? api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()) : api.ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); - if (!result_status.value_or(false)) { + if (!result.value_or(false)) { std::stringstream error_stream; error_stream << "Error processing " << (image_type_ == ImageType::kJpeg ? "jpeg" : "rgb") << " data" @@ -91,7 +67,22 @@ absl::Status GuetzliTransaction::Main() { ); } - auto write_result = api.WriteDataToFd(out_fd_.GetRemoteFd(), + sapi::v::Fd out_fd(open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); + + if (out_fd.GetValue() < 0) { + return absl::FailedPreconditionError( + "Error creating temp output file" + ); + } + + SAPI_RETURN_IF_ERROR(sandbox()->TransferToSandboxee(&out_fd)); + + if (out_fd.GetRemoteFd() < 0) { + return absl::FailedPreconditionError( + "Error receiving remote FD: remote output fd is set to -1"); + } + + auto write_result = api.WriteDataToFd(out_fd.GetRemoteFd(), output.PtrBefore()); if (!write_result.value_or(false)) { @@ -100,21 +91,39 @@ absl::Status GuetzliTransaction::Main() { ); } + SAPI_RETURN_IF_ERROR(LinkOutFile(out_fd.GetValue())); + return absl::OkStatus(); } -time_t GuetzliTransaction::CalculateTimeLimitFromImageSize( - uint64_t pixels) const { - return (pixels / kMpixPixels + 5) * 60; +absl::Status GuetzliTransaction::LinkOutFile(int out_fd) const { + if (access(params_.out_file, F_OK) != -1) { + if (remove(params_.out_file) < 0) { + std::stringstream error; + error << "Error deleting existing output file: " << params_.out_file; + return absl::FailedPreconditionError(error.str()); + } + } + + std::stringstream path; + path << "/proc/self/fd/" << out_fd; + if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, params_.out_file, + AT_SYMLINK_FOLLOW) < 0) { + std::stringstream error; + error << "Error linking: " << params_.out_file; + return absl::FailedPreconditionError(error.str()); + } + + return absl::OkStatus(); } sapi::StatusOr GuetzliTransaction::GetImageTypeFromFd(int fd) const { static const unsigned char kPNGMagicBytes[] = { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n', }; - char read_buf[8]; + char read_buf[sizeof(kPNGMagicBytes)]; - if (read(fd, read_buf, 8) != 8) { + if (read(fd, read_buf, sizeof(kPNGMagicBytes)) != sizeof(kPNGMagicBytes)) { return absl::FailedPreconditionError( "Error determining type of the input file" ); diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index 2db681d..be66633 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -15,7 +15,6 @@ #ifndef GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ #define GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ -#include #include #include "sandboxed_api/transaction.h" @@ -32,8 +31,8 @@ enum class ImageType { }; struct TransactionParams { - int in_fd = -1; - int out_fd = -1; + const char* in_file = nullptr; + const char* out_file = nullptr; int verbose = 0; int quality = 0; int memlimit_mb = 0; @@ -43,35 +42,26 @@ struct TransactionParams { // Create a new one for each processing operation class GuetzliTransaction : public sapi::Transaction { public: - GuetzliTransaction(TransactionParams&& params) + GuetzliTransaction(TransactionParams params) : sapi::Transaction(std::make_unique()) , params_(std::move(params)) - , in_fd_(params_.in_fd) - , out_fd_(params_.out_fd) { //TODO: Add retry count as a parameter sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount); - //TODO: Try to use sandbox().set_wall_limit instead of infinite time limit - sapi::Transaction::SetTimeLimit(0); + sapi::Transaction::SetTimeLimit(0); // Infinite time limit } private: - absl::Status Init() override; + //absl::Status Init() override; absl::Status Main() final; + absl::Status LinkOutFile(int out_fd) const; sapi::StatusOr GetImageTypeFromFd(int fd) const; - // As guetzli takes roughly 1 minute of CPU per 1 MPix we need to calculate - // approximate time for transaction to complete - time_t CalculateTimeLimitFromImageSize(uint64_t pixels) const; - const TransactionParams params_; - sapi::v::Fd in_fd_; - sapi::v::Fd out_fd_; ImageType image_type_ = ImageType::kJpeg; static const int kDefaultTransactionRetryCount = 0; - static const uint64_t kMpixPixels = 1'000'000; }; } // namespace sandbox diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel index 34f3f76..a59237e 100644 --- a/oss-internship-2020/guetzli/tests/BUILD.bazel +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +licenses(["unencumbered"]) # code authored by Google + cc_test( name = "transaction_tests", srcs = ["guetzli_transaction_test.cc"], @@ -34,4 +36,4 @@ cc_test( ], size = "large", data = glob(["testdata/*"]) -) \ No newline at end of file +) diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc index 71439d7..66f2ef6 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -39,9 +39,6 @@ constexpr const char* kInJpegFilename = "nature.jpg"; constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; -constexpr int kPngExpectedSize = 38'625; -constexpr int kJpegExpectedSize = 10'816; - constexpr int kDefaultQualityTarget = 95; constexpr int kDefaultMemlimitMb = 6000; @@ -49,9 +46,7 @@ constexpr const char* kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; std::string GetPathToInputFile(const char* filename) { - return std::string(getenv("TEST_SRCDIR")) - + std::string(kRelativePathToTestdata) - + std::string(filename); + return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); } std::string ReadFromFile(const std::string& filename) { @@ -66,18 +61,6 @@ std::string ReadFromFile(const std::string& filename) { return result.str(); } -template -bool CompareBytesInLenValAndContainer(const sapi::v::LenVal& lenval, - const Container& container) { - return std::equal( - lenval.GetData(), lenval.GetData() + lenval.GetDataSize(), - container.begin(), - [](const uint8_t lhs, const auto rhs) { - return lhs == static_cast(rhs); - } - ); -} - } // namespace class GuetzliSapiTest : public ::testing::Test { @@ -110,14 +93,13 @@ TEST_F(GuetzliSapiTest, ProcessRGB) { auto processing_result = api_->ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing rgb data"; - ASSERT_EQ(output.GetDataSize(), kPngExpectedSize) - << "Incorrect result data size"; std::string reference_data = ReadFromFile(GetPathToInputFile(kPngReferenceFilename)); ASSERT_EQ(output.GetDataSize(), reference_data.size()) << "Incorrect result data size"; - ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) - << "Processed data doesn't match reference output"; + ASSERT_EQ(std::string(output.GetData(), + output.GetData() + output.GetDataSize()), reference_data) + << "Processed data doesn't match reference output"; } // This test can take up to few minutes depending on your hardware @@ -138,14 +120,13 @@ TEST_F(GuetzliSapiTest, ProcessJpeg) { auto processing_result = api_->ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing jpg data"; - ASSERT_EQ(output.GetDataSize(), kJpegExpectedSize) - << "Incorrect result data size"; std::string reference_data = ReadFromFile(GetPathToInputFile(kJpegReferenceFIlename)); ASSERT_EQ(output.GetDataSize(), reference_data.size()) << "Incorrect result data size"; - ASSERT_TRUE(CompareBytesInLenValAndContainer(output, reference_data)) - << "Processed data doesn't match reference output"; + ASSERT_EQ(std::string(output.GetData(), + output.GetData() + output.GetDataSize()), reference_data) + << "Processed data doesn't match reference output"; } } // namespace tests diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc index da76dde..4b33b61 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -34,6 +34,8 @@ namespace { constexpr const char* kInPngFilename = "bees.png"; constexpr const char* kInJpegFilename = "nature.jpg"; +constexpr const char* kOutJpegFilename = "out_jpeg.jpg"; +constexpr const char* kOutPngFilename = "out_png.png"; constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; @@ -46,10 +48,8 @@ constexpr int kDefaultMemlimitMb = 6000; constexpr const char* kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; -std::string GetPathToInputFile(const char* filename) { - return std::string(getenv("TEST_SRCDIR")) - + std::string(kRelativePathToTestdata) - + std::string(filename); +std::string GetPathToFile(const char* filename) { + return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); } std::string ReadFromFile(const std::string& filename) { @@ -64,18 +64,35 @@ std::string ReadFromFile(const std::string& filename) { return result.str(); } +// Helper class to delete file after opening +class FileRemover { + public: + explicit FileRemover(const char* path) + : path_(path) + , fd_(open(path, O_RDONLY)) + {} + + ~FileRemover() { + close(fd_); + remove(path_); + } + + int get() const { return fd_; } + + private: + const char* path_; + int fd_; +}; + } // namespace TEST(GuetzliTransactionTest, TestTransactionJpg) { - sandbox2::file_util::fileops::FDCloser in_fd_closer( - open(GetPathToInputFile(kInJpegFilename).c_str(), O_RDONLY)); - ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input jpg file"; - sandbox2::file_util::fileops::FDCloser out_fd_closer( - open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); - ASSERT_TRUE(out_fd_closer.get() != -1) << "Error creating temp output file"; + std::string in_path = GetPathToFile(kInJpegFilename); + std::string out_path = GetPathToFile(kOutJpegFilename); + TransactionParams params = { - in_fd_closer.get(), - out_fd_closer.get(), + in_path.c_str(), + out_path.c_str(), 0, kDefaultQualityTarget, kDefaultMemlimitMb @@ -86,17 +103,17 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { ASSERT_TRUE(result.ok()) << result.ToString(); } - ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) - << "Local output fd closed"; - auto reference_data = ReadFromFile(GetPathToInputFile(kJpegReferenceFIlename)); - auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); + auto reference_data = ReadFromFile(GetPathToFile(kJpegReferenceFIlename)); + FileRemover file_remover(out_path.c_str()); + ASSERT_TRUE(file_remover.get() != -1) << "Error opening output file"; + auto output_size = lseek(file_remover.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; - ASSERT_EQ(lseek(out_fd_closer.get(), 0, SEEK_SET), 0) + ASSERT_EQ(lseek(file_remover.get(), 0, SEEK_SET), 0) << "Error repositioning out file"; std::unique_ptr buf(new char[output_size]); - auto status = read(out_fd_closer.get(), buf.get(), output_size); + auto status = read(file_remover.get(), buf.get(), output_size); ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; ASSERT_TRUE( @@ -105,16 +122,13 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { } TEST(GuetzliTransactionTest, TestTransactionPng) { - sandbox2::file_util::fileops::FDCloser in_fd_closer( - open(GetPathToInputFile(kInPngFilename).c_str(), O_RDONLY)); - ASSERT_TRUE(in_fd_closer.get() != -1) << "Error opening input png file"; - sandbox2::file_util::fileops::FDCloser out_fd_closer( - open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); - ASSERT_TRUE(out_fd_closer.get() != -1) << "Error creating temp output file"; + std::string in_path = GetPathToFile(kInPngFilename); + std::string out_path = GetPathToFile(kOutPngFilename); + TransactionParams params = { - in_fd_closer.get(), - out_fd_closer.get(), - 0, + in_path.c_str(), + out_path.c_str(), + 0, kDefaultQualityTarget, kDefaultMemlimitMb }; @@ -124,17 +138,17 @@ TEST(GuetzliTransactionTest, TestTransactionPng) { ASSERT_TRUE(result.ok()) << result.ToString(); } - ASSERT_TRUE(fcntl(out_fd_closer.get(), F_GETFD) != -1 || errno != EBADF) - << "Local output fd closed"; - auto reference_data = ReadFromFile(GetPathToInputFile(kPngReferenceFilename)); - auto output_size = lseek(out_fd_closer.get(), 0, SEEK_END); + auto reference_data = ReadFromFile(GetPathToFile(kPngReferenceFilename)); + FileRemover file_remover(out_path.c_str()); + ASSERT_TRUE(file_remover.get() != -1) << "Error opening output file"; + auto output_size = lseek(file_remover.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; - ASSERT_EQ(lseek(out_fd_closer.get(), 0, SEEK_SET), 0) + ASSERT_EQ(lseek(file_remover.get(), 0, SEEK_SET), 0) << "Error repositioning out file"; std::unique_ptr buf(new char[output_size]); - auto status = read(out_fd_closer.get(), buf.get(), output_size); + auto status = read(file_remover.get(), buf.get(), output_size); ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; ASSERT_TRUE( diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/BUILD deleted file mode 100755 index f553c0b..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -cc_library( - name = "butteraugli_lib", - srcs = [ - "butteraugli/butteraugli.cc", - "butteraugli/butteraugli.h", - ], - hdrs = [ - "butteraugli/butteraugli.h", - ], - copts = ["-Wno-sign-compare"], - visibility = ["//visibility:public"], -) - -cc_binary( - name = "butteraugli", - srcs = ["butteraugli/butteraugli_main.cc"], - copts = ["-Wno-sign-compare"], - visibility = ["//visibility:public"], - deps = [ - ":butteraugli_lib", - "@jpeg_archive//:jpeg", - "@png_archive//:png", - ], -) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE b/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE deleted file mode 100755 index 261eeb9..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/README.md b/oss-internship-2020/guetzli/third_party/butteraugli/README.md deleted file mode 100755 index 4623442..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# butteraugli - -> A tool for measuring perceived differences between images - -## Introduction - -Butteraugli is a project that estimates the psychovisual similarity of two -images. It gives a score for the images that is reliable in the domain of barely -noticeable differences. Butteraugli not only gives a scalar score, but also -computes a spatial map of the level of differences. - -One of the main motivations for this project is the statistical differences in -location and density of different color receptors, particularly the low density -of blue cones in the fovea. Another motivation comes from more accurate modeling -of ganglion cells, particularly the frequency space inhibition. - -## Use - -Butteraugli can work as a quality metric for lossy image and video compression. -On our small test corpus butteraugli performs better than our implementations of -the reference methods, psnrhsv-m, ssim, and our yuv-color-space variant of ssim. -One possible use is to define the quality level setting used in a jpeg -compressor, or to compare two or more compression methods at the same level of -psychovisual differences. - -Butteraugli is intended to be a research tool more than a practical tool for -choosing compression formats. We don't know how well butteraugli performs with -major deformations -- we have mostly tuned it within a small range of quality, -roughly corresponding to jpeg qualities 90 to 95. - -## Interface - -Only a C++ interface is provided. The interface takes two images and outputs a -map together with a scalar value defining the difference. The scalar value can -be compared to two reference values that divide the value space into three -experience classes: 'great', 'acceptable' and 'not acceptable'. - -## Build instructions - -Install [Bazel](http://bazel.build) by following the -[instructions](https://www.bazel.build/docs/install.html). Run `bazel build -c opt -//:butteraugli` in the directory that contains this README file to build the -[command-line utility](#cmdline-tool). If you want to use Butteraugli as a -library, depend on the `//:butteraugli_lib` target. - -Alternatively, you can use the Makefile provided in the `butteraugli` directory, -after ensuring that [libpng](http://www.libpng.org/) and -[libjpeg](http://ijg.org/) are installed. On some systems you might need to also -install corresponding `-dev` packages. - -The code is portable and also compiles on Windows after defining -`_CRT_SECURE_NO_WARNINGS` in the project settings. - -## Command-line utility {#cmdline-tool} - -Butteraugli, apart from the library, comes bundled with a comparison tool. The -comparison tool supports PNG and JPG images as inputs. To compare images, run: - -``` -butteraugli image1.{png|jpg} image2.{png|jpg} -``` - -The tool can also produce a heatmap of differences between images. The heatmap -will be output as a PNM image. To produce one, run: - -``` -butteraugli image1.{png|jpg} image2.{png|jpg} heatmap.pnm -``` diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE b/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE deleted file mode 100755 index 4d6ed65..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/WORKSPACE +++ /dev/null @@ -1,25 +0,0 @@ -workspace(name = "butteraugli") - -new_http_archive( - name = "png_archive", - url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", - sha256 = "a941dc09ca00148fe7aaf4ecdd6a67579c293678ed1e1cf633b5ffc02f4f8cf7", - strip_prefix = "libpng-1.2.57", - build_file = "png.BUILD", -) - -new_http_archive( - name = "zlib_archive", - url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", - sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", - strip_prefix = "zlib-1.2.10", - build_file = "zlib.BUILD", -) - -new_http_archive( - name = "jpeg_archive", - url = "http://www.ijg.org/files/jpegsrc.v9b.tar.gz", - sha256 = "240fd398da741669bf3c90366f58452ea59041cacc741a489b99f2f6a0bad052", - strip_prefix = "jpeg-9b", - build_file = "jpeg.BUILD", -) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile deleted file mode 100755 index 76b3a9b..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -LDLIBS += -lpng -ljpeg -CXXFLAGS += -std=c++11 -I.. -LINK.o = $(LINK.cc) - -all: butteraugli.o butteraugli_main.o butteraugli - -butteraugli: butteraugli.o butteraugli_main.o diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc deleted file mode 100755 index 77c91cc..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.cc +++ /dev/null @@ -1,1994 +0,0 @@ -// Copyright 2016 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) -// -// The physical architecture of butteraugli is based on the following naming -// convention: -// * Opsin - dynamics of the photosensitive chemicals in the retina -// with their immediate electrical processing -// * Xyb - hybrid opponent/trichromatic color space -// x is roughly red-subtract-green. -// y is yellow. -// b is blue. -// Xyb values are computed from Opsin mixing, not directly from rgb. -// * Mask - for visual masking -// * Hf - color modeling for spatially high-frequency features -// * Lf - color modeling for spatially low-frequency features -// * Diffmap - to cluster and build an image of error between the images -// * Blur - to hold the smoothing code - -#include "butteraugli/butteraugli.h" - -#include -#include -#include -#include -#include - -#include -#include - - -// Restricted pointers speed up Convolution(); MSVC uses a different keyword. -#ifdef _MSC_VER -#define __restrict__ __restrict -#endif - -#ifndef PROFILER_ENABLED -#define PROFILER_ENABLED 0 -#endif -#if PROFILER_ENABLED -#else -#define PROFILER_FUNC -#define PROFILER_ZONE(name) -#endif - -namespace butteraugli { - -void *CacheAligned::Allocate(const size_t bytes) { - char *const allocated = static_cast(malloc(bytes + kCacheLineSize)); - if (allocated == nullptr) { - return nullptr; - } - const uintptr_t misalignment = - reinterpret_cast(allocated) & (kCacheLineSize - 1); - // malloc is at least kPointerSize aligned, so we can store the "allocated" - // pointer immediately before the aligned memory. - assert(misalignment % kPointerSize == 0); - char *const aligned = allocated + kCacheLineSize - misalignment; - memcpy(aligned - kPointerSize, &allocated, kPointerSize); - return BUTTERAUGLI_ASSUME_ALIGNED(aligned, 64); -} - -void CacheAligned::Free(void *aligned_pointer) { - if (aligned_pointer == nullptr) { - return; - } - char *const aligned = static_cast(aligned_pointer); - assert(reinterpret_cast(aligned) % kCacheLineSize == 0); - char *allocated; - memcpy(&allocated, aligned - kPointerSize, kPointerSize); - assert(allocated <= aligned - kPointerSize); - assert(allocated >= aligned - kCacheLineSize); - free(allocated); -} - -static inline bool IsNan(const float x) { - uint32_t bits; - memcpy(&bits, &x, sizeof(bits)); - const uint32_t bitmask_exp = 0x7F800000; - return (bits & bitmask_exp) == bitmask_exp && (bits & 0x7FFFFF); -} - -static inline bool IsNan(const double x) { - uint64_t bits; - memcpy(&bits, &x, sizeof(bits)); - return (0x7ff0000000000001ULL <= bits && bits <= 0x7fffffffffffffffULL) || - (0xfff0000000000001ULL <= bits && bits <= 0xffffffffffffffffULL); -} - -static inline void CheckImage(const ImageF &image, const char *name) { - for (size_t y = 0; y < image.ysize(); ++y) { - const float * const BUTTERAUGLI_RESTRICT row = image.Row(y); - for (size_t x = 0; x < image.xsize(); ++x) { - if (IsNan(row[x])) { - printf("Image %s @ %lu,%lu (of %lu,%lu)\n", name, x, y, image.xsize(), - image.ysize()); - exit(1); - } - } - } -} - -#if BUTTERAUGLI_ENABLE_CHECKS - -#define CHECK_NAN(x, str) \ - do { \ - if (IsNan(x)) { \ - printf("%d: %s\n", __LINE__, str); \ - abort(); \ - } \ - } while (0) - -#define CHECK_IMAGE(image, name) CheckImage(image, name) - -#else - -#define CHECK_NAN(x, str) -#define CHECK_IMAGE(image, name) - -#endif - - -// Purpose of kInternalGoodQualityThreshold: -// Normalize 'ok' image degradation to 1.0 across different versions of -// butteraugli. -static const double kInternalGoodQualityThreshold = 20.35; -static const double kGlobalScale = 1.0 / kInternalGoodQualityThreshold; - -inline float DotProduct(const float u[3], const float v[3]) { - return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; -} - -std::vector ComputeKernel(float sigma) { - const float m = 2.25; // Accuracy increases when m is increased. - const float scaler = -1.0 / (2 * sigma * sigma); - const int diff = std::max(1, m * fabs(sigma)); - std::vector kernel(2 * diff + 1); - for (int i = -diff; i <= diff; ++i) { - kernel[i + diff] = exp(scaler * i * i); - } - return kernel; -} - -void ConvolveBorderColumn( - const ImageF& in, - const std::vector& kernel, - const float weight_no_border, - const float border_ratio, - const size_t x, - float* const BUTTERAUGLI_RESTRICT row_out) { - const int offset = kernel.size() / 2; - int minx = x < offset ? 0 : x - offset; - int maxx = std::min(in.xsize() - 1, x + offset); - float weight = 0.0f; - for (int j = minx; j <= maxx; ++j) { - weight += kernel[j - x + offset]; - } - // Interpolate linearly between the no-border scaling and border scaling. - weight = (1.0f - border_ratio) * weight + border_ratio * weight_no_border; - float scale = 1.0f / weight; - for (size_t y = 0; y < in.ysize(); ++y) { - const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); - float sum = 0.0f; - for (int j = minx; j <= maxx; ++j) { - sum += row_in[j] * kernel[j - x + offset]; - } - row_out[y] = sum * scale; - } -} - -// Computes a horizontal convolution and transposes the result. -ImageF Convolution(const ImageF& in, - const std::vector& kernel, - const float border_ratio) { - ImageF out(in.ysize(), in.xsize()); - const int len = kernel.size(); - const int offset = kernel.size() / 2; - float weight_no_border = 0.0f; - for (int j = 0; j < len; ++j) { - weight_no_border += kernel[j]; - } - float scale_no_border = 1.0f / weight_no_border; - const int border1 = in.xsize() <= offset ? in.xsize() : offset; - const int border2 = in.xsize() - offset; - std::vector scaled_kernel = kernel; - for (int i = 0; i < scaled_kernel.size(); ++i) { - scaled_kernel[i] *= scale_no_border; - } - // left border - for (int x = 0; x < border1; ++x) { - ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, - out.Row(x)); - } - // middle - for (size_t y = 0; y < in.ysize(); ++y) { - const float* const BUTTERAUGLI_RESTRICT row_in = in.Row(y); - for (int x = border1; x < border2; ++x) { - const int d = x - offset; - float* const BUTTERAUGLI_RESTRICT row_out = out.Row(x); - float sum = 0.0f; - for (int j = 0; j < len; ++j) { - sum += row_in[d + j] * scaled_kernel[j]; - } - row_out[y] = sum; - } - } - // right border - for (int x = border2; x < in.xsize(); ++x) { - ConvolveBorderColumn(in, kernel, weight_no_border, border_ratio, x, - out.Row(x)); - } - return out; -} - -// A blur somewhat similar to a 2D Gaussian blur. -// See: https://en.wikipedia.org/wiki/Gaussian_blur -ImageF Blur(const ImageF& in, float sigma, float border_ratio) { - std::vector kernel = ComputeKernel(sigma); - return Convolution(Convolution(in, kernel, border_ratio), - kernel, border_ratio); -} - -// Clamping linear interpolator. -inline double InterpolateClampNegative(const double *array, - int size, double ix) { - if (ix < 0) { - ix = 0; - } - int baseix = static_cast(ix); - double res; - if (baseix >= size - 1) { - res = array[size - 1]; - } else { - double mix = ix - baseix; - int nextix = baseix + 1; - res = array[baseix] + mix * (array[nextix] - array[baseix]); - } - return res; -} - -double GammaMinArg() { - double out0, out1, out2; - OpsinAbsorbance(0.0, 0.0, 0.0, &out0, &out1, &out2); - return std::min(out0, std::min(out1, out2)); -} - -double GammaMaxArg() { - double out0, out1, out2; - OpsinAbsorbance(255.0, 255.0, 255.0, &out0, &out1, &out2); - return std::max(out0, std::max(out1, out2)); -} - -double SimpleGamma(double v) { - static const double kGamma = 0.372322653176; - static const double limit = 37.8000499603; - double bright = v - limit; - if (bright >= 0) { - static const double mul = 0.0950819040934; - v -= bright * mul; - } - { - static const double limit2 = 74.6154406429; - double bright2 = v - limit2; - if (bright2 >= 0) { - static const double mul = 0.01; - v -= bright2 * mul; - } - } - { - static const double limit2 = 82.8505938033; - double bright2 = v - limit2; - if (bright2 >= 0) { - static const double mul = 0.0316722592629; - v -= bright2 * mul; - } - } - { - static const double limit2 = 92.8505938033; - double bright2 = v - limit2; - if (bright2 >= 0) { - static const double mul = 0.221249885752; - v -= bright2 * mul; - } - } - { - static const double limit2 = 102.8505938033; - double bright2 = v - limit2; - if (bright2 >= 0) { - static const double mul = 0.0402547853939; - v -= bright2 * mul; - } - } - { - static const double limit2 = 112.8505938033; - double bright2 = v - limit2; - if (bright2 >= 0) { - static const double mul = 0.021471798711500003; - v -= bright2 * mul; - } - } - static const double offset = 0.106544447664; - static const double scale = 10.7950943969; - double retval = scale * (offset + pow(v, kGamma)); - return retval; -} - -static inline double Gamma(double v) { - //return SimpleGamma(v); - return GammaPolynomial(v); -} - -std::vector OpsinDynamicsImage(const std::vector& rgb) { - PROFILER_FUNC; - std::vector xyb(3); - std::vector blurred(3); - const double kSigma = 1.2; - for (int i = 0; i < 3; ++i) { - xyb[i] = ImageF(rgb[i].xsize(), rgb[i].ysize()); - blurred[i] = Blur(rgb[i], kSigma, 0.0f); - } - for (size_t y = 0; y < rgb[0].ysize(); ++y) { - const float* const BUTTERAUGLI_RESTRICT row_r = rgb[0].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_g = rgb[1].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_b = rgb[2].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_blurred_r = blurred[0].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_blurred_g = blurred[1].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_blurred_b = blurred[2].Row(y); - float* const BUTTERAUGLI_RESTRICT row_out_x = xyb[0].Row(y); - float* const BUTTERAUGLI_RESTRICT row_out_y = xyb[1].Row(y); - float* const BUTTERAUGLI_RESTRICT row_out_b = xyb[2].Row(y); - for (size_t x = 0; x < rgb[0].xsize(); ++x) { - float sensitivity[3]; - { - // Calculate sensitivity based on the smoothed image gamma derivative. - float pre_mixed0, pre_mixed1, pre_mixed2; - OpsinAbsorbance(row_blurred_r[x], row_blurred_g[x], row_blurred_b[x], - &pre_mixed0, &pre_mixed1, &pre_mixed2); - // TODO: use new polynomial to compute Gamma(x)/x derivative. - sensitivity[0] = Gamma(pre_mixed0) / pre_mixed0; - sensitivity[1] = Gamma(pre_mixed1) / pre_mixed1; - sensitivity[2] = Gamma(pre_mixed2) / pre_mixed2; - } - float cur_mixed0, cur_mixed1, cur_mixed2; - OpsinAbsorbance(row_r[x], row_g[x], row_b[x], - &cur_mixed0, &cur_mixed1, &cur_mixed2); - cur_mixed0 *= sensitivity[0]; - cur_mixed1 *= sensitivity[1]; - cur_mixed2 *= sensitivity[2]; - RgbToXyb(cur_mixed0, cur_mixed1, cur_mixed2, - &row_out_x[x], &row_out_y[x], &row_out_b[x]); - } - } - return xyb; -} - -// Make area around zero less important (remove it). -static BUTTERAUGLI_INLINE float RemoveRangeAroundZero(float w, float x) { - return x > w ? x - w : x < -w ? x + w : 0.0f; -} - -// Make area around zero more important (2x it until the limit). -static BUTTERAUGLI_INLINE float AmplifyRangeAroundZero(float w, float x) { - return x > w ? x + w : x < -w ? x - w : 2.0f * x; -} - -// XybLowFreqToVals converts from low-frequency XYB space to the 'vals' space. -// Vals space can be converted to L2-norm space (Euclidean and normalized) -// through visual masking. -template -BUTTERAUGLI_INLINE void XybLowFreqToVals(const V &x, const V &y, const V &b_arg, - V *BUTTERAUGLI_RESTRICT valx, - V *BUTTERAUGLI_RESTRICT valy, - V *BUTTERAUGLI_RESTRICT valb) { - static const double xmuli = 5.57547552483; - static const double ymuli = 1.20828034498; - static const double bmuli = 6.08319517575; - static const double y_to_b_muli = -0.628811683685; - - const V xmul(xmuli); - const V ymul(ymuli); - const V bmul(bmuli); - const V y_to_b_mul(y_to_b_muli); - const V b = b_arg + y_to_b_mul * y; - *valb = b * bmul; - *valx = x * xmul; - *valy = y * ymul; -} - -static ImageF SuppressInBrightAreas(size_t xsize, size_t ysize, - double mul, double mul2, double reg, - const ImageF& hf, - const ImageF& brightness) { - ImageF inew(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - const float* const rowhf = hf.Row(y); - const float* const rowbr = brightness.Row(y); - float* const rownew = inew.Row(y); - for (size_t x = 0; x < xsize; ++x) { - float v = rowhf[x]; - float scaler = mul * reg / (reg + rowbr[x]); - rownew[x] = scaler * v; - } - } - return inew; -} - - -static float SuppressHfInBrightAreas(float hf, float brightness, - float mul, float reg) { - float scaler = mul * reg / (reg + brightness); - return scaler * hf; -} - -static float SuppressUhfInBrightAreas(float hf, float brightness, - float mul, float reg) { - float scaler = mul * reg / (reg + brightness); - return scaler * hf; -} - -static float MaximumClamp(float v, float maxval) { - static const double kMul = 0.688059627878; - if (v >= maxval) { - v -= maxval; - v *= kMul; - v += maxval; - } else if (v < -maxval) { - v += maxval; - v *= kMul; - v -= maxval; - } - return v; -} - -static ImageF MaximumClamping(size_t xsize, size_t ysize, const ImageF& ix, - double yw) { - static const double kMul = 0.688059627878; - ImageF inew(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - const float* const rowx = ix.Row(y); - float* const rownew = inew.Row(y); - for (size_t x = 0; x < xsize; ++x) { - double v = rowx[x]; - if (v >= yw) { - v -= yw; - v *= kMul; - v += yw; - } else if (v < -yw) { - v += yw; - v *= kMul; - v -= yw; - } - rownew[x] = v; - } - } - return inew; -} - -static ImageF SuppressXByY(size_t xsize, size_t ysize, - const ImageF& ix, const ImageF& iy, - const double yw) { - static const double s = 0.745954517135; - ImageF inew(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - const float* const rowx = ix.Row(y); - const float* const rowy = iy.Row(y); - float* const rownew = inew.Row(y); - for (size_t x = 0; x < xsize; ++x) { - const double xval = rowx[x]; - const double yval = rowy[x]; - const double scaler = s + (yw * (1.0 - s)) / (yw + yval * yval); - rownew[x] = scaler * xval; - } - } - return inew; -} - -static void SeparateFrequencies( - size_t xsize, size_t ysize, - const std::vector& xyb, - PsychoImage &ps) { - PROFILER_FUNC; - ps.lf.resize(3); // XYB - ps.mf.resize(3); // XYB - ps.hf.resize(2); // XY - ps.uhf.resize(2); // XY - // Extract lf ... - static const double kSigmaLf = 7.46953768697; - static const double kSigmaHf = 3.734768843485; - static const double kSigmaUhf = 1.8673844217425; - // At borders we move some more of the energy to the high frequency - // parts, because there can be unfortunate continuations in tiling - // background color etc. So we want to represent the borders with - // some more accuracy. - static double border_lf = -0.00457628248637; - static double border_mf = -0.271277366628; - static double border_hf = 0.147068973249; - for (int i = 0; i < 3; ++i) { - ps.lf[i] = Blur(xyb[i], kSigmaLf, border_lf); - // ... and keep everything else in mf. - ps.mf[i] = ImageF(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - ps.mf[i].Row(y)[x] = xyb[i].Row(y)[x] - ps.lf[i].Row(y)[x]; - } - } - if (i == 2) { - ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); - break; - } - // Divide mf into mf and hf. - ps.hf[i] = ImageF(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[i].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_hf[x] = row_mf[x]; - } - } - ps.mf[i] = Blur(ps.mf[i], kSigmaHf, border_mf); - static const double w0 = 0.120079806822; - static const double w1 = 0.03430529365; - if (i == 0) { - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[0].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_hf[x] -= row_mf[x]; - row_mf[x] = RemoveRangeAroundZero(w0, row_mf[x]); - } - } - } else { - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_mf = ps.mf[1].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_hf[x] -= row_mf[x]; - row_mf[x] = AmplifyRangeAroundZero(w1, row_mf[x]); - } - } - } - } - // Suppress red-green by intensity change in the high freq channels. - static const double suppress = 2.96534974403; - ps.hf[0] = SuppressXByY(xsize, ysize, ps.hf[0], ps.hf[1], suppress); - - for (int i = 0; i < 2; ++i) { - // Divide hf into hf and uhf. - ps.uhf[i] = ImageF(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[i].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[i].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_uhf[x] = row_hf[x]; - } - } - ps.hf[i] = Blur(ps.hf[i], kSigmaUhf, border_hf); - static const double kRemoveHfRange = 0.0287615200377; - static const double kMaxclampHf = 78.8223237675; - static const double kMaxclampUhf = 5.8907152736; - static const float kMulSuppressHf = 1.10684769012; - static const float kMulRegHf = 0.478741530298; - static const float kRegHf = 2000 * kMulRegHf; - static const float kMulSuppressUhf = 1.76905001176; - static const float kMulRegUhf = 0.310148420674; - static const float kRegUhf = 2000 * kMulRegUhf; - - if (i == 0) { - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[0].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[0].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_uhf[x] -= row_hf[x]; - row_hf[x] = RemoveRangeAroundZero(kRemoveHfRange, row_hf[x]); - } - } - } else { - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_uhf = ps.uhf[1].Row(y); - float* BUTTERAUGLI_RESTRICT const row_hf = ps.hf[1].Row(y); - float* BUTTERAUGLI_RESTRICT const row_lf = ps.lf[1].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row_uhf[x] -= row_hf[x]; - row_hf[x] = MaximumClamp(row_hf[x], kMaxclampHf); - row_uhf[x] = MaximumClamp(row_uhf[x], kMaxclampUhf); - row_uhf[x] = SuppressUhfInBrightAreas(row_uhf[x], row_lf[x], - kMulSuppressUhf, kRegUhf); - row_hf[x] = SuppressHfInBrightAreas(row_hf[x], row_lf[x], - kMulSuppressHf, kRegHf); - - } - } - } - } - // Modify range around zero code only concerns the high frequency - // planes and only the X and Y channels. - // Convert low freq xyb to vals space so that we can do a simple squared sum - // diff on the low frequencies later. - for (size_t y = 0; y < ysize; ++y) { - float* BUTTERAUGLI_RESTRICT const row_x = ps.lf[0].Row(y); - float* BUTTERAUGLI_RESTRICT const row_y = ps.lf[1].Row(y); - float* BUTTERAUGLI_RESTRICT const row_b = ps.lf[2].Row(y); - for (size_t x = 0; x < xsize; ++x) { - float valx, valy, valb; - XybLowFreqToVals(row_x[x], row_y[x], row_b[x], &valx, &valy, &valb); - row_x[x] = valx; - row_y[x] = valy; - row_b[x] = valb; - } - } -} - -static void SameNoiseLevels(const ImageF& i0, const ImageF& i1, - const double kSigma, - const double w, - const double maxclamp, - ImageF* BUTTERAUGLI_RESTRICT diffmap) { - ImageF blurred(i0.xsize(), i0.ysize()); - for (size_t y = 0; y < i0.ysize(); ++y) { - const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); - const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); - float* BUTTERAUGLI_RESTRICT const to = blurred.Row(y); - for (size_t x = 0; x < i0.xsize(); ++x) { - double v0 = fabs(row0[x]); - double v1 = fabs(row1[x]); - if (v0 > maxclamp) v0 = maxclamp; - if (v1 > maxclamp) v1 = maxclamp; - to[x] = v0 - v1; - } - - } - blurred = Blur(blurred, kSigma, 0.0); - for (size_t y = 0; y < i0.ysize(); ++y) { - const float* BUTTERAUGLI_RESTRICT const row = blurred.Row(y); - float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); - for (size_t x = 0; x < i0.xsize(); ++x) { - double diff = row[x]; - row_diff[x] += w * diff * diff; - } - } -} - -static void L2Diff(const ImageF& i0, const ImageF& i1, const double w, - ImageF* BUTTERAUGLI_RESTRICT diffmap) { - if (w == 0) { - return; - } - for (size_t y = 0; y < i0.ysize(); ++y) { - const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); - const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); - float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); - for (size_t x = 0; x < i0.xsize(); ++x) { - double diff = row0[x] - row1[x]; - row_diff[x] += w * diff * diff; - } - } -} - -// i0 is the original image. -// i1 is the deformed copy. -static void L2DiffAsymmetric(const ImageF& i0, const ImageF& i1, - double w_0gt1, - double w_0lt1, - ImageF* BUTTERAUGLI_RESTRICT diffmap) { - if (w_0gt1 == 0 && w_0lt1 == 0) { - return; - } - w_0gt1 *= 0.8; - w_0lt1 *= 0.8; - for (size_t y = 0; y < i0.ysize(); ++y) { - const float* BUTTERAUGLI_RESTRICT const row0 = i0.Row(y); - const float* BUTTERAUGLI_RESTRICT const row1 = i1.Row(y); - float* BUTTERAUGLI_RESTRICT const row_diff = diffmap->Row(y); - for (size_t x = 0; x < i0.xsize(); ++x) { - // Primary symmetric quadratic objective. - double diff = row0[x] - row1[x]; - row_diff[x] += w_0gt1 * diff * diff; - - // Secondary half-open quadratic objectives. - const double fabs0 = fabs(row0[x]); - const double too_small = 0.4 * fabs0; - const double too_big = 1.0 * fabs0; - - if (row0[x] < 0) { - if (row1[x] > -too_small) { - double v = row1[x] + too_small; - row_diff[x] += w_0lt1 * v * v; - } else if (row1[x] < -too_big) { - double v = -row1[x] - too_big; - row_diff[x] += w_0lt1 * v * v; - } - } else { - if (row1[x] < too_small) { - double v = too_small - row1[x]; - row_diff[x] += w_0lt1 * v * v; - } else if (row1[x] > too_big) { - double v = row1[x] - too_big; - row_diff[x] += w_0lt1 * v * v; - } - } - } - } -} - -// Making a cluster of local errors to be more impactful than -// just a single error. -ImageF CalculateDiffmap(const ImageF& diffmap_in) { - PROFILER_FUNC; - // Take square root. - ImageF diffmap(diffmap_in.xsize(), diffmap_in.ysize()); - static const float kInitialSlope = 100.0f; - for (size_t y = 0; y < diffmap.ysize(); ++y) { - const float* const BUTTERAUGLI_RESTRICT row_in = diffmap_in.Row(y); - float* const BUTTERAUGLI_RESTRICT row_out = diffmap.Row(y); - for (size_t x = 0; x < diffmap.xsize(); ++x) { - const float orig_val = row_in[x]; - // TODO(b/29974893): Until that is fixed do not call sqrt on very small - // numbers. - row_out[x] = (orig_val < (1.0f / (kInitialSlope * kInitialSlope)) - ? kInitialSlope * orig_val - : std::sqrt(orig_val)); - } - } - { - static const double kSigma = 1.72547472444; - static const double mul1 = 0.458794906198; - static const float scale = 1.0f / (1.0f + mul1); - static const double border_ratio = 1.0; // 2.01209066992; - ImageF blurred = Blur(diffmap, kSigma, border_ratio); - for (int y = 0; y < diffmap.ysize(); ++y) { - const float* const BUTTERAUGLI_RESTRICT row_blurred = blurred.Row(y); - float* const BUTTERAUGLI_RESTRICT row = diffmap.Row(y); - for (int x = 0; x < diffmap.xsize(); ++x) { - row[x] += mul1 * row_blurred[x]; - row[x] *= scale; - } - } - } - return diffmap; -} - -void MaskPsychoImage(const PsychoImage& pi0, const PsychoImage& pi1, - const size_t xsize, const size_t ysize, - std::vector* BUTTERAUGLI_RESTRICT mask, - std::vector* BUTTERAUGLI_RESTRICT mask_dc) { - std::vector mask_xyb0 = CreatePlanes(xsize, ysize, 3); - std::vector mask_xyb1 = CreatePlanes(xsize, ysize, 3); - static const double muls[4] = { - 0, - 1.64178305129, - 0.831081703362, - 3.23680933546, - }; - for (int i = 0; i < 2; ++i) { - double a = muls[2 * i]; - double b = muls[2 * i + 1]; - for (size_t y = 0; y < ysize; ++y) { - const float* const BUTTERAUGLI_RESTRICT row_hf0 = pi0.hf[i].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_hf1 = pi1.hf[i].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_uhf0 = pi0.uhf[i].Row(y); - const float* const BUTTERAUGLI_RESTRICT row_uhf1 = pi1.uhf[i].Row(y); - float* const BUTTERAUGLI_RESTRICT row0 = mask_xyb0[i].Row(y); - float* const BUTTERAUGLI_RESTRICT row1 = mask_xyb1[i].Row(y); - for (size_t x = 0; x < xsize; ++x) { - row0[x] = a * row_uhf0[x] + b * row_hf0[x]; - row1[x] = a * row_uhf1[x] + b * row_hf1[x]; - } - } - } - Mask(mask_xyb0, mask_xyb1, mask, mask_dc); -} - -ButteraugliComparator::ButteraugliComparator(const std::vector& rgb0) - : xsize_(rgb0[0].xsize()), - ysize_(rgb0[0].ysize()), - num_pixels_(xsize_ * ysize_) { - if (xsize_ < 8 || ysize_ < 8) return; - std::vector xyb0 = OpsinDynamicsImage(rgb0); - SeparateFrequencies(xsize_, ysize_, xyb0, pi0_); -} - -void ButteraugliComparator::Mask( - std::vector* BUTTERAUGLI_RESTRICT mask, - std::vector* BUTTERAUGLI_RESTRICT mask_dc) const { - MaskPsychoImage(pi0_, pi0_, xsize_, ysize_, mask, mask_dc); -} - -void ButteraugliComparator::Diffmap(const std::vector& rgb1, - ImageF &result) const { - PROFILER_FUNC; - if (xsize_ < 8 || ysize_ < 8) return; - DiffmapOpsinDynamicsImage(OpsinDynamicsImage(rgb1), result); -} - -void ButteraugliComparator::DiffmapOpsinDynamicsImage( - const std::vector& xyb1, - ImageF &result) const { - PROFILER_FUNC; - if (xsize_ < 8 || ysize_ < 8) return; - PsychoImage pi1; - SeparateFrequencies(xsize_, ysize_, xyb1, pi1); - result = ImageF(xsize_, ysize_); - DiffmapPsychoImage(pi1, result); -} - -void ButteraugliComparator::DiffmapPsychoImage(const PsychoImage& pi1, - ImageF& result) const { - PROFILER_FUNC; - const float hf_asymmetry_ = 0.8f; - if (xsize_ < 8 || ysize_ < 8) { - return; - } - std::vector block_diff_dc(3); - std::vector block_diff_ac(3); - for (int c = 0; c < 3; ++c) { - block_diff_dc[c] = ImageF(xsize_, ysize_, 0.0); - block_diff_ac[c] = ImageF(xsize_, ysize_, 0.0); - } - - static const double wUhfMalta = 5.1409625726; - static const double norm1Uhf = 58.5001247061; - MaltaDiffMap(pi0_.uhf[1], pi1.uhf[1], - wUhfMalta * hf_asymmetry_, - wUhfMalta / hf_asymmetry_, - norm1Uhf, - &block_diff_ac[1]); - - static const double wUhfMaltaX = 4.91743441556; - static const double norm1UhfX = 687196.39002; - MaltaDiffMap(pi0_.uhf[0], pi1.uhf[0], - wUhfMaltaX * hf_asymmetry_, - wUhfMaltaX / hf_asymmetry_, - norm1UhfX, - &block_diff_ac[0]); - - static const double wHfMalta = 153.671655716; - static const double norm1Hf = 83150785.9592; - MaltaDiffMapLF(pi0_.hf[1], pi1.hf[1], - wHfMalta * sqrt(hf_asymmetry_), - wHfMalta / sqrt(hf_asymmetry_), - norm1Hf, - &block_diff_ac[1]); - - static const double wHfMaltaX = 668.358918152; - static const double norm1HfX = 0.882954368025; - MaltaDiffMapLF(pi0_.hf[0], pi1.hf[0], - wHfMaltaX * sqrt(hf_asymmetry_), - wHfMaltaX / sqrt(hf_asymmetry_), - norm1HfX, - &block_diff_ac[0]); - - static const double wMfMalta = 6841.81248144; - static const double norm1Mf = 0.0135134962487; - MaltaDiffMapLF(pi0_.mf[1], pi1.mf[1], wMfMalta, wMfMalta, norm1Mf, - &block_diff_ac[1]); - - static const double wMfMaltaX = 813.901703816; - static const double norm1MfX = 16792.9322251; - MaltaDiffMapLF(pi0_.mf[0], pi1.mf[0], wMfMaltaX, wMfMaltaX, norm1MfX, - &block_diff_ac[0]); - - static const double wmul[9] = { - 0, - 32.4449876135, - 0, - 0, - 0, - 0, - 1.01370836411, - 0, - 1.74566011615, - }; - - static const double maxclamp = 85.7047444518; - static const double kSigmaHfX = 10.6666499623; - static const double w = 884.809801415; - SameNoiseLevels(pi0_.hf[1], pi1.hf[1], kSigmaHfX, w, maxclamp, - &block_diff_ac[1]); - - for (int c = 0; c < 3; ++c) { - if (c < 2) { - L2DiffAsymmetric(pi0_.hf[c], pi1.hf[c], - wmul[c] * hf_asymmetry_, - wmul[c] / hf_asymmetry_, - &block_diff_ac[c]); - } - L2Diff(pi0_.mf[c], pi1.mf[c], wmul[3 + c], &block_diff_ac[c]); - L2Diff(pi0_.lf[c], pi1.lf[c], wmul[6 + c], &block_diff_dc[c]); - } - - std::vector mask_xyb; - std::vector mask_xyb_dc; - MaskPsychoImage(pi0_, pi1, xsize_, ysize_, &mask_xyb, &mask_xyb_dc); - - result = CalculateDiffmap( - CombineChannels(mask_xyb, mask_xyb_dc, block_diff_dc, block_diff_ac)); -} - -// Allows PaddedMaltaUnit to call either function via overloading. -struct MaltaTagLF {}; -struct MaltaTag {}; - -static float MaltaUnit(MaltaTagLF, const float* BUTTERAUGLI_RESTRICT d, - const int xs) { - const int xs3 = 3 * xs; - float retval = 0; - { - // x grows, y constant - float sum = - d[-4] + - d[-2] + - d[0] + - d[2] + - d[4]; - retval += sum * sum; - } - { - // y grows, x constant - float sum = - d[-xs3 - xs] + - d[-xs - xs] + - d[0] + - d[xs + xs] + - d[xs3 + xs]; - retval += sum * sum; - } - { - // both grow - float sum = - d[-xs3 - 3] + - d[-xs - xs - 2] + - d[0] + - d[xs + xs + 2] + - d[xs3 + 3]; - retval += sum * sum; - } - { - // y grows, x shrinks - float sum = - d[-xs3 + 3] + - d[-xs - xs + 2] + - d[0] + - d[xs + xs - 2] + - d[xs3 - 3]; - retval += sum * sum; - } - { - // y grows -4 to 4, x shrinks 1 -> -1 - float sum = - d[-xs3 - xs + 1] + - d[-xs - xs + 1] + - d[0] + - d[xs + xs - 1] + - d[xs3 + xs - 1]; - retval += sum * sum; - } - { - // y grows -4 to 4, x grows -1 -> 1 - float sum = - d[-xs3 - xs - 1] + - d[-xs - xs - 1] + - d[0] + - d[xs + xs + 1] + - d[xs3 + xs + 1]; - retval += sum * sum; - } - { - // x grows -4 to 4, y grows -1 to 1 - float sum = - d[-4 - xs] + - d[-2 - xs] + - d[0] + - d[2 + xs] + - d[4 + xs]; - retval += sum * sum; - } - { - // x grows -4 to 4, y shrinks 1 to -1 - float sum = - d[-4 + xs] + - d[-2 + xs] + - d[0] + - d[2 - xs] + - d[4 - xs]; - retval += sum * sum; - } - { - /* 0_________ - 1__*______ - 2___*_____ - 3_________ - 4____0____ - 5_________ - 6_____*___ - 7______*__ - 8_________ */ - float sum = - d[-xs3 - 2] + - d[-xs - xs - 1] + - d[0] + - d[xs + xs + 1] + - d[xs3 + 2]; - retval += sum * sum; - } - { - /* 0_________ - 1______*__ - 2_____*___ - 3_________ - 4____0____ - 5_________ - 6___*_____ - 7__*______ - 8_________ */ - float sum = - d[-xs3 + 2] + - d[-xs - xs + 1] + - d[0] + - d[xs + xs - 1] + - d[xs3 - 2]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2_*_______ - 3__*______ - 4____0____ - 5______*__ - 6_______*_ - 7_________ - 8_________ */ - float sum = - d[-xs - xs - 3] + - d[-xs - 2] + - d[0] + - d[xs + 2] + - d[xs + xs + 3]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2_______*_ - 3______*__ - 4____0____ - 5__*______ - 6_*_______ - 7_________ - 8_________ */ - float sum = - d[-xs - xs + 3] + - d[-xs + 2] + - d[0] + - d[xs - 2] + - d[xs + xs - 3]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2________* - 3______*__ - 4____0____ - 5__*______ - 6*________ - 7_________ - 8_________ */ - - float sum = - d[xs + xs - 4] + - d[xs - 2] + - d[0] + - d[-xs + 2] + - d[-xs - xs + 4]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2*________ - 3__*______ - 4____0____ - 5______*__ - 6________* - 7_________ - 8_________ */ - float sum = - d[-xs - xs - 4] + - d[-xs - 2] + - d[0] + - d[xs + 2] + - d[xs + xs + 4]; - retval += sum * sum; - } - { - /* 0__*______ - 1_________ - 2___*_____ - 3_________ - 4____0____ - 5_________ - 6_____*___ - 7_________ - 8______*__ */ - float sum = - d[-xs3 - xs - 2] + - d[-xs - xs - 1] + - d[0] + - d[xs + xs + 1] + - d[xs3 + xs + 2]; - retval += sum * sum; - } - { - /* 0______*__ - 1_________ - 2_____*___ - 3_________ - 4____0____ - 5_________ - 6___*_____ - 7_________ - 8__*______ */ - float sum = - d[-xs3 - xs + 2] + - d[-xs - xs + 1] + - d[0] + - d[xs + xs - 1] + - d[xs3 + xs - 2]; - retval += sum * sum; - } - return retval; -} - -static float MaltaUnit(MaltaTag, const float* BUTTERAUGLI_RESTRICT d, - const int xs) { - const int xs3 = 3 * xs; - float retval = 0; - { - // x grows, y constant - float sum = - d[-4] + - d[-3] + - d[-2] + - d[-1] + - d[0] + - d[1] + - d[2] + - d[3] + - d[4]; - retval += sum * sum; - } - { - // y grows, x constant - float sum = - d[-xs3 - xs] + - d[-xs3] + - d[-xs - xs] + - d[-xs] + - d[0] + - d[xs] + - d[xs + xs] + - d[xs3] + - d[xs3 + xs]; - retval += sum * sum; - } - { - // both grow - float sum = - d[-xs3 - 3] + - d[-xs - xs - 2] + - d[-xs - 1] + - d[0] + - d[xs + 1] + - d[xs + xs + 2] + - d[xs3 + 3]; - retval += sum * sum; - } - { - // y grows, x shrinks - float sum = - d[-xs3 + 3] + - d[-xs - xs + 2] + - d[-xs + 1] + - d[0] + - d[xs - 1] + - d[xs + xs - 2] + - d[xs3 - 3]; - retval += sum * sum; - } - { - // y grows -4 to 4, x shrinks 1 -> -1 - float sum = - d[-xs3 - xs + 1] + - d[-xs3 + 1] + - d[-xs - xs + 1] + - d[-xs] + - d[0] + - d[xs] + - d[xs + xs - 1] + - d[xs3 - 1] + - d[xs3 + xs - 1]; - retval += sum * sum; - } - { - // y grows -4 to 4, x grows -1 -> 1 - float sum = - d[-xs3 - xs - 1] + - d[-xs3 - 1] + - d[-xs - xs - 1] + - d[-xs] + - d[0] + - d[xs] + - d[xs + xs + 1] + - d[xs3 + 1] + - d[xs3 + xs + 1]; - retval += sum * sum; - } - { - // x grows -4 to 4, y grows -1 to 1 - float sum = - d[-4 - xs] + - d[-3 - xs] + - d[-2 - xs] + - d[-1] + - d[0] + - d[1] + - d[2 + xs] + - d[3 + xs] + - d[4 + xs]; - retval += sum * sum; - } - { - // x grows -4 to 4, y shrinks 1 to -1 - float sum = - d[-4 + xs] + - d[-3 + xs] + - d[-2 + xs] + - d[-1] + - d[0] + - d[1] + - d[2 - xs] + - d[3 - xs] + - d[4 - xs]; - retval += sum * sum; - } - { - /* 0_________ - 1__*______ - 2___*_____ - 3___*_____ - 4____0____ - 5_____*___ - 6_____*___ - 7______*__ - 8_________ */ - float sum = - d[-xs3 - 2] + - d[-xs - xs - 1] + - d[-xs - 1] + - d[0] + - d[xs + 1] + - d[xs + xs + 1] + - d[xs3 + 2]; - retval += sum * sum; - } - { - /* 0_________ - 1______*__ - 2_____*___ - 3_____*___ - 4____0____ - 5___*_____ - 6___*_____ - 7__*______ - 8_________ */ - float sum = - d[-xs3 + 2] + - d[-xs - xs + 1] + - d[-xs + 1] + - d[0] + - d[xs - 1] + - d[xs + xs - 1] + - d[xs3 - 2]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2_*_______ - 3__**_____ - 4____0____ - 5_____**__ - 6_______*_ - 7_________ - 8_________ */ - float sum = - d[-xs - xs - 3] + - d[-xs - 2] + - d[-xs - 1] + - d[0] + - d[xs + 1] + - d[xs + 2] + - d[xs + xs + 3]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2_______*_ - 3_____**__ - 4____0____ - 5__**_____ - 6_*_______ - 7_________ - 8_________ */ - float sum = - d[-xs - xs + 3] + - d[-xs + 2] + - d[-xs + 1] + - d[0] + - d[xs - 1] + - d[xs - 2] + - d[xs + xs - 3]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2_________ - 3______**_ - 4____0*___ - 5__**_____ - 6**_______ - 7_________ - 8_________ */ - - float sum = - d[xs + xs - 4] + - d[xs + xs - 3] + - d[xs - 2] + - d[xs - 1] + - d[0] + - d[1] + - d[-xs + 2] + - d[-xs + 3]; - retval += sum * sum; - } - { - /* 0_________ - 1_________ - 2**_______ - 3__**_____ - 4____0*___ - 5______**_ - 6_________ - 7_________ - 8_________ */ - float sum = - d[-xs - xs - 4] + - d[-xs - xs - 3] + - d[-xs - 2] + - d[-xs - 1] + - d[0] + - d[1] + - d[xs + 2] + - d[xs + 3]; - retval += sum * sum; - } - { - /* 0__*______ - 1__*______ - 2___*_____ - 3___*_____ - 4____0____ - 5____*____ - 6_____*___ - 7_____*___ - 8_________ */ - float sum = - d[-xs3 - xs - 2] + - d[-xs3 - 2] + - d[-xs - xs - 1] + - d[-xs - 1] + - d[0] + - d[xs] + - d[xs + xs + 1] + - d[xs3 + 1]; - retval += sum * sum; - } - { - /* 0______*__ - 1______*__ - 2_____*___ - 3_____*___ - 4____0____ - 5____*____ - 6___*_____ - 7___*_____ - 8_________ */ - float sum = - d[-xs3 - xs + 2] + - d[-xs3 + 2] + - d[-xs - xs + 1] + - d[-xs + 1] + - d[0] + - d[xs] + - d[xs + xs - 1] + - d[xs3 - 1]; - retval += sum * sum; - } - return retval; -} - -// Returns MaltaUnit. "fastMode" avoids bounds-checks when x0 and y0 are known -// to be far enough from the image borders. -template -static BUTTERAUGLI_INLINE float PaddedMaltaUnit( - float* const BUTTERAUGLI_RESTRICT diffs, const size_t x0, const size_t y0, - const size_t xsize_, const size_t ysize_) { - int ix0 = y0 * xsize_ + x0; - const float* BUTTERAUGLI_RESTRICT d = &diffs[ix0]; - if (fastMode || - (x0 >= 4 && y0 >= 4 && x0 < (xsize_ - 4) && y0 < (ysize_ - 4))) { - return MaltaUnit(Tag(), d, xsize_); - } - - float borderimage[9 * 9]; - for (int dy = 0; dy < 9; ++dy) { - int y = y0 + dy - 4; - if (y < 0 || y >= ysize_) { - for (int dx = 0; dx < 9; ++dx) { - borderimage[dy * 9 + dx] = 0.0f; - } - } else { - for (int dx = 0; dx < 9; ++dx) { - int x = x0 + dx - 4; - if (x < 0 || x >= xsize_) { - borderimage[dy * 9 + dx] = 0.0f; - } else { - borderimage[dy * 9 + dx] = diffs[y * xsize_ + x]; - } - } - } - } - return MaltaUnit(Tag(), &borderimage[4 * 9 + 4], 9); -} - -template -static void MaltaDiffMapImpl(const ImageF& lum0, const ImageF& lum1, - const size_t xsize_, const size_t ysize_, - const double w_0gt1, - const double w_0lt1, - double norm1, - const double len, const double mulli, - ImageF* block_diff_ac) { - const float kWeight0 = 0.5; - const float kWeight1 = 0.33; - - const double w_pre0gt1 = mulli * sqrt(kWeight0 * w_0gt1) / (len * 2 + 1); - const double w_pre0lt1 = mulli * sqrt(kWeight1 * w_0lt1) / (len * 2 + 1); - const float norm2_0gt1 = w_pre0gt1 * norm1; - const float norm2_0lt1 = w_pre0lt1 * norm1; - - std::vector diffs(ysize_ * xsize_); - for (size_t y = 0, ix = 0; y < ysize_; ++y) { - const float* BUTTERAUGLI_RESTRICT const row0 = lum0.Row(y); - const float* BUTTERAUGLI_RESTRICT const row1 = lum1.Row(y); - for (size_t x = 0; x < xsize_; ++x, ++ix) { - const float absval = 0.5 * std::abs(row0[x]) + 0.5 * std::abs(row1[x]); - const float diff = row0[x] - row1[x]; - const float scaler = norm2_0gt1 / (static_cast(norm1) + absval); - - // Primary symmetric quadratic objective. - diffs[ix] = scaler * diff; - - const float scaler2 = norm2_0lt1 / (static_cast(norm1) + absval); - const double fabs0 = fabs(row0[x]); - - // Secondary half-open quadratic objectives. - const double too_small = 0.55 * fabs0; - const double too_big = 1.05 * fabs0; - - if (row0[x] < 0) { - if (row1[x] > -too_small) { - double impact = scaler2 * (row1[x] + too_small); - if (diff < 0) { - diffs[ix] -= impact; - } else { - diffs[ix] += impact; - } - } else if (row1[x] < -too_big) { - double impact = scaler2 * (-row1[x] - too_big); - if (diff < 0) { - diffs[ix] -= impact; - } else { - diffs[ix] += impact; - } - } - } else { - if (row1[x] < too_small) { - double impact = scaler2 * (too_small - row1[x]); - if (diff < 0) { - diffs[ix] -= impact; - } else { - diffs[ix] += impact; - } - } else if (row1[x] > too_big) { - double impact = scaler2 * (row1[x] - too_big); - if (diff < 0) { - diffs[ix] -= impact; - } else { - diffs[ix] += impact; - } - } - } - } - } - - size_t y0 = 0; - // Top - for (; y0 < 4; ++y0) { - float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); - for (size_t x0 = 0; x0 < xsize_; ++x0) { - row_diff[x0] += - PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); - } - } - - // Middle - for (; y0 < ysize_ - 4; ++y0) { - float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); - size_t x0 = 0; - for (; x0 < 4; ++x0) { - row_diff[x0] += - PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); - } - for (; x0 < xsize_ - 4; ++x0) { - row_diff[x0] += - PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); - } - - for (; x0 < xsize_; ++x0) { - row_diff[x0] += - PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); - } - } - - // Bottom - for (; y0 < ysize_; ++y0) { - float* const BUTTERAUGLI_RESTRICT row_diff = block_diff_ac->Row(y0); - for (size_t x0 = 0; x0 < xsize_; ++x0) { - row_diff[x0] += - PaddedMaltaUnit(&diffs[0], x0, y0, xsize_, ysize_); - } - } -} - -void ButteraugliComparator::MaltaDiffMap( - const ImageF& lum0, const ImageF& lum1, - const double w_0gt1, - const double w_0lt1, - const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { - PROFILER_FUNC; - const double len = 3.75; - static const double mulli = 0.354191303559; - MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, w_0gt1, w_0lt1, - norm1, len, - mulli, block_diff_ac); -} - -void ButteraugliComparator::MaltaDiffMapLF( - const ImageF& lum0, const ImageF& lum1, - const double w_0gt1, - const double w_0lt1, - const double norm1, ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const { - PROFILER_FUNC; - const double len = 3.75; - static const double mulli = 0.405371989604; - MaltaDiffMapImpl(lum0, lum1, xsize_, ysize_, - w_0gt1, w_0lt1, - norm1, len, - mulli, block_diff_ac); -} - -ImageF ButteraugliComparator::CombineChannels( - const std::vector& mask_xyb, - const std::vector& mask_xyb_dc, - const std::vector& block_diff_dc, - const std::vector& block_diff_ac) const { - PROFILER_FUNC; - ImageF result(xsize_, ysize_); - for (size_t y = 0; y < ysize_; ++y) { - float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); - for (size_t x = 0; x < xsize_; ++x) { - float mask[3]; - float dc_mask[3]; - float diff_dc[3]; - float diff_ac[3]; - for (int i = 0; i < 3; ++i) { - mask[i] = mask_xyb[i].Row(y)[x]; - dc_mask[i] = mask_xyb_dc[i].Row(y)[x]; - diff_dc[i] = block_diff_dc[i].Row(y)[x]; - diff_ac[i] = block_diff_ac[i].Row(y)[x]; - } - row_out[x] = (DotProduct(diff_dc, dc_mask) + DotProduct(diff_ac, mask)); - } - } - return result; -} - -double ButteraugliScoreFromDiffmap(const ImageF& diffmap) { - PROFILER_FUNC; - float retval = 0.0f; - for (size_t y = 0; y < diffmap.ysize(); ++y) { - const float * const BUTTERAUGLI_RESTRICT row = diffmap.Row(y); - for (size_t x = 0; x < diffmap.xsize(); ++x) { - retval = std::max(retval, row[x]); - } - } - return retval; -} - -#include - -// ===== Functions used by Mask only ===== -static std::array MakeMask( - double extmul, double extoff, - double mul, double offset, - double scaler) { - std::array lut; - for (int i = 0; i < lut.size(); ++i) { - const double c = mul / ((0.01 * scaler * i) + offset); - lut[i] = kGlobalScale * (1.0 + extmul * (c + extoff)); - if (lut[i] < 1e-5) { - lut[i] = 1e-5; - } - assert(lut[i] >= 0.0); - lut[i] *= lut[i]; - } - return lut; -} - -double MaskX(double delta) { - static const double extmul = 2.59885507073; - static const double extoff = 3.08805636789; - static const double offset = 0.315424196682; - static const double scaler = 16.2770141832; - static const double mul = 5.62939030582; - static const std::array lut = - MakeMask(extmul, extoff, mul, offset, scaler); - return InterpolateClampNegative(lut.data(), lut.size(), delta); -} - -double MaskY(double delta) { - static const double extmul = 0.9613705131; - static const double extoff = -0.581933100068; - static const double offset = 1.00846207765; - static const double scaler = 2.2342321176; - static const double mul = 6.64307621174; - static const std::array lut = - MakeMask(extmul, extoff, mul, offset, scaler); - return InterpolateClampNegative(lut.data(), lut.size(), delta); -} - -double MaskDcX(double delta) { - static const double extmul = 10.0470705878; - static const double extoff = 3.18472654033; - static const double offset = 0.0551512255218; - static const double scaler = 70.0; - static const double mul = 0.373092999662; - static const std::array lut = - MakeMask(extmul, extoff, mul, offset, scaler); - return InterpolateClampNegative(lut.data(), lut.size(), delta); -} - -double MaskDcY(double delta) { - static const double extmul = 0.0115640939227; - static const double extoff = 45.9483175519; - static const double offset = 0.0142290066313; - static const double scaler = 5.0; - static const double mul = 2.52611324247; - static const std::array lut = - MakeMask(extmul, extoff, mul, offset, scaler); - return InterpolateClampNegative(lut.data(), lut.size(), delta); -} - -ImageF DiffPrecompute(const ImageF& xyb0, const ImageF& xyb1) { - PROFILER_FUNC; - const size_t xsize = xyb0.xsize(); - const size_t ysize = xyb0.ysize(); - ImageF result(xsize, ysize); - size_t x2, y2; - for (size_t y = 0; y < ysize; ++y) { - if (y + 1 < ysize) { - y2 = y + 1; - } else if (y > 0) { - y2 = y - 1; - } else { - y2 = y; - } - const float* const BUTTERAUGLI_RESTRICT row0_in = xyb0.Row(y); - const float* const BUTTERAUGLI_RESTRICT row1_in = xyb1.Row(y); - const float* const BUTTERAUGLI_RESTRICT row0_in2 = xyb0.Row(y2); - const float* const BUTTERAUGLI_RESTRICT row1_in2 = xyb1.Row(y2); - float* const BUTTERAUGLI_RESTRICT row_out = result.Row(y); - for (size_t x = 0; x < xsize; ++x) { - if (x + 1 < xsize) { - x2 = x + 1; - } else if (x > 0) { - x2 = x - 1; - } else { - x2 = x; - } - double sup0 = (fabs(row0_in[x] - row0_in[x2]) + - fabs(row0_in[x] - row0_in2[x])); - double sup1 = (fabs(row1_in[x] - row1_in[x2]) + - fabs(row1_in[x] - row1_in2[x])); - static const double mul0 = 0.918416534734; - row_out[x] = mul0 * std::min(sup0, sup1); - static const double cutoff = 55.0184555849; - if (row_out[x] >= cutoff) { - row_out[x] = cutoff; - } - } - } - return result; -} - -void Mask(const std::vector& xyb0, - const std::vector& xyb1, - std::vector* BUTTERAUGLI_RESTRICT mask, - std::vector* BUTTERAUGLI_RESTRICT mask_dc) { - PROFILER_FUNC; - const size_t xsize = xyb0[0].xsize(); - const size_t ysize = xyb0[0].ysize(); - mask->resize(3); - *mask_dc = CreatePlanes(xsize, ysize, 3); - double muls[2] = { - 0.207017089891, - 0.267138152891, - }; - double normalizer = { - 1.0 / (muls[0] + muls[1]), - }; - static const double r0 = 2.3770330432; - static const double r1 = 9.04353323561; - static const double r2 = 9.24456601467; - static const double border_ratio = -0.0724948220913; - - { - // X component - ImageF diff = DiffPrecompute(xyb0[0], xyb1[0]); - ImageF blurred = Blur(diff, r2, border_ratio); - (*mask)[0] = ImageF(xsize, ysize); - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - (*mask)[0].Row(y)[x] = blurred.Row(y)[x]; - } - } - } - { - // Y component - (*mask)[1] = ImageF(xsize, ysize); - ImageF diff = DiffPrecompute(xyb0[1], xyb1[1]); - ImageF blurred1 = Blur(diff, r0, border_ratio); - ImageF blurred2 = Blur(diff, r1, border_ratio); - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - const double val = normalizer * ( - muls[0] * blurred1.Row(y)[x] + - muls[1] * blurred2.Row(y)[x]); - (*mask)[1].Row(y)[x] = val; - } - } - } - // B component - (*mask)[2] = ImageF(xsize, ysize); - static const double mul[2] = { - 16.6963293877, - 2.1364621982, - }; - static const double w00 = 36.4671237619; - static const double w11 = 2.1887170895; - static const double w_ytob_hf = std::max( - 0.086624184478, - 0.0); - static const double w_ytob_lf = 21.6804277046; - static const double p1_to_p0 = 0.0513061271723; - - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - const double s0 = (*mask)[0].Row(y)[x]; - const double s1 = (*mask)[1].Row(y)[x]; - const double p1 = mul[1] * w11 * s1; - const double p0 = mul[0] * w00 * s0 + p1_to_p0 * p1; - - (*mask)[0].Row(y)[x] = MaskX(p0); - (*mask)[1].Row(y)[x] = MaskY(p1); - (*mask)[2].Row(y)[x] = w_ytob_hf * MaskY(p1); - (*mask_dc)[0].Row(y)[x] = MaskDcX(p0); - (*mask_dc)[1].Row(y)[x] = MaskDcY(p1); - (*mask_dc)[2].Row(y)[x] = w_ytob_lf * MaskDcY(p1); - } - } -} - -void ButteraugliDiffmap(const std::vector &rgb0_image, - const std::vector &rgb1_image, - ImageF &result_image) { - const size_t xsize = rgb0_image[0].xsize(); - const size_t ysize = rgb0_image[0].ysize(); - static const int kMax = 8; - if (xsize < kMax || ysize < kMax) { - // Butteraugli values for small (where xsize or ysize is smaller - // than 8 pixels) images are non-sensical, but most likely it is - // less disruptive to try to compute something than just give up. - // Temporarily extend the borders of the image to fit 8 x 8 size. - int xborder = xsize < kMax ? (kMax - xsize) / 2 : 0; - int yborder = ysize < kMax ? (kMax - ysize) / 2 : 0; - size_t xscaled = std::max(kMax, xsize); - size_t yscaled = std::max(kMax, ysize); - std::vector scaled0 = CreatePlanes(xscaled, yscaled, 3); - std::vector scaled1 = CreatePlanes(xscaled, yscaled, 3); - for (int i = 0; i < 3; ++i) { - for (int y = 0; y < yscaled; ++y) { - for (int x = 0; x < xscaled; ++x) { - size_t x2 = std::min(xsize - 1, std::max(0, x - xborder)); - size_t y2 = std::min(ysize - 1, std::max(0, y - yborder)); - scaled0[i].Row(y)[x] = rgb0_image[i].Row(y2)[x2]; - scaled1[i].Row(y)[x] = rgb1_image[i].Row(y2)[x2]; - } - } - } - ImageF diffmap_scaled; - ButteraugliDiffmap(scaled0, scaled1, diffmap_scaled); - result_image = ImageF(xsize, ysize); - for (int y = 0; y < ysize; ++y) { - for (int x = 0; x < xsize; ++x) { - result_image.Row(y)[x] = diffmap_scaled.Row(y + yborder)[x + xborder]; - } - } - return; - } - ButteraugliComparator butteraugli(rgb0_image); - butteraugli.Diffmap(rgb1_image, result_image); -} - -bool ButteraugliInterface(const std::vector &rgb0, - const std::vector &rgb1, - ImageF &diffmap, - double &diffvalue) { - const size_t xsize = rgb0[0].xsize(); - const size_t ysize = rgb0[0].ysize(); - if (xsize < 1 || ysize < 1) { - return false; // No image. - } - for (int i = 1; i < 3; i++) { - if (rgb0[i].xsize() != xsize || rgb0[i].ysize() != ysize || - rgb1[i].xsize() != xsize || rgb1[i].ysize() != ysize) { - return false; // Image planes must have same dimensions. - } - } - ButteraugliDiffmap(rgb0, rgb1, diffmap); - diffvalue = ButteraugliScoreFromDiffmap(diffmap); - return true; -} - -bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, - const std::vector > &rgb, std::vector &quant) { - if (xsize < 16 || ysize < 16) { - return false; // Butteraugli is undefined for small images. - } - size_t size = xsize * ysize; - - std::vector rgb_planes = PlanesFromPacked(xsize, ysize, rgb); - std::vector scale_xyb; - std::vector scale_xyb_dc; - Mask(rgb_planes, rgb_planes, &scale_xyb, &scale_xyb_dc); - quant.reserve(size); - - // Mask gives us values in 3 color channels, but for now we take only - // the intensity channel. - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - quant.push_back(scale_xyb[1].Row(y)[x]); - } - } - return true; -} - -double ButteraugliFuzzyClass(double score) { - static const double fuzzy_width_up = 6.07887388532; - static const double fuzzy_width_down = 5.50793514384; - static const double m0 = 2.0; - static const double scaler = 0.840253347958; - double val; - if (score < 1.0) { - // val in [scaler .. 2.0] - val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_down)); - val -= 1.0; // from [1 .. 2] to [0 .. 1] - val *= 2.0 - scaler; // from [0 .. 1] to [0 .. 2.0 - scaler] - val += scaler; // from [0 .. 2.0 - scaler] to [scaler .. 2.0] - } else { - // val in [0 .. scaler] - val = m0 / (1.0 + exp((score - 1.0) * fuzzy_width_up)); - val *= scaler; - } - return val; -} - -double ButteraugliFuzzyInverse(double seek) { - double pos = 0; - for (double range = 1.0; range >= 1e-10; range *= 0.5) { - double cur = ButteraugliFuzzyClass(pos); - if (cur < seek) { - pos -= range; - } else { - pos += range; - } - } - return pos; -} - -namespace { - -void ScoreToRgb(double score, double good_threshold, double bad_threshold, - uint8_t rgb[3]) { - double heatmap[12][3] = { - {0, 0, 0}, - {0, 0, 1}, - {0, 1, 1}, - {0, 1, 0}, // Good level - {1, 1, 0}, - {1, 0, 0}, // Bad level - {1, 0, 1}, - {0.5, 0.5, 1.0}, - {1.0, 0.5, 0.5}, // Pastel colors for the very bad quality range. - {1.0, 1.0, 0.5}, - { - 1, 1, 1, - }, - { - 1, 1, 1, - }, - }; - if (score < good_threshold) { - score = (score / good_threshold) * 0.3; - } else if (score < bad_threshold) { - score = 0.3 + - (score - good_threshold) / (bad_threshold - good_threshold) * 0.15; - } else { - score = 0.45 + (score - bad_threshold) / (bad_threshold * 12) * 0.5; - } - static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); - score = std::min(std::max(score * (kTableSize - 1), 0.0), - kTableSize - 2); - int ix = static_cast(score); - double mix = score - ix; - for (int i = 0; i < 3; ++i) { - double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; - rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); - } -} - -} // namespace - -void CreateHeatMapImage(const std::vector& distmap, - double good_threshold, double bad_threshold, - size_t xsize, size_t ysize, - std::vector* heatmap) { - heatmap->resize(3 * xsize * ysize); - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - int px = xsize * y + x; - double d = distmap[px]; - uint8_t* rgb = &(*heatmap)[3 * px]; - ScoreToRgb(d, good_threshold, bad_threshold, rgb); - } - } -} - -} // namespace butteraugli diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h deleted file mode 100755 index 2f5d938..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli.h +++ /dev/null @@ -1,619 +0,0 @@ -// Copyright 2016 Google Inc. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Disclaimer: This is not an official Google product. -// -// Author: Jyrki Alakuijala (jyrki.alakuijala@gmail.com) - -#ifndef BUTTERAUGLI_BUTTERAUGLI_H_ -#define BUTTERAUGLI_BUTTERAUGLI_H_ - -#include -#include -#include -#include -#include -#include -#include -#include - -#define BUTTERAUGLI_ENABLE_CHECKS 0 - -// This is the main interface to butteraugli image similarity -// analysis function. - -namespace butteraugli { - -template -class Image; - -using Image8 = Image; -using ImageF = Image; - -// ButteraugliInterface defines the public interface for butteraugli. -// -// It calculates the difference between rgb0 and rgb1. -// -// rgb0 and rgb1 contain the images. rgb0[c][px] and rgb1[c][px] contains -// the red image for c == 0, green for c == 1, blue for c == 2. Location index -// px is calculated as y * xsize + x. -// -// Value of pixels of images rgb0 and rgb1 need to be represented as raw -// intensity. Most image formats store gamma corrected intensity in pixel -// values. This gamma correction has to be removed, by applying the following -// function: -// butteraugli_val = 255.0 * pow(png_val / 255.0, gamma); -// A typical value of gamma is 2.2. It is usually stored in the image header. -// Take care not to confuse that value with its inverse. The gamma value should -// be always greater than one. -// Butteraugli does not work as intended if the caller does not perform -// gamma correction. -// -// diffmap will contain an image of the size xsize * ysize, containing -// localized differences for values px (indexed with the px the same as rgb0 -// and rgb1). diffvalue will give a global score of similarity. -// -// A diffvalue smaller than kButteraugliGood indicates that images can be -// observed as the same image. -// diffvalue larger than kButteraugliBad indicates that a difference between -// the images can be observed. -// A diffvalue between kButteraugliGood and kButteraugliBad indicates that -// a subtle difference can be observed between the images. -// -// Returns true on success. - -bool ButteraugliInterface(const std::vector &rgb0, - const std::vector &rgb1, - ImageF &diffmap, - double &diffvalue); - -const double kButteraugliQuantLow = 0.26; -const double kButteraugliQuantHigh = 1.454; - -// Converts the butteraugli score into fuzzy class values that are continuous -// at the class boundary. The class boundary location is based on human -// raters, but the slope is arbitrary. Particularly, it does not reflect -// the expectation value of probabilities of the human raters. It is just -// expected that a smoother class boundary will allow for higher-level -// optimization algorithms to work faster. -// -// Returns 2.0 for a perfect match, and 1.0 for 'ok', 0.0 for bad. Because the -// scoring is fuzzy, a butteraugli score of 0.96 would return a class of -// around 1.9. -double ButteraugliFuzzyClass(double score); - -// Input values should be in range 0 (bad) to 2 (good). Use -// kButteraugliNormalization as normalization. -double ButteraugliFuzzyInverse(double seek); - -// Returns a map which can be used for adaptive quantization. Values can -// typically range from kButteraugliQuantLow to kButteraugliQuantHigh. Low -// values require coarse quantization (e.g. near random noise), high values -// require fine quantization (e.g. in smooth bright areas). -bool ButteraugliAdaptiveQuantization(size_t xsize, size_t ysize, - const std::vector > &rgb, std::vector &quant); - -// Implementation details, don't use anything below or your code will -// break in the future. - -#ifdef _MSC_VER -#define BUTTERAUGLI_RESTRICT __restrict -#else -#define BUTTERAUGLI_RESTRICT __restrict__ -#endif - -#ifdef _MSC_VER -#define BUTTERAUGLI_INLINE __forceinline -#else -#define BUTTERAUGLI_INLINE inline -#endif - -#ifdef __clang__ -// Early versions of Clang did not support __builtin_assume_aligned. -#define BUTTERAUGLI_HAS_ASSUME_ALIGNED __has_builtin(__builtin_assume_aligned) -#elif defined(__GNUC__) -#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 1 -#else -#define BUTTERAUGLI_HAS_ASSUME_ALIGNED 0 -#endif - -// Returns a void* pointer which the compiler then assumes is N-byte aligned. -// Example: float* PIK_RESTRICT aligned = (float*)PIK_ASSUME_ALIGNED(in, 32); -// -// The assignment semantics are required by GCC/Clang. ICC provides an in-place -// __assume_aligned, whereas MSVC's __assume appears unsuitable. -#if BUTTERAUGLI_HAS_ASSUME_ALIGNED -#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) __builtin_assume_aligned((ptr), (align)) -#else -#define BUTTERAUGLI_ASSUME_ALIGNED(ptr, align) (ptr) -#endif // BUTTERAUGLI_HAS_ASSUME_ALIGNED - -// Functions that depend on the cache line size. -class CacheAligned { - public: - static constexpr size_t kPointerSize = sizeof(void *); - static constexpr size_t kCacheLineSize = 64; - - // The aligned-return annotation is only allowed on function declarations. - static void *Allocate(const size_t bytes); - static void Free(void *aligned_pointer); -}; - -template -using CacheAlignedUniquePtrT = std::unique_ptr; - -using CacheAlignedUniquePtr = CacheAlignedUniquePtrT; - -template -static inline CacheAlignedUniquePtrT Allocate(const size_t entries) { - return CacheAlignedUniquePtrT( - static_cast( - CacheAligned::Allocate(entries * sizeof(T))), - CacheAligned::Free); -} - -// Returns the smallest integer not less than "amount" that is divisible by -// "multiple", which must be a power of two. -template -static inline size_t Align(const size_t amount) { - static_assert(multiple != 0 && ((multiple & (multiple - 1)) == 0), - "Align<> argument must be a power of two"); - return (amount + multiple - 1) & ~(multiple - 1); -} - -// Single channel, contiguous (cache-aligned) rows separated by padding. -// T must be POD. -// -// Rationale: vectorization benefits from aligned operands - unaligned loads and -// especially stores are expensive when the address crosses cache line -// boundaries. Introducing padding after each row ensures the start of a row is -// aligned, and that row loops can process entire vectors (writes to the padding -// are allowed and ignored). -// -// We prefer a planar representation, where channels are stored as separate -// 2D arrays, because that simplifies vectorization (repeating the same -// operation on multiple adjacent components) without the complexity of a -// hybrid layout (8 R, 8 G, 8 B, ...). In particular, clients can easily iterate -// over all components in a row and Image requires no knowledge of the pixel -// format beyond the component type "T". The downside is that we duplicate the -// xsize/ysize members for each channel. -// -// This image layout could also be achieved with a vector and a row accessor -// function, but a class wrapper with support for "deleter" allows wrapping -// existing memory allocated by clients without copying the pixels. It also -// provides convenient accessors for xsize/ysize, which shortens function -// argument lists. Supports move-construction so it can be stored in containers. -template -class Image { - // Returns cache-aligned row stride, being careful to avoid 2K aliasing. - static size_t BytesPerRow(const size_t xsize) { - // Allow reading one extra AVX-2 vector on the right margin. - const size_t row_size = xsize * sizeof(T) + 32; - const size_t align = CacheAligned::kCacheLineSize; - size_t bytes_per_row = (row_size + align - 1) & ~(align - 1); - // During the lengthy window before writes are committed to memory, CPUs - // guard against read after write hazards by checking the address, but - // only the lower 11 bits. We avoid a false dependency between writes to - // consecutive rows by ensuring their sizes are not multiples of 2 KiB. - if (bytes_per_row % 2048 == 0) { - bytes_per_row += align; - } - return bytes_per_row; - } - - public: - using T = ComponentType; - - Image() : xsize_(0), ysize_(0), bytes_per_row_(0), - bytes_(static_cast(nullptr), Ignore) {} - - Image(const size_t xsize, const size_t ysize) - : xsize_(xsize), - ysize_(ysize), - bytes_per_row_(BytesPerRow(xsize)), - bytes_(Allocate(bytes_per_row_ * ysize)) {} - - Image(const size_t xsize, const size_t ysize, T val) - : xsize_(xsize), - ysize_(ysize), - bytes_per_row_(BytesPerRow(xsize)), - bytes_(Allocate(bytes_per_row_ * ysize)) { - for (size_t y = 0; y < ysize_; ++y) { - T* const BUTTERAUGLI_RESTRICT row = Row(y); - for (int x = 0; x < xsize_; ++x) { - row[x] = val; - } - } - } - - Image(const size_t xsize, const size_t ysize, - uint8_t * const BUTTERAUGLI_RESTRICT bytes, - const size_t bytes_per_row) - : xsize_(xsize), - ysize_(ysize), - bytes_per_row_(bytes_per_row), - bytes_(bytes, Ignore) {} - - // Move constructor (required for returning Image from function) - Image(Image &&other) - : xsize_(other.xsize_), - ysize_(other.ysize_), - bytes_per_row_(other.bytes_per_row_), - bytes_(std::move(other.bytes_)) {} - - // Move assignment (required for std::vector) - Image &operator=(Image &&other) { - xsize_ = other.xsize_; - ysize_ = other.ysize_; - bytes_per_row_ = other.bytes_per_row_; - bytes_ = std::move(other.bytes_); - return *this; - } - - void Swap(Image &other) { - std::swap(xsize_, other.xsize_); - std::swap(ysize_, other.ysize_); - std::swap(bytes_per_row_, other.bytes_per_row_); - std::swap(bytes_, other.bytes_); - } - - // How many pixels. - size_t xsize() const { return xsize_; } - size_t ysize() const { return ysize_; } - - T *const BUTTERAUGLI_RESTRICT Row(const size_t y) { -#ifdef BUTTERAUGLI_ENABLE_CHECKS - if (y >= ysize_) { - printf("Row %zu out of bounds (ysize=%zu)\n", y, ysize_); - abort(); - } -#endif - void *row = bytes_.get() + y * bytes_per_row_; - return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); - } - - const T *const BUTTERAUGLI_RESTRICT Row(const size_t y) const { -#ifdef BUTTERAUGLI_ENABLE_CHECKS - if (y >= ysize_) { - printf("Const row %zu out of bounds (ysize=%zu)\n", y, ysize_); - abort(); - } -#endif - void *row = bytes_.get() + y * bytes_per_row_; - return reinterpret_cast(BUTTERAUGLI_ASSUME_ALIGNED(row, 64)); - } - - // Raw access to byte contents, for interfacing with other libraries. - // Unsigned char instead of char to avoid surprises (sign extension). - uint8_t * const BUTTERAUGLI_RESTRICT bytes() { return bytes_.get(); } - const uint8_t * const BUTTERAUGLI_RESTRICT bytes() const { - return bytes_.get(); - } - size_t bytes_per_row() const { return bytes_per_row_; } - - // Returns number of pixels (some of which are padding) per row. Useful for - // computing other rows via pointer arithmetic. - intptr_t PixelsPerRow() const { - static_assert(CacheAligned::kCacheLineSize % sizeof(T) == 0, - "Padding must be divisible by the pixel size."); - return static_cast(bytes_per_row_ / sizeof(T)); - } - - private: - // Deleter used when bytes are not owned. - static void Ignore(void *ptr) {} - - // (Members are non-const to enable assignment during move-assignment.) - size_t xsize_; // original intended pixels, not including any padding. - size_t ysize_; - size_t bytes_per_row_; // [bytes] including padding. - CacheAlignedUniquePtr bytes_; -}; - -// Returns newly allocated planes of the given dimensions. -template -static inline std::vector> CreatePlanes(const size_t xsize, - const size_t ysize, - const size_t num_planes) { - std::vector> planes; - planes.reserve(num_planes); - for (size_t i = 0; i < num_planes; ++i) { - planes.emplace_back(xsize, ysize); - } - return planes; -} - -// Returns a new image with the same dimensions and pixel values. -template -static inline Image CopyPixels(const Image &other) { - Image copy(other.xsize(), other.ysize()); - const void *BUTTERAUGLI_RESTRICT from = other.bytes(); - void *BUTTERAUGLI_RESTRICT to = copy.bytes(); - memcpy(to, from, other.ysize() * other.bytes_per_row()); - return copy; -} - -// Returns new planes with the same dimensions and pixel values. -template -static inline std::vector> CopyPlanes( - const std::vector> &planes) { - std::vector> copy; - copy.reserve(planes.size()); - for (const Image &plane : planes) { - copy.push_back(CopyPixels(plane)); - } - return copy; -} - -// Compacts a padded image into a preallocated packed vector. -template -static inline void CopyToPacked(const Image &from, std::vector *to) { - const size_t xsize = from.xsize(); - const size_t ysize = from.ysize(); -#if BUTTERAUGLI_ENABLE_CHECKS - if (to->size() < xsize * ysize) { - printf("%zu x %zu exceeds %zu capacity\n", xsize, ysize, to->size()); - abort(); - } -#endif - for (size_t y = 0; y < ysize; ++y) { - const float* const BUTTERAUGLI_RESTRICT row_from = from.Row(y); - float* const BUTTERAUGLI_RESTRICT row_to = to->data() + y * xsize; - memcpy(row_to, row_from, xsize * sizeof(T)); - } -} - -// Expands a packed vector into a preallocated padded image. -template -static inline void CopyFromPacked(const std::vector &from, Image *to) { - const size_t xsize = to->xsize(); - const size_t ysize = to->ysize(); - assert(from.size() == xsize * ysize); - for (size_t y = 0; y < ysize; ++y) { - const float* const BUTTERAUGLI_RESTRICT row_from = - from.data() + y * xsize; - float* const BUTTERAUGLI_RESTRICT row_to = to->Row(y); - memcpy(row_to, row_from, xsize * sizeof(T)); - } -} - -template -static inline std::vector> PlanesFromPacked( - const size_t xsize, const size_t ysize, - const std::vector> &packed) { - std::vector> planes; - planes.reserve(packed.size()); - for (const std::vector &p : packed) { - planes.push_back(Image(xsize, ysize)); - CopyFromPacked(p, &planes.back()); - } - return planes; -} - -template -static inline std::vector> PackedFromPlanes( - const std::vector> &planes) { - assert(!planes.empty()); - const size_t num_pixels = planes[0].xsize() * planes[0].ysize(); - std::vector> packed; - packed.reserve(planes.size()); - for (const Image &image : planes) { - packed.push_back(std::vector(num_pixels)); - CopyToPacked(image, &packed.back()); - } - return packed; -} - -struct PsychoImage { - std::vector uhf; - std::vector hf; - std::vector mf; - std::vector lf; -}; - -class ButteraugliComparator { - public: - ButteraugliComparator(const std::vector& rgb0); - - // Computes the butteraugli map between the original image given in the - // constructor and the distorted image give here. - void Diffmap(const std::vector& rgb1, ImageF& result) const; - - // Same as above, but OpsinDynamicsImage() was already applied. - void DiffmapOpsinDynamicsImage(const std::vector& xyb1, - ImageF& result) const; - - // Same as above, but the frequency decomposition was already applied. - void DiffmapPsychoImage(const PsychoImage& ps1, ImageF &result) const; - - void Mask(std::vector* BUTTERAUGLI_RESTRICT mask, - std::vector* BUTTERAUGLI_RESTRICT mask_dc) const; - - private: - void MaltaDiffMapLF(const ImageF& y0, - const ImageF& y1, - double w_0gt1, - double w_0lt1, - double normalization, - ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; - - void MaltaDiffMap(const ImageF& y0, - const ImageF& y1, - double w_0gt1, - double w_0lt1, - double normalization, - ImageF* BUTTERAUGLI_RESTRICT block_diff_ac) const; - - ImageF CombineChannels(const std::vector& scale_xyb, - const std::vector& scale_xyb_dc, - const std::vector& block_diff_dc, - const std::vector& block_diff_ac) const; - - const size_t xsize_; - const size_t ysize_; - const size_t num_pixels_; - PsychoImage pi0_; -}; - -void ButteraugliDiffmap(const std::vector &rgb0, - const std::vector &rgb1, - ImageF &diffmap); - -double ButteraugliScoreFromDiffmap(const ImageF& distmap); - -// Generate rgb-representation of the distance between two images. -void CreateHeatMapImage(const std::vector &distmap, - double good_threshold, double bad_threshold, - size_t xsize, size_t ysize, - std::vector *heatmap); - -// Compute values of local frequency and dc masking based on the activity -// in the two images. -void Mask(const std::vector& xyb0, - const std::vector& xyb1, - std::vector* BUTTERAUGLI_RESTRICT mask, - std::vector* BUTTERAUGLI_RESTRICT mask_dc); - -template -BUTTERAUGLI_INLINE void RgbToXyb(const V &r, const V &g, const V &b, - V *BUTTERAUGLI_RESTRICT valx, - V *BUTTERAUGLI_RESTRICT valy, - V *BUTTERAUGLI_RESTRICT valb) { - *valx = r - g; - *valy = r + g; - *valb = b; -} - -template -BUTTERAUGLI_INLINE void OpsinAbsorbance(const V &in0, const V &in1, - const V &in2, - V *BUTTERAUGLI_RESTRICT out0, - V *BUTTERAUGLI_RESTRICT out1, - V *BUTTERAUGLI_RESTRICT out2) { - // https://en.wikipedia.org/wiki/Photopsin absorbance modeling. - static const double mixi0 = 0.254462330846; - static const double mixi1 = 0.488238255095; - static const double mixi2 = 0.0635278003854; - static const double mixi3 = 1.01681026909; - static const double mixi4 = 0.195214015766; - static const double mixi5 = 0.568019861857; - static const double mixi6 = 0.0860755536007; - static const double mixi7 = 1.1510118369; - static const double mixi8 = 0.07374607900105684; - static const double mixi9 = 0.06142425304154509; - static const double mixi10 = 0.24416850520714256; - static const double mixi11 = 1.20481945273; - - const V mix0(mixi0); - const V mix1(mixi1); - const V mix2(mixi2); - const V mix3(mixi3); - const V mix4(mixi4); - const V mix5(mixi5); - const V mix6(mixi6); - const V mix7(mixi7); - const V mix8(mixi8); - const V mix9(mixi9); - const V mix10(mixi10); - const V mix11(mixi11); - - *out0 = mix0 * in0 + mix1 * in1 + mix2 * in2 + mix3; - *out1 = mix4 * in0 + mix5 * in1 + mix6 * in2 + mix7; - *out2 = mix8 * in0 + mix9 * in1 + mix10 * in2 + mix11; -} - -std::vector OpsinDynamicsImage(const std::vector& rgb); - -ImageF Blur(const ImageF& in, float sigma, float border_ratio); - -double SimpleGamma(double v); - -double GammaMinArg(); -double GammaMaxArg(); - -// Polynomial evaluation via Clenshaw's scheme (similar to Horner's). -// Template enables compile-time unrolling of the recursion, but must reside -// outside of a class due to the specialization. -template -static inline void ClenshawRecursion(const double x, const double *coefficients, - double *b1, double *b2) { - const double x_b1 = x * (*b1); - const double t = (x_b1 + x_b1) - (*b2) + coefficients[INDEX]; - *b2 = *b1; - *b1 = t; - - ClenshawRecursion(x, coefficients, b1, b2); -} - -// Base case -template <> -inline void ClenshawRecursion<0>(const double x, const double *coefficients, - double *b1, double *b2) { - const double x_b1 = x * (*b1); - // The final iteration differs - no 2 * x_b1 here. - *b1 = x_b1 - (*b2) + coefficients[0]; -} - -// Rational polynomial := dividing two polynomial evaluations. These are easier -// to find than minimax polynomials. -struct RationalPolynomial { - template - static double EvaluatePolynomial(const double x, - const double (&coefficients)[N]) { - double b1 = 0.0; - double b2 = 0.0; - ClenshawRecursion(x, coefficients, &b1, &b2); - return b1; - } - - // Evaluates the polynomial at x (in [min_value, max_value]). - inline double operator()(const double x) const { - // First normalize to [0, 1]. - const double x01 = (x - min_value) / (max_value - min_value); - // And then to [-1, 1] domain of Chebyshev polynomials. - const double xc = 2.0 * x01 - 1.0; - - const double yp = EvaluatePolynomial(xc, p); - const double yq = EvaluatePolynomial(xc, q); - if (yq == 0.0) return 0.0; - return static_cast(yp / yq); - } - - // Domain of the polynomials; they are undefined elsewhere. - double min_value; - double max_value; - - // Coefficients of T_n (Chebyshev polynomials of the first kind). - // Degree 5/5 is a compromise between accuracy (0.1%) and numerical stability. - double p[5 + 1]; - double q[5 + 1]; -}; - -static inline double GammaPolynomial(double value) { - static const RationalPolynomial r = { - 0.971783, 590.188894, - { - 98.7821300963361, 164.273222212631, 92.948112871376, - 33.8165311212688, 6.91626704983562, 0.556380877028234 - }, - { - 1, 1.64339473427892, 0.89392405219969, 0.298947051776379, - 0.0507146002577288, 0.00226495093949756 - }}; - return r(value); -} - -} // namespace butteraugli - -#endif // BUTTERAUGLI_BUTTERAUGLI_H_ diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc b/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc deleted file mode 100755 index f38af1d..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/butteraugli/butteraugli_main.cc +++ /dev/null @@ -1,457 +0,0 @@ -#include -#include -#include -#include -#include "butteraugli/butteraugli.h" - -extern "C" { -#include "png.h" -#include "jpeglib.h" -} - -namespace butteraugli { -namespace { - -// "rgb": cleared and filled with same-sized image planes (one per channel); -// either RGB, or RGBA if the PNG contains an alpha channel. -bool ReadPNG(FILE* f, std::vector* rgb) { - png_structp png_ptr = - png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); - if (!png_ptr) { - return false; - } - - png_infop info_ptr = png_create_info_struct(png_ptr); - if (!info_ptr) { - png_destroy_read_struct(&png_ptr, NULL, NULL); - return false; - } - - if (setjmp(png_jmpbuf(png_ptr)) != 0) { - // Ok we are here because of the setjmp. - png_destroy_read_struct(&png_ptr, &info_ptr, NULL); - return false; - } - - rewind(f); - png_init_io(png_ptr, f); - - // The png_transforms flags are as follows: - // packing == convert 1,2,4 bit images, - // strip == 16 -> 8 bits / channel, - // shift == use sBIT dynamics, and - // expand == palettes -> rgb, grayscale -> 8 bit images, tRNS -> alpha. - const unsigned int png_transforms = - PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND | PNG_TRANSFORM_STRIP_16; - - png_read_png(png_ptr, info_ptr, png_transforms, NULL); - - png_bytep* row_pointers = png_get_rows(png_ptr, info_ptr); - - const int xsize = png_get_image_width(png_ptr, info_ptr); - const int ysize = png_get_image_height(png_ptr, info_ptr); - const int components = png_get_channels(png_ptr, info_ptr); - - *rgb = CreatePlanes(xsize, ysize, 3); - - switch (components) { - case 1: { - // GRAYSCALE - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); - - for (int x = 0; x < xsize; ++x) { - const uint8_t gray = row[x]; - row0[x] = row1[x] = row2[x] = gray; - } - } - break; - } - case 2: { - // GRAYSCALE_ALPHA - rgb->push_back(Image8(xsize, ysize)); - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); - - for (int x = 0; x < xsize; ++x) { - const uint8_t gray = row[2 * x + 0]; - const uint8_t alpha = row[2 * x + 1]; - row0[x] = gray; - row1[x] = gray; - row2[x] = gray; - row3[x] = alpha; - } - } - break; - } - case 3: { - // RGB - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); - - for (int x = 0; x < xsize; ++x) { - row0[x] = row[3 * x + 0]; - row1[x] = row[3 * x + 1]; - row2[x] = row[3 * x + 2]; - } - } - break; - } - case 4: { - // RGBA - rgb->push_back(Image8(xsize, ysize)); - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row = row_pointers[y]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = (*rgb)[0].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = (*rgb)[1].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = (*rgb)[2].Row(y); - uint8_t* const BUTTERAUGLI_RESTRICT row3 = (*rgb)[3].Row(y); - - for (int x = 0; x < xsize; ++x) { - row0[x] = row[4 * x + 0]; - row1[x] = row[4 * x + 1]; - row2[x] = row[4 * x + 2]; - row3[x] = row[4 * x + 3]; - } - } - break; - } - default: - png_destroy_read_struct(&png_ptr, &info_ptr, NULL); - return false; - } - png_destroy_read_struct(&png_ptr, &info_ptr, NULL); - return true; -} - -const double* NewSrgbToLinearTable() { - double* table = new double[256]; - for (int i = 0; i < 256; ++i) { - const double srgb = i / 255.0; - table[i] = - 255.0 * (srgb <= 0.04045 ? srgb / 12.92 - : std::pow((srgb + 0.055) / 1.055, 2.4)); - } - return table; -} - -void jpeg_catch_error(j_common_ptr cinfo) { - (*cinfo->err->output_message) (cinfo); - jmp_buf* jpeg_jmpbuf = (jmp_buf*) cinfo->client_data; - jpeg_destroy(cinfo); - longjmp(*jpeg_jmpbuf, 1); -} - -// "rgb": cleared and filled with same-sized image planes (one per channel); -// either RGB, or RGBA if the PNG contains an alpha channel. -bool ReadJPEG(FILE* f, std::vector* rgb) { - rewind(f); - - struct jpeg_decompress_struct cinfo; - struct jpeg_error_mgr jerr; - cinfo.err = jpeg_std_error(&jerr); - jmp_buf jpeg_jmpbuf; - cinfo.client_data = &jpeg_jmpbuf; - jerr.error_exit = jpeg_catch_error; - if (setjmp(jpeg_jmpbuf)) { - return false; - } - - jpeg_create_decompress(&cinfo); - - jpeg_stdio_src(&cinfo, f); - jpeg_read_header(&cinfo, TRUE); - jpeg_start_decompress(&cinfo); - - int row_stride = cinfo.output_width * cinfo.output_components; - JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray) - ((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1); - - const size_t xsize = cinfo.output_width; - const size_t ysize = cinfo.output_height; - - *rgb = CreatePlanes(xsize, ysize, 3); - - switch (cinfo.out_color_space) { - case JCS_GRAYSCALE: - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - - const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = - (*rgb)[0].Row(cinfo.output_scanline - 1); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = - (*rgb)[1].Row(cinfo.output_scanline - 1); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = - (*rgb)[2].Row(cinfo.output_scanline - 1); - - for (int x = 0; x < xsize; x++) { - const uint8_t gray = row[x]; - row0[x] = row1[x] = row2[x] = gray; - } - } - break; - - case JCS_RGB: - while (cinfo.output_scanline < cinfo.output_height) { - jpeg_read_scanlines(&cinfo, buffer, 1); - - const uint8_t* const BUTTERAUGLI_RESTRICT row = buffer[0]; - uint8_t* const BUTTERAUGLI_RESTRICT row0 = - (*rgb)[0].Row(cinfo.output_scanline - 1); - uint8_t* const BUTTERAUGLI_RESTRICT row1 = - (*rgb)[1].Row(cinfo.output_scanline - 1); - uint8_t* const BUTTERAUGLI_RESTRICT row2 = - (*rgb)[2].Row(cinfo.output_scanline - 1); - - for (int x = 0; x < xsize; x++) { - row0[x] = row[3 * x + 0]; - row1[x] = row[3 * x + 1]; - row2[x] = row[3 * x + 2]; - } - } - break; - - default: - jpeg_destroy_decompress(&cinfo); - return false; - } - - jpeg_finish_decompress(&cinfo); - jpeg_destroy_decompress(&cinfo); - return true; -} - -// Translate R, G, B channels from sRGB to linear space. If an alpha channel -// is present, overlay the image over a black or white background. Overlaying -// is done in the sRGB space; while technically incorrect, this is aligned with -// many other software (web browsers, WebP near lossless). -void FromSrgbToLinear(const std::vector& rgb, - std::vector& linear, int background) { - const size_t xsize = rgb[0].xsize(); - const size_t ysize = rgb[0].ysize(); - static const double* const kSrgbToLinearTable = NewSrgbToLinearTable(); - - if (rgb.size() == 3) { // RGB - for (int c = 0; c < 3; c++) { - linear.push_back(ImageF(xsize, ysize)); - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); - float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); - for (size_t x = 0; x < xsize; x++) { - const int value = row_rgb[x]; - row_linear[x] = kSrgbToLinearTable[value]; - } - } - } - } else { // RGBA - for (int c = 0; c < 3; c++) { - linear.push_back(ImageF(xsize, ysize)); - for (int y = 0; y < ysize; ++y) { - const uint8_t* const BUTTERAUGLI_RESTRICT row_rgb = rgb[c].Row(y); - float* const BUTTERAUGLI_RESTRICT row_linear = linear[c].Row(y); - const uint8_t* const BUTTERAUGLI_RESTRICT row_alpha = rgb[3].Row(y); - for (size_t x = 0; x < xsize; x++) { - int value; - if (row_alpha[x] == 255) { - value = row_rgb[x]; - } else if (row_alpha[x] == 0) { - value = background; - } else { - const int fg_weight = row_alpha[x]; - const int bg_weight = 255 - fg_weight; - value = - (row_rgb[x] * fg_weight + background * bg_weight + 127) / 255; - } - row_linear[x] = kSrgbToLinearTable[value]; - } - } - } - } -} - -std::vector ReadImageOrDie(const char* filename) { - std::vector rgb; - FILE* f = fopen(filename, "rb"); - if (!f) { - fprintf(stderr, "Cannot open %s\n", filename); - exit(1); - } - unsigned char magic[2]; - if (fread(magic, 1, 2, f) != 2) { - fprintf(stderr, "Cannot read from %s\n", filename); - exit(1); - } - if (magic[0] == 0xFF && magic[1] == 0xD8) { - if (!ReadJPEG(f, &rgb)) { - fprintf(stderr, "File %s is a malformed JPEG.\n", filename); - exit(1); - } - } else { - if (!ReadPNG(f, &rgb)) { - fprintf(stderr, "File %s is neither a valid JPEG nor a valid PNG.\n", - filename); - exit(1); - } - } - fclose(f); - return rgb; -} - -static void ScoreToRgb(double score, double good_threshold, - double bad_threshold, uint8_t rgb[3]) { - double heatmap[12][3] = { - { 0, 0, 0 }, - { 0, 0, 1 }, - { 0, 1, 1 }, - { 0, 1, 0 }, // Good level - { 1, 1, 0 }, - { 1, 0, 0 }, // Bad level - { 1, 0, 1 }, - { 0.5, 0.5, 1.0 }, - { 1.0, 0.5, 0.5 }, // Pastel colors for the very bad quality range. - { 1.0, 1.0, 0.5 }, - { 1, 1, 1, }, - { 1, 1, 1, }, - }; - if (score < good_threshold) { - score = (score / good_threshold) * 0.3; - } else if (score < bad_threshold) { - score = 0.3 + (score - good_threshold) / - (bad_threshold - good_threshold) * 0.15; - } else { - score = 0.45 + (score - bad_threshold) / - (bad_threshold * 12) * 0.5; - } - static const int kTableSize = sizeof(heatmap) / sizeof(heatmap[0]); - score = std::min(std::max( - score * (kTableSize - 1), 0.0), kTableSize - 2); - int ix = static_cast(score); - double mix = score - ix; - for (int i = 0; i < 3; ++i) { - double v = mix * heatmap[ix + 1][i] + (1 - mix) * heatmap[ix][i]; - rgb[i] = static_cast(255 * pow(v, 0.5) + 0.5); - } -} - -void CreateHeatMapImage(const ImageF& distmap, double good_threshold, - double bad_threshold, size_t xsize, size_t ysize, - std::vector* heatmap) { - heatmap->resize(3 * xsize * ysize); - for (size_t y = 0; y < ysize; ++y) { - for (size_t x = 0; x < xsize; ++x) { - int px = xsize * y + x; - double d = distmap.Row(y)[x]; - uint8_t* rgb = &(*heatmap)[3 * px]; - ScoreToRgb(d, good_threshold, bad_threshold, rgb); - } - } -} - -// main() function, within butteraugli namespace for convenience. -int Run(int argc, char* argv[]) { - if (argc != 3 && argc != 4) { - fprintf(stderr, - "Usage: %s {image1.(png|jpg|jpeg)} {image2.(png|jpg|jpeg)} " - "[heatmap.ppm]\n", - argv[0]); - return 1; - } - - std::vector rgb1 = ReadImageOrDie(argv[1]); - std::vector rgb2 = ReadImageOrDie(argv[2]); - - if (rgb1.size() != rgb2.size()) { - fprintf(stderr, "Different number of channels: %lu vs %lu\n", rgb1.size(), - rgb2.size()); - exit(1); - } - - for (size_t c = 0; c < rgb1.size(); ++c) { - if (rgb1[c].xsize() != rgb2[c].xsize() || - rgb1[c].ysize() != rgb2[c].ysize()) { - fprintf( - stderr, "The images are not equal in size: (%lu,%lu) vs (%lu,%lu)\n", - rgb1[c].xsize(), rgb2[c].xsize(), rgb1[c].ysize(), rgb2[c].ysize()); - return 1; - } - } - - // TODO: Figure out if it is a good idea to fetch the gamma from the image - // instead of applying sRGB conversion. - std::vector linear1, linear2; - // Overlay the image over a black background. - FromSrgbToLinear(rgb1, linear1, 0); - FromSrgbToLinear(rgb2, linear2, 0); - ImageF diff_map, diff_map_on_white; - double diff_value; - if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map, - diff_value)) { - fprintf(stderr, "Butteraugli comparison failed\n"); - return 1; - } - ImageF* diff_map_ptr = &diff_map; - if (rgb1.size() == 4 || rgb2.size() == 4) { - // If the alpha channel is present, overlay the image over a white - // background as well. - FromSrgbToLinear(rgb1, linear1, 255); - FromSrgbToLinear(rgb2, linear2, 255); - double diff_value_on_white; - if (!butteraugli::ButteraugliInterface(linear1, linear2, diff_map_on_white, - diff_value_on_white)) { - fprintf(stderr, "Butteraugli comparison failed\n"); - return 1; - } - if (diff_value_on_white > diff_value) { - diff_value = diff_value_on_white; - diff_map_ptr = &diff_map_on_white; - } - } - printf("%lf\n", diff_value); - - if (argc == 4) { - const double good_quality = ::butteraugli::ButteraugliFuzzyInverse(1.5); - const double bad_quality = ::butteraugli::ButteraugliFuzzyInverse(0.5); - std::vector rgb; - CreateHeatMapImage(*diff_map_ptr, good_quality, bad_quality, - rgb1[0].xsize(), rgb2[0].ysize(), &rgb); - FILE* const fmap = fopen(argv[3], "wb"); - if (fmap == NULL) { - fprintf(stderr, "Cannot open %s\n", argv[3]); - perror("fopen"); - return 1; - } - bool ok = true; - if (fprintf(fmap, "P6\n%lu %lu\n255\n", - rgb1[0].xsize(), rgb1[0].ysize()) < 0){ - perror("fprintf"); - ok = false; - } - if (ok && fwrite(rgb.data(), 1, rgb.size(), fmap) != rgb.size()) { - perror("fwrite"); - ok = false; - } - if (fclose(fmap) != 0) { - perror("fclose"); - ok = false; - } - if (!ok) return 1; - } - - return 0; -} - -} // namespace -} // namespace butteraugli - -int main(int argc, char** argv) { return butteraugli::Run(argc, argv); } diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD deleted file mode 100755 index 9ff982b..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/png.BUILD +++ /dev/null @@ -1,33 +0,0 @@ -# Description: -# libpng is the official PNG reference library. - -licenses(["notice"]) # BSD/MIT-like license - -cc_library( - name = "png", - srcs = [ - "png.c", - "pngerror.c", - "pngget.c", - "pngmem.c", - "pngpread.c", - "pngread.c", - "pngrio.c", - "pngrtran.c", - "pngrutil.c", - "pngset.c", - "pngtrans.c", - "pngwio.c", - "pngwrite.c", - "pngwtran.c", - "pngwutil.c", - ], - hdrs = [ - "png.h", - "pngconf.h", - ], - includes = ["."], - linkopts = ["-lm"], - visibility = ["//visibility:public"], - deps = ["@zlib_archive//:zlib"], -) diff --git a/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD b/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD deleted file mode 100755 index edb77fd..0000000 --- a/oss-internship-2020/guetzli/third_party/butteraugli/zlib.BUILD +++ /dev/null @@ -1,36 +0,0 @@ -package(default_visibility = ["//visibility:public"]) - -licenses(["notice"]) # BSD/MIT-like license (for zlib) - -cc_library( - name = "zlib", - srcs = [ - "adler32.c", - "compress.c", - "crc32.c", - "crc32.h", - "deflate.c", - "deflate.h", - "gzclose.c", - "gzguts.h", - "gzlib.c", - "gzread.c", - "gzwrite.c", - "infback.c", - "inffast.c", - "inffast.h", - "inffixed.h", - "inflate.c", - "inflate.h", - "inftrees.c", - "inftrees.h", - "trees.c", - "trees.h", - "uncompr.c", - "zconf.h", - "zutil.c", - "zutil.h", - ], - hdrs = ["zlib.h"], - includes = ["."], -) From e99de21e9559cf9e00b5ed975975a74e8f8532ba Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Mon, 17 Aug 2020 13:31:14 -0700 Subject: [PATCH 07/10] Quick update --- oss-internship-2020/guetzli/guetzli_transaction.h | 1 - 1 file changed, 1 deletion(-) diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index be66633..1a7c7d3 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -52,7 +52,6 @@ class GuetzliTransaction : public sapi::Transaction { } private: - //absl::Status Init() override; absl::Status Main() final; absl::Status LinkOutFile(int out_fd) const; From 465cf9c4bb55e46d5a9b2ccceb3f5e49ff9b83e3 Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Wed, 19 Aug 2020 01:21:30 -0700 Subject: [PATCH 08/10] Codestyle fix --- oss-internship-2020/guetzli/README.md | 3 +++ oss-internship-2020/guetzli/guetzli_entry_points.cc | 12 ++++++------ oss-internship-2020/guetzli/guetzli_transaction.cc | 1 + oss-internship-2020/guetzli/guetzli_transaction.h | 7 ++----- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/oss-internship-2020/guetzli/README.md b/oss-internship-2020/guetzli/README.md index 042f165..97b3bc3 100644 --- a/oss-internship-2020/guetzli/README.md +++ b/oss-internship-2020/guetzli/README.md @@ -25,5 +25,8 @@ There are two different sets of unit tests which demonstrate how to use differen * `tests/guetzli_sapi_test.cc` - example usage of Guetzli sandboxed API. * `tests/guetzli_transaction_test.cc` - example usage of Guetzli transaction. +To run tests use following command: +`bazel test ...` + Also, there is an example of custom security policy for your sandbox in `guetzli_sandbox.h` diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc index a36431b..7368928 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.cc +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -154,7 +154,7 @@ sapi::StatusOr ReadPNG(const std::string& data) { xsize = png_get_image_width(png_ptr, info_ptr); ysize = png_get_image_height(png_ptr, info_ptr); - rgb.resize(3 * (xsize) * (ysize)); + rgb.resize(3 * xsize * ysize); const int components = png_get_channels(png_ptr, info_ptr); switch (components) { @@ -162,7 +162,7 @@ sapi::StatusOr ReadPNG(const std::string& data) { // GRAYSCALE for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + uint8_t* row_out = &rgb[3 * y * xsize]; for (int x = 0; x < xsize; ++x) { const uint8_t gray = row_in[x]; row_out[3 * x + 0] = gray; @@ -176,7 +176,7 @@ sapi::StatusOr ReadPNG(const std::string& data) { // GRAYSCALE + ALPHA for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + uint8_t* row_out = &rgb[3 * y * xsize]; for (int x = 0; x < xsize; ++x) { const uint8_t gray = BlendOnBlack(row_in[2 * x], row_in[2 * x + 1]); row_out[3 * x + 0] = gray; @@ -190,8 +190,8 @@ sapi::StatusOr ReadPNG(const std::string& data) { // RGB for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (xsize)]; - memcpy(row_out, row_in, 3 * (xsize)); + uint8_t* row_out = &rgb[3 * y * xsize]; + memcpy(row_out, row_in, 3 * xsize); } break; } @@ -199,7 +199,7 @@ sapi::StatusOr ReadPNG(const std::string& data) { // RGBA for (int y = 0; y < ysize; ++y) { const uint8_t* row_in = row_pointers[y]; - uint8_t* row_out = &(rgb)[3 * y * (xsize)]; + uint8_t* row_out = &rgb[3 * y * xsize]; for (int x = 0; x < xsize; ++x) { const uint8_t alpha = row_in[4 * x + 3]; row_out[3 * x + 0] = BlendOnBlack(row_in[4 * x + 0], alpha); diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc index 64fac51..8fd1db4 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -107,6 +107,7 @@ absl::Status GuetzliTransaction::LinkOutFile(int out_fd) const { std::stringstream path; path << "/proc/self/fd/" << out_fd; + if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, params_.out_file, AT_SYMLINK_FOLLOW) < 0) { std::stringstream error; diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index 1a7c7d3..c9c0d7d 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -42,12 +42,11 @@ struct TransactionParams { // Create a new one for each processing operation class GuetzliTransaction : public sapi::Transaction { public: - GuetzliTransaction(TransactionParams params) + GuetzliTransaction(TransactionParams params, int retry_count = 0) : sapi::Transaction(std::make_unique()) , params_(std::move(params)) { - //TODO: Add retry count as a parameter - sapi::Transaction::set_retry_count(kDefaultTransactionRetryCount); + sapi::Transaction::set_retry_count(retry_count); sapi::Transaction::SetTimeLimit(0); // Infinite time limit } @@ -59,8 +58,6 @@ class GuetzliTransaction : public sapi::Transaction { const TransactionParams params_; ImageType image_type_ = ImageType::kJpeg; - - static const int kDefaultTransactionRetryCount = 0; }; } // namespace sandbox From 9803d0549fac835093d7a743c217d5fe1362fb35 Mon Sep 17 00:00:00 2001 From: Bohdan Tyshchenko Date: Mon, 31 Aug 2020 02:19:00 -0700 Subject: [PATCH 09/10] Changed README, Bazel deps and different parts of code according to the review --- oss-internship-2020/guetzli/BUILD.bazel | 2 +- oss-internship-2020/guetzli/README.md | 10 ++-- oss-internship-2020/guetzli/WORKSPACE | 31 +++++----- .../guetzli/external/butteraugli.BUILD | 4 +- .../guetzli/external/guetzli.BUILD | 4 +- .../guetzli/external/jpeg.BUILD | 1 - .../guetzli/external/png.BUILD | 2 +- .../guetzli/external/zlib.BUILD | 40 +++++++++---- .../guetzli/guetzli_entry_points.cc | 12 ++-- oss-internship-2020/guetzli/guetzli_sandbox.h | 6 +- .../guetzli/guetzli_sandboxed.cc | 1 + .../guetzli/guetzli_transaction.cc | 40 +++++-------- .../guetzli/guetzli_transaction.h | 16 +++-- oss-internship-2020/guetzli/tests/BUILD.bazel | 10 ++-- .../guetzli/tests/guetzli_sapi_test.cc | 22 +++---- .../guetzli/tests/guetzli_transaction_test.cc | 58 +++++++++---------- 16 files changed, 132 insertions(+), 127 deletions(-) diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel index 88a6da2..c1c40b2 100644 --- a/oss-internship-2020/guetzli/BUILD.bazel +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -licenses(["unencumbered"]) # code authored by Google +licenses(["notice"]) load( "@com_google_sandboxed_api//sandboxed_api/bazel:sapi.bzl", diff --git a/oss-internship-2020/guetzli/README.md b/oss-internship-2020/guetzli/README.md index 97b3bc3..5fa8412 100644 --- a/oss-internship-2020/guetzli/README.md +++ b/oss-internship-2020/guetzli/README.md @@ -1,11 +1,11 @@ -# Guetzli Sandboxed +# Guetzli Sandbox This is an example implementation of a sandbox for the [Guetzli](https://github.com/google/guetzli) library using [Sandboxed API](https://github.com/google/sandboxed-api). Please read Guetzli's [documentation](https://github.com/google/guetzli#introduction) to learn more about it. ## Implementation details -Because Guetzli provides C++ API and SAPI requires functions to be `extern "C"` a wrapper library has been written for the compatibility. SAPI provides a Transaction class, which is a convenient way to create a wrapper for your sandboxed API that handles internal errors. Original Guetzli has command-line utility to encode images, so fully compatible utility that uses sandboxed Guetzli is provided. +Because Guetzli provides a C++ API and SAPI requires functions to be `extern "C"`, a wrapper library has been written for the compatibility. SAPI provides a Transaction class, which is a convenient way to create a wrapper for your sandboxed API that handles internal errors. The original Guetzli has a command-line utility to encode images, so a fully compatible utility that uses sandboxed Guetzli is provided. -Wrapper around Guetzli uses file descriptors to pass data to the sandbox. This approach restricts the sandbox from using open() syscall and also helps to prevent making copies of data, because you need to synchronize it between processes. +The wrapper around Guetzli uses file descriptors to pass data to the sandbox. This approach restricts the sandbox from using the `open()` syscall and also helps to prevent making copies of data, because you need to synchronize it between processes. ## Build Guetzli Sandboxed Right now Sandboxed API support only Linux systems, so you need one to build it. Guetzli sandboxed uses [Bazel](https://bazel.build/) as a build system so you need to [install it](https://docs.bazel.build/versions/3.4.0/install.html) before building. @@ -13,7 +13,7 @@ Right now Sandboxed API support only Linux systems, so you need one to build it. To build Guetzli sandboxed encoding utility you can use this command: `bazel build //:guetzli_sandboxed` -Than you can use it in this way: +Then you can use it in this way: ``` guetzli_sandboxed [--quality Q] [--verbose] original.png output.jpg guetzli_sandboxed [--quality Q] [--verbose] original.jpg output.jpg @@ -25,7 +25,7 @@ There are two different sets of unit tests which demonstrate how to use differen * `tests/guetzli_sapi_test.cc` - example usage of Guetzli sandboxed API. * `tests/guetzli_transaction_test.cc` - example usage of Guetzli transaction. -To run tests use following command: +To run tests use the following command: `bazel test ...` Also, there is an example of custom security policy for your sandbox in diff --git a/oss-internship-2020/guetzli/WORKSPACE b/oss-internship-2020/guetzli/WORKSPACE index 7f2b667..c952173 100644 --- a/oss-internship-2020/guetzli/WORKSPACE +++ b/oss-internship-2020/guetzli/WORKSPACE @@ -71,14 +71,6 @@ http_archive( url = "http://github.com/glennrp/libpng/archive/v1.2.57.zip", ) -http_archive( - name = "zlib_archive", - build_file = "zlib.BUILD", - sha256 = "8d7e9f698ce48787b6e1c67e6bff79e487303e66077e25cb9784ac8835978017", - strip_prefix = "zlib-1.2.10", - url = "http://zlib.net/fossils/zlib-1.2.10.tar.gz", -) - http_archive( name = "jpeg_archive", url = "http://www.ijg.org/files/jpegsrc.v9b.tar.gz", @@ -87,9 +79,22 @@ http_archive( build_file = "jpeg.BUILD", ) -maybe( - git_repository, - name = "googletest", - remote = "https://github.com/google/googletest", - tag = "release-1.10.0", +http_archive( + name = "net_zlib", + build_file = "zlib.BUILD", + sha256 = "c3e5e9fdd5004dcb542feda5ee4f0ff0744628baf8ed2dd5d66f8ca1197cb1a1", # 2020-04-23 + strip_prefix = "zlib-1.2.11", + urls = [ + "https://mirror.bazel.build/zlib.net/zlib-1.2.11.tar.gz", + "https://www.zlib.net/zlib-1.2.11.tar.gz", + ], ) + +# GoogleTest/GoogleMock +maybe( + http_archive, + name = "com_google_googletest", + sha256 = "a6ab7c7d6fd4dd727f6012b5d85d71a73d3aa1274f529ecd4ad84eb9ec4ff767", # 2020-04-16 + strip_prefix = "googletest-dcc92d0ab6c4ce022162a23566d44f673251eee4", + urls = ["https://github.com/google/googletest/archive/dcc92d0ab6c4ce022162a23566d44f673251eee4.zip"], +) \ No newline at end of file diff --git a/oss-internship-2020/guetzli/external/butteraugli.BUILD b/oss-internship-2020/guetzli/external/butteraugli.BUILD index 9058593..d61eb35 100644 --- a/oss-internship-2020/guetzli/external/butteraugli.BUILD +++ b/oss-internship-2020/guetzli/external/butteraugli.BUILD @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -licenses(["unencumbered"]) # code authored by Google +licenses(["notice"]) cc_library( - name = "butteraugli_lib", + name = "butteraugli", srcs = [ "butteraugli/butteraugli.cc", "butteraugli/butteraugli.h", diff --git a/oss-internship-2020/guetzli/external/guetzli.BUILD b/oss-internship-2020/guetzli/external/guetzli.BUILD index d2d4f32..454ec62 100644 --- a/oss-internship-2020/guetzli/external/guetzli.BUILD +++ b/oss-internship-2020/guetzli/external/guetzli.BUILD @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -licenses(["unencumbered"]) # code authored by Google +licenses(["notice"]) cc_library( name = "guetzli_lib", @@ -27,6 +27,6 @@ cc_library( copts = [ "-Wno-sign-compare" ], visibility= [ "//visibility:public" ], deps = [ - "@butteraugli//:butteraugli_lib", + "@butteraugli//:butteraugli", ], ) diff --git a/oss-internship-2020/guetzli/external/jpeg.BUILD b/oss-internship-2020/guetzli/external/jpeg.BUILD index 71eb87b..92c9ddc 100644 --- a/oss-internship-2020/guetzli/external/jpeg.BUILD +++ b/oss-internship-2020/guetzli/external/jpeg.BUILD @@ -1,4 +1,3 @@ - # Description: # The Independent JPEG Group's JPEG runtime library. diff --git a/oss-internship-2020/guetzli/external/png.BUILD b/oss-internship-2020/guetzli/external/png.BUILD index 9ff982b..4b061f5 100644 --- a/oss-internship-2020/guetzli/external/png.BUILD +++ b/oss-internship-2020/guetzli/external/png.BUILD @@ -29,5 +29,5 @@ cc_library( includes = ["."], linkopts = ["-lm"], visibility = ["//visibility:public"], - deps = ["@zlib_archive//:zlib"], + deps = ["@net_zlib//:zlib"], ) diff --git a/oss-internship-2020/guetzli/external/zlib.BUILD b/oss-internship-2020/guetzli/external/zlib.BUILD index edb77fd..ec4fff0 100644 --- a/oss-internship-2020/guetzli/external/zlib.BUILD +++ b/oss-internship-2020/guetzli/external/zlib.BUILD @@ -1,6 +1,18 @@ -package(default_visibility = ["//visibility:public"]) +# Copyright 2019 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -licenses(["notice"]) # BSD/MIT-like license (for zlib) +licenses(["notice"]) cc_library( name = "zlib", @@ -8,29 +20,37 @@ cc_library( "adler32.c", "compress.c", "crc32.c", - "crc32.h", "deflate.c", - "deflate.h", "gzclose.c", - "gzguts.h", "gzlib.c", "gzread.c", "gzwrite.c", "infback.c", "inffast.c", - "inffast.h", - "inffixed.h", "inflate.c", "inflate.h", "inftrees.c", - "inftrees.h", "trees.c", "trees.h", "uncompr.c", - "zconf.h", "zutil.c", - "zutil.h", ], hdrs = ["zlib.h"], + # Use -Dverbose=-1 to turn off zlib's trace logging. (#3280) + copts = [ + "-w", + "-Dverbose=-1", + ], includes = ["."], + textual_hdrs = [ + "crc32.h", + "deflate.h", + "gzguts.h", + "inffast.h", + "inffixed.h", + "inftrees.h", + "zconf.h", + "zutil.h", + ], + visibility = ["//visibility:public"], ) diff --git a/oss-internship-2020/guetzli/guetzli_entry_points.cc b/oss-internship-2020/guetzli/guetzli_entry_points.cc index 7368928..ea8dbcd 100644 --- a/oss-internship-2020/guetzli/guetzli_entry_points.cc +++ b/oss-internship-2020/guetzli/guetzli_entry_points.cc @@ -75,8 +75,8 @@ sapi::StatusOr ReadFromFd(int fd) { } sapi::StatusOr PrepareDataForProcessing( - const ProcessingParams* processing_params) { - sapi::StatusOr input = ReadFromFd(processing_params->remote_fd); + const ProcessingParams& processing_params) { + sapi::StatusOr input = ReadFromFd(processing_params.remote_fd); if (!input.ok()) { return input.status(); @@ -84,11 +84,11 @@ sapi::StatusOr PrepareDataForProcessing( guetzli::Params guetzli_params; guetzli_params.butteraugli_target = static_cast( - guetzli::ButteraugliScoreForQuality(processing_params->quality)); + guetzli::ButteraugliScoreForQuality(processing_params.quality)); guetzli::ProcessStats stats; - if (processing_params->verbose) { + if (processing_params.verbose) { stats.debug_output_file = stderr; } @@ -234,7 +234,7 @@ bool CheckMemoryLimitExceeded(int memlimit_mb, int xsize, int ysize) { extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, sapi::LenValStruct* output) { - auto processing_data = PrepareDataForProcessing(processing_params); + auto processing_data = PrepareDataForProcessing(*processing_params); if (!processing_data.ok()) { std::cerr << processing_data.status().ToString() << std::endl; @@ -269,7 +269,7 @@ extern "C" bool ProcessJpeg(const ProcessingParams* processing_params, extern "C" bool ProcessRgb(const ProcessingParams* processing_params, sapi::LenValStruct* output) { - auto processing_data = PrepareDataForProcessing(processing_params); + auto processing_data = PrepareDataForProcessing(*processing_params); if (!processing_data.ok()) { std::cerr << processing_data.status().ToString() << std::endl; diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h index d1da1f8..712b012 100644 --- a/oss-internship-2020/guetzli/guetzli_sandbox.h +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -19,8 +19,7 @@ #include "guetzli_sapi.sapi.h" -namespace guetzli { -namespace sandbox { +namespace guetzli::sandbox { class GuetzliSapiSandbox : public GuetzliSandbox { public: @@ -43,7 +42,6 @@ class GuetzliSapiSandbox : public GuetzliSandbox { } }; -} // namespace sandbox -} // namespace guetzli +} // namespace guetzli::sandbox #endif // GUETZLI_SANDBOXED_GUETZLI_SANDBOX_H_ diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc index 6628f3e..6c53bf3 100644 --- a/oss-internship-2020/guetzli/guetzli_sandboxed.cc +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -19,6 +19,7 @@ #include #include +#include #include "sandboxed_api/sandbox2/util/fileops.h" #include "sandboxed_api/util/statusor.h" diff --git a/oss-internship-2020/guetzli/guetzli_transaction.cc b/oss-internship-2020/guetzli/guetzli_transaction.cc index 8fd1db4..deaa3e9 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction.cc @@ -22,8 +22,7 @@ #include #include -namespace guetzli { -namespace sandbox { +namespace guetzli::sandbox { absl::Status GuetzliTransaction::Main() { sapi::v::Fd in_fd(open(params_.in_file, O_RDONLY)); @@ -53,22 +52,17 @@ absl::Status GuetzliTransaction::Main() { }; auto result = image_type_ == ImageType::kJpeg ? - api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()) : - api.ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); + api.ProcessJpeg(processing_params.PtrBefore(), output.PtrBefore()) : + api.ProcessRgb(processing_params.PtrBefore(), output.PtrBefore()); if (!result.value_or(false)) { - std::stringstream error_stream; - error_stream << "Error processing " - << (image_type_ == ImageType::kJpeg ? "jpeg" : "rgb") << " data" - << std::endl; - return absl::FailedPreconditionError( - error_stream.str() + absl::StrCat("Error processing ", + (image_type_ == ImageType::kJpeg ? "jpeg" : "rgb"), " data") ); } sapi::v::Fd out_fd(open(".", O_TMPFILE | O_RDWR, S_IRUSR | S_IWUSR)); - if (out_fd.GetValue() < 0) { return absl::FailedPreconditionError( "Error creating temp output file" @@ -83,7 +77,7 @@ absl::Status GuetzliTransaction::Main() { } auto write_result = api.WriteDataToFd(out_fd.GetRemoteFd(), - output.PtrBefore()); + output.PtrNone()); if (!write_result.value_or(false)) { return absl::FailedPreconditionError( @@ -99,20 +93,19 @@ absl::Status GuetzliTransaction::Main() { absl::Status GuetzliTransaction::LinkOutFile(int out_fd) const { if (access(params_.out_file, F_OK) != -1) { if (remove(params_.out_file) < 0) { - std::stringstream error; - error << "Error deleting existing output file: " << params_.out_file; - return absl::FailedPreconditionError(error.str()); + return absl::FailedPreconditionError( + absl::StrCat("Error deleting existing output file: ", params_.out_file) + ); } - } + } - std::stringstream path; - path << "/proc/self/fd/" << out_fd; + std::string path = absl::StrCat("/proc/self/fd/", out_fd); - if (linkat(AT_FDCWD, path.str().c_str(), AT_FDCWD, params_.out_file, + if (linkat(AT_FDCWD, path.c_str(), AT_FDCWD, params_.out_file, AT_SYMLINK_FOLLOW) < 0) { - std::stringstream error; - error << "Error linking: " << params_.out_file; - return absl::FailedPreconditionError(error.str()); + return absl::FailedPreconditionError( + absl::StrCat("Error linking: ", params_.out_file) + ); } return absl::OkStatus(); @@ -140,5 +133,4 @@ sapi::StatusOr GuetzliTransaction::GetImageTypeFromFd(int fd) const { ImageType::kPng : ImageType::kJpeg; } -} // namespace sandbox -} // namespace guetzli +} // namespace guetzli::sandbox diff --git a/oss-internship-2020/guetzli/guetzli_transaction.h b/oss-internship-2020/guetzli/guetzli_transaction.h index c9c0d7d..4e5b25c 100644 --- a/oss-internship-2020/guetzli/guetzli_transaction.h +++ b/oss-internship-2020/guetzli/guetzli_transaction.h @@ -22,8 +22,7 @@ #include "guetzli_sandbox.h" -namespace guetzli { -namespace sandbox { +namespace guetzli::sandbox { enum class ImageType { kJpeg, @@ -42,12 +41,12 @@ struct TransactionParams { // Create a new one for each processing operation class GuetzliTransaction : public sapi::Transaction { public: - GuetzliTransaction(TransactionParams params, int retry_count = 0) - : sapi::Transaction(std::make_unique()) - , params_(std::move(params)) + explicit GuetzliTransaction(TransactionParams params, int retry_count = 0) + : sapi::Transaction(std::make_unique()), + params_(std::move(params)) { - sapi::Transaction::set_retry_count(retry_count); - sapi::Transaction::SetTimeLimit(0); // Infinite time limit + set_retry_count(retry_count); + SetTimeLimit(absl::InfiniteDuration()); } private: @@ -60,7 +59,6 @@ class GuetzliTransaction : public sapi::Transaction { ImageType image_type_ = ImageType::kJpeg; }; -} // namespace sandbox -} // namespace guetzli +} // namespace guetzli::sandbox #endif // GUETZLI_SANDBOXED_GUETZLI_TRANSACTION_H_ diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel index a59237e..36f618d 100644 --- a/oss-internship-2020/guetzli/tests/BUILD.bazel +++ b/oss-internship-2020/guetzli/tests/BUILD.bazel @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -licenses(["unencumbered"]) # code authored by Google +licenses(["notice"]) cc_test( name = "transaction_tests", @@ -20,10 +20,10 @@ cc_test( visibility=["//visibility:public"], deps = [ "//:guetzli_sapi", - "@googletest//:gtest_main" + "@com_google_googletest//:gtest_main", ], size = "large", - data = glob(["testdata/*"]) + data = glob(["testdata/*"]), ) cc_test( @@ -32,8 +32,8 @@ cc_test( visibility=["//visibility:public"], deps = [ "//:guetzli_sapi", - "@googletest//:gtest_main" + "@com_google_googletest//:gtest_main", ], size = "large", - data = glob(["testdata/*"]) + data = glob(["testdata/*"]), ) diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc index 66f2ef6..433c4a1 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc @@ -28,24 +28,22 @@ #include "guetzli_sandbox.h" -namespace guetzli { -namespace sandbox { -namespace tests { +namespace guetzli::sandbox::tests { namespace { -constexpr const char* kInPngFilename = "bees.png"; -constexpr const char* kInJpegFilename = "nature.jpg"; -constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; -constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; +constexpr absl::string_view kInPngFilename = "bees.png"; +constexpr absl::string_view kInJpegFilename = "nature.jpg"; +constexpr absl::string_view kPngReferenceFilename = "bees_reference.jpg"; +constexpr absl::string_view kJpegReferenceFIlename = "nature_reference.jpg"; constexpr int kDefaultQualityTarget = 95; constexpr int kDefaultMemlimitMb = 6000; -constexpr const char* kRelativePathToTestdata = +constexpr absl::string_view kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; -std::string GetPathToInputFile(const char* filename) { +std::string GetPathToInputFile(absl::string_view filename) { return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); } @@ -67,7 +65,7 @@ class GuetzliSapiTest : public ::testing::Test { protected: void SetUp() override { sandbox_ = std::make_unique(); - sandbox_->Init().IgnoreError(); + ASSERT_EQ(sandbox_->Init(), absl::OkStatus()); api_ = std::make_unique(sandbox_.get()); } @@ -129,6 +127,4 @@ TEST_F(GuetzliSapiTest, ProcessJpeg) { << "Processed data doesn't match reference output"; } -} // namespace tests -} // namespace sandbox -} // namespace guetzli +} // namespace guetzli::sandbox::tests diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc index 4b33b61..875b328 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc +++ b/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc @@ -26,18 +26,16 @@ #include "guetzli_transaction.h" -namespace guetzli { -namespace sandbox { -namespace tests { +namespace guetzli::sandbox::tests { namespace { -constexpr const char* kInPngFilename = "bees.png"; -constexpr const char* kInJpegFilename = "nature.jpg"; -constexpr const char* kOutJpegFilename = "out_jpeg.jpg"; -constexpr const char* kOutPngFilename = "out_png.png"; -constexpr const char* kPngReferenceFilename = "bees_reference.jpg"; -constexpr const char* kJpegReferenceFIlename = "nature_reference.jpg"; +constexpr absl::string_view kInPngFilename = "bees.png"; +constexpr absl::string_view kInJpegFilename = "nature.jpg"; +constexpr absl::string_view kOutJpegFilename = "out_jpeg.jpg"; +constexpr absl::string_view kOutPngFilename = "out_png.png"; +constexpr absl::string_view kPngReferenceFilename = "bees_reference.jpg"; +constexpr absl::string_view kJpegReferenceFIlename = "nature_reference.jpg"; constexpr int kPngExpectedSize = 38'625; constexpr int kJpegExpectedSize = 10'816; @@ -45,10 +43,10 @@ constexpr int kJpegExpectedSize = 10'816; constexpr int kDefaultQualityTarget = 95; constexpr int kDefaultMemlimitMb = 6000; -constexpr const char* kRelativePathToTestdata = +constexpr absl::string_view kRelativePathToTestdata = "/guetzli_sandboxed/tests/testdata/"; -std::string GetPathToFile(const char* filename) { +std::string GetPathToFile(absl::string_view filename) { return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); } @@ -99,26 +97,26 @@ TEST(GuetzliTransactionTest, TestTransactionJpg) { }; { GuetzliTransaction transaction(std::move(params)); - auto result = transaction.Run(); + absl::Status result = transaction.Run(); ASSERT_TRUE(result.ok()) << result.ToString(); } - auto reference_data = ReadFromFile(GetPathToFile(kJpegReferenceFIlename)); + std::string reference_data = ReadFromFile( + GetPathToFile(kJpegReferenceFIlename)); FileRemover file_remover(out_path.c_str()); ASSERT_TRUE(file_remover.get() != -1) << "Error opening output file"; - auto output_size = lseek(file_remover.get(), 0, SEEK_END); + off_t output_size = lseek(file_remover.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; ASSERT_EQ(lseek(file_remover.get(), 0, SEEK_SET), 0) << "Error repositioning out file"; - std::unique_ptr buf(new char[output_size]); - auto status = read(file_remover.get(), buf.get(), output_size); + std::string output; + output.resize(output_size); + ssize_t status = read(file_remover.get(), output.data(), output_size); ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; - ASSERT_TRUE( - std::equal(buf.get(), buf.get() + output_size, reference_data.begin())) - << "Returned data doesn't match reference"; + ASSERT_EQ(output, reference_data) << "Returned data doesn't match reference"; } TEST(GuetzliTransactionTest, TestTransactionPng) { @@ -134,28 +132,26 @@ TEST(GuetzliTransactionTest, TestTransactionPng) { }; { GuetzliTransaction transaction(std::move(params)); - auto result = transaction.Run(); + absl::Status result = transaction.Run(); ASSERT_TRUE(result.ok()) << result.ToString(); } - auto reference_data = ReadFromFile(GetPathToFile(kPngReferenceFilename)); + std::string reference_data = ReadFromFile( + GetPathToFile(kPngReferenceFilename)); FileRemover file_remover(out_path.c_str()); ASSERT_TRUE(file_remover.get() != -1) << "Error opening output file"; - auto output_size = lseek(file_remover.get(), 0, SEEK_END); + off_t output_size = lseek(file_remover.get(), 0, SEEK_END); ASSERT_EQ(reference_data.size(), output_size) << "Different sizes of reference and returned data"; ASSERT_EQ(lseek(file_remover.get(), 0, SEEK_SET), 0) << "Error repositioning out file"; - - std::unique_ptr buf(new char[output_size]); - auto status = read(file_remover.get(), buf.get(), output_size); + + std::string output; + output.resize(output_size); + ssize_t status = read(file_remover.get(), output.data(), output_size); ASSERT_EQ(status, output_size) << "Error reading data from temp output file"; - ASSERT_TRUE( - std::equal(buf.get(), buf.get() + output_size, reference_data.begin())) - << "Returned data doesn't match refernce"; + ASSERT_EQ(output, reference_data) << "Returned data doesn't match refernce"; } -} // namespace tests -} // namespace sandbox -} // namespace guetzli +} // namespace guetzli::sandbox::tests From e8a15ea151078cf83477afc67aab465485f7d57b Mon Sep 17 00:00:00 2001 From: Bohdan Date: Sun, 13 Sep 2020 18:20:10 +0300 Subject: [PATCH 10/10] Moved tests to root folder, removed unused headers --- oss-internship-2020/guetzli/BUILD.bazel | 24 +++++++++++ oss-internship-2020/guetzli/guetzli_sandbox.h | 2 +- .../guetzli/guetzli_sandboxed.cc | 6 --- .../guetzli/{tests => }/guetzli_sapi_test.cc | 6 +-- .../{tests => }/guetzli_transaction_test.cc | 2 +- .../guetzli/{tests => }/testdata/bees.png | Bin .../{tests => }/testdata/bees_reference.jpg | Bin .../guetzli/{tests => }/testdata/nature.jpg | Bin .../{tests => }/testdata/nature_reference.jpg | Bin oss-internship-2020/guetzli/tests/BUILD.bazel | 39 ------------------ 10 files changed, 29 insertions(+), 50 deletions(-) rename oss-internship-2020/guetzli/{tests => }/guetzli_sapi_test.cc (95%) rename oss-internship-2020/guetzli/{tests => }/guetzli_transaction_test.cc (99%) rename oss-internship-2020/guetzli/{tests => }/testdata/bees.png (100%) rename oss-internship-2020/guetzli/{tests => }/testdata/bees_reference.jpg (100%) rename oss-internship-2020/guetzli/{tests => }/testdata/nature.jpg (100%) rename oss-internship-2020/guetzli/{tests => }/testdata/nature_reference.jpg (100%) delete mode 100644 oss-internship-2020/guetzli/tests/BUILD.bazel diff --git a/oss-internship-2020/guetzli/BUILD.bazel b/oss-internship-2020/guetzli/BUILD.bazel index c1c40b2..d28eca9 100644 --- a/oss-internship-2020/guetzli/BUILD.bazel +++ b/oss-internship-2020/guetzli/BUILD.bazel @@ -54,3 +54,27 @@ cc_binary( ":guetzli_sapi", ], ) + +cc_test( + name = "transaction_tests", + srcs = ["guetzli_transaction_test.cc"], + visibility=["//visibility:public"], + deps = [ + "//:guetzli_sapi", + "@com_google_googletest//:gtest_main", + ], + size = "large", + data = glob(["testdata/*"]), +) + +cc_test( + name = "sapi_lib_tests", + srcs = ["guetzli_sapi_test.cc"], + visibility=["//visibility:public"], + deps = [ + "//:guetzli_sapi", + "@com_google_googletest//:gtest_main", + ], + size = "large", + data = glob(["testdata/*"]), +) diff --git a/oss-internship-2020/guetzli/guetzli_sandbox.h b/oss-internship-2020/guetzli/guetzli_sandbox.h index 712b012..4793d24 100644 --- a/oss-internship-2020/guetzli/guetzli_sandbox.h +++ b/oss-internship-2020/guetzli/guetzli_sandbox.h @@ -22,7 +22,7 @@ namespace guetzli::sandbox { class GuetzliSapiSandbox : public GuetzliSandbox { - public: + public: std::unique_ptr ModifyPolicy( sandbox2::PolicyBuilder*) override { diff --git a/oss-internship-2020/guetzli/guetzli_sandboxed.cc b/oss-internship-2020/guetzli/guetzli_sandboxed.cc index 6c53bf3..ec5723c 100644 --- a/oss-internship-2020/guetzli/guetzli_sandboxed.cc +++ b/oss-internship-2020/guetzli/guetzli_sandboxed.cc @@ -12,12 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include -#include -#include -#include -#include - #include #include diff --git a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc b/oss-internship-2020/guetzli/guetzli_sapi_test.cc similarity index 95% rename from oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc rename to oss-internship-2020/guetzli/guetzli_sapi_test.cc index 433c4a1..03cb587 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_sapi_test.cc +++ b/oss-internship-2020/guetzli/guetzli_sapi_test.cc @@ -41,7 +41,7 @@ constexpr int kDefaultQualityTarget = 95; constexpr int kDefaultMemlimitMb = 6000; constexpr absl::string_view kRelativePathToTestdata = - "/guetzli_sandboxed/tests/testdata/"; + "/guetzli_sandboxed/testdata/"; std::string GetPathToInputFile(absl::string_view filename) { return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); @@ -88,7 +88,7 @@ TEST_F(GuetzliSapiTest, ProcessRGB) { kDefaultMemlimitMb }; sapi::v::LenVal output(0); - auto processing_result = api_->ProcessRgb(processing_params.PtrBefore(), + sapi::StatusOr processing_result = api_->ProcessRgb(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing rgb data"; std::string reference_data = @@ -115,7 +115,7 @@ TEST_F(GuetzliSapiTest, ProcessJpeg) { kDefaultMemlimitMb }; sapi::v::LenVal output(0); - auto processing_result = api_->ProcessJpeg(processing_params.PtrBefore(), + sapi::StatusOr processing_result = api_->ProcessJpeg(processing_params.PtrBefore(), output.PtrBoth()); ASSERT_TRUE(processing_result.value_or(false)) << "Error processing jpg data"; std::string reference_data = diff --git a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc b/oss-internship-2020/guetzli/guetzli_transaction_test.cc similarity index 99% rename from oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc rename to oss-internship-2020/guetzli/guetzli_transaction_test.cc index 875b328..f723c5c 100644 --- a/oss-internship-2020/guetzli/tests/guetzli_transaction_test.cc +++ b/oss-internship-2020/guetzli/guetzli_transaction_test.cc @@ -44,7 +44,7 @@ constexpr int kDefaultQualityTarget = 95; constexpr int kDefaultMemlimitMb = 6000; constexpr absl::string_view kRelativePathToTestdata = - "/guetzli_sandboxed/tests/testdata/"; + "/guetzli_sandboxed/testdata/"; std::string GetPathToFile(absl::string_view filename) { return absl::StrCat(getenv("TEST_SRCDIR"), kRelativePathToTestdata, filename); diff --git a/oss-internship-2020/guetzli/tests/testdata/bees.png b/oss-internship-2020/guetzli/testdata/bees.png similarity index 100% rename from oss-internship-2020/guetzli/tests/testdata/bees.png rename to oss-internship-2020/guetzli/testdata/bees.png diff --git a/oss-internship-2020/guetzli/tests/testdata/bees_reference.jpg b/oss-internship-2020/guetzli/testdata/bees_reference.jpg similarity index 100% rename from oss-internship-2020/guetzli/tests/testdata/bees_reference.jpg rename to oss-internship-2020/guetzli/testdata/bees_reference.jpg diff --git a/oss-internship-2020/guetzli/tests/testdata/nature.jpg b/oss-internship-2020/guetzli/testdata/nature.jpg similarity index 100% rename from oss-internship-2020/guetzli/tests/testdata/nature.jpg rename to oss-internship-2020/guetzli/testdata/nature.jpg diff --git a/oss-internship-2020/guetzli/tests/testdata/nature_reference.jpg b/oss-internship-2020/guetzli/testdata/nature_reference.jpg similarity index 100% rename from oss-internship-2020/guetzli/tests/testdata/nature_reference.jpg rename to oss-internship-2020/guetzli/testdata/nature_reference.jpg diff --git a/oss-internship-2020/guetzli/tests/BUILD.bazel b/oss-internship-2020/guetzli/tests/BUILD.bazel deleted file mode 100644 index 36f618d..0000000 --- a/oss-internship-2020/guetzli/tests/BUILD.bazel +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2020 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -licenses(["notice"]) - -cc_test( - name = "transaction_tests", - srcs = ["guetzli_transaction_test.cc"], - visibility=["//visibility:public"], - deps = [ - "//:guetzli_sapi", - "@com_google_googletest//:gtest_main", - ], - size = "large", - data = glob(["testdata/*"]), -) - -cc_test( - name = "sapi_lib_tests", - srcs = ["guetzli_sapi_test.cc"], - visibility=["//visibility:public"], - deps = [ - "//:guetzli_sapi", - "@com_google_googletest//:gtest_main", - ], - size = "large", - data = glob(["testdata/*"]), -)