PNG Support for avifenc/avifdec * Added AVIF_LOCAL_ZLIBPNG CMake option for static Windows builds (with associated .cmd) * Updated AppVeyor to use AVIF_LOCAL_ZLIBPNG
diff --git a/.gitignore b/.gitignore index 842a54a..e67ac8b 100644 --- a/.gitignore +++ b/.gitignore
@@ -1,9 +1,11 @@ /build* /obj* /ext/aom -/ext/rav1e /ext/dav1d /ext/libgav1 +/ext/libpng +/ext/rav1e +/ext/zlib .clangd/ cscope.* tags
diff --git a/CMakeLists.txt b/CMakeLists.txt index 59b109f..0d795cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -31,6 +31,32 @@ option(AVIF_LOCAL_LIBGAV1 "Build the libgav1 codec by providing your own copy of the repo in ext/libgav1 (see Local Builds in README)" OFF) option(AVIF_LOCAL_RAV1E "Build the rav1e codec by providing your own copy of the repo in ext/rav1e (see Local Builds in README)" OFF) +# --------------------------------------------------------------------------------------- +# This insanity is for people embedding libavif or making fully static or Windows builds. +# Any proper unix environment should ignore this entire block. +option(AVIF_LOCAL_ZLIBPNG "Build zlib and libpng by providing your own copy inside the ext subdir." OFF) +if(AVIF_LOCAL_ZLIBPNG) + add_subdirectory(ext/zlib) + set(ZLIB_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/zlib") + include_directories("${CMAKE_CURRENT_BINARY_DIR}/ext/zlib") + set(CMAKE_DEBUG_POSTFIX "") + + # This is the only way I could avoid libpng going crazy if it found awk.exe, seems benign otherwise + set(PREV_ANDROID ${ANDROID}) + set(ANDROID TRUE) + set(PNG_BUILD_ZLIB "${CMAKE_CURRENT_SOURCE_DIR}/ext/zlib" CACHE STRING "" FORCE) + set(PNG_SHARED OFF CACHE BOOL "") + set(PNG_TESTS OFF CACHE BOOL "") + add_subdirectory(ext/libpng) + set(PNG_PNG_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ext/libpng") + set(PNG_LIBRARY png_static) + include_directories("${CMAKE_CURRENT_BINARY_DIR}/ext/libpng") + set(ANDROID ${PREV_ANDROID}) + + set(ZLIB_LIBRARY zlibstatic) +endif() +# --------------------------------------------------------------------------------------- + # Enable all warnings if(CMAKE_C_COMPILER_ID MATCHES "Clang") MESSAGE(STATUS "libavif: Enabling warnings for Clang") @@ -269,26 +295,33 @@ option(AVIF_BUILD_APPS "Build avif apps." OFF) if(AVIF_BUILD_APPS) + find_package(ZLIB REQUIRED) + find_package(PNG REQUIRED) + add_executable(avifenc apps/avifenc.c - apps/shared/y4m.c + + apps/shared/avifpng.c apps/shared/avifutil.c + apps/shared/y4m.c ) if(AVIF_LOCAL_LIBGAV1) set_target_properties(avifenc PROPERTIES LINKER_LANGUAGE "CXX") endif() - target_link_libraries(avifenc avif ${AVIF_PLATFORM_LIBRARIES}) - target_include_directories(avifenc PRIVATE apps/shared) + target_link_libraries(avifenc avif ${AVIF_PLATFORM_LIBRARIES} ${ZLIB_LIBRARY} ${PNG_LIBRARY}) + target_include_directories(avifenc PRIVATE apps/shared ${PNG_PNG_INCLUDE_DIR}) add_executable(avifdec apps/avifdec.c - apps/shared/y4m.c + + apps/shared/avifpng.c apps/shared/avifutil.c + apps/shared/y4m.c ) if(AVIF_LOCAL_LIBGAV1) set_target_properties(avifdec PROPERTIES LINKER_LANGUAGE "CXX") endif() - target_link_libraries(avifdec avif ${AVIF_PLATFORM_LIBRARIES}) - target_include_directories(avifdec PRIVATE apps/shared) + target_link_libraries(avifdec avif ${AVIF_PLATFORM_LIBRARIES} ${ZLIB_LIBRARY} ${PNG_LIBRARY}) + target_include_directories(avifdec PRIVATE apps/shared ${PNG_PNG_INCLUDE_DIR}) if(NOT SKIP_INSTALL_APPS AND NOT SKIP_INSTALL_ALL) install(TARGETS avifenc avifdec @@ -358,14 +391,38 @@ ) endif() +# --------------------------------------------------------------------------------------- +# Win32 (Visual Studio) fixups + macro(avif_set_folder_safe target folder) if(TARGET ${target}) set_target_properties(${target} PROPERTIES FOLDER ${folder}) endif() endmacro() +macro(avif_exclude_safe target) + if(TARGET ${target}) + set_target_properties(${target} PROPERTIES EXCLUDE_FROM_DEFAULT_BUILD True) + endif() +endmacro() + if(WIN32) set_property(GLOBAL PROPERTY USE_FOLDERS ON) avif_set_folder_safe(avif "ext/avif") + if(AVIF_LOCAL_ZLIBPNG) + avif_set_folder_safe(example "ext/zlibpng") + avif_set_folder_safe(genfiles "ext/zlibpng") + avif_set_folder_safe(minigzip "ext/zlibpng") + avif_set_folder_safe(png_static "ext/zlibpng") + avif_set_folder_safe(zlib "ext/zlibpng") + avif_set_folder_safe(zlibstatic "ext/zlibpng") + + # Don't bother building these targets + avif_exclude_safe(example) + avif_exclude_safe(genfiles) + avif_exclude_safe(minigzip) + endif() endif() + +# ---------------------------------------------------------------------------------------
diff --git a/apps/avifdec.c b/apps/avifdec.c index 378d1d6..03a5074 100644 --- a/apps/avifdec.c +++ b/apps/avifdec.c
@@ -3,10 +3,12 @@ #include "avif/avif.h" +#include "avifpng.h" #include "avifutil.h" #include "y4m.h" #include <stdio.h> +#include <stdlib.h> #include <string.h> #define NEXTARG() \ @@ -18,10 +20,11 @@ static void syntax(void) { - printf("Syntax: avifdec [options] input.avif output.y4m\n"); + printf("Syntax: avifdec [options] input.avif output.[png|y4m]\n"); printf("Options:\n"); printf(" -h,--help : Show syntax help\n"); printf(" -c,--codec C : AV1 codec to use (choose from versions list below)\n"); + printf(" -d,--depth D : Output depth [8,16]. (PNG only; For y4m, depth is retained)\n"); printf("\n"); avifPrintVersions(); } @@ -30,6 +33,7 @@ { const char * inputFilename = NULL; const char * outputFilename = NULL; + int requestedDepth = 0; avifCodecChoice codecChoice = AVIF_CODEC_CHOICE_AUTO; if (argc < 2) { @@ -57,6 +61,13 @@ return 1; } } + } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { + NEXTARG(); + requestedDepth = atoi(arg); + if ((requestedDepth != 8) && (requestedDepth != 16)) { + fprintf(stderr, "ERROR: invalid depth: %s\n", arg); + return 1; + } } else { // Positional argument if (!inputFilename) { @@ -115,8 +126,16 @@ printf("Image decoded: %s\n", inputFilename); printf("Image details:\n"); avifImageDump(avif); - if (!y4mWrite(avif, outputFilename)) { - returnCode = 1; + + const char * fileExt = strrchr(outputFilename, '.'); + if (fileExt && (!strcmp(fileExt, ".y4m"))) { + if (!y4mWrite(avif, outputFilename)) { + returnCode = 1; + } + } else { + if (!avifPNGWrite(avif, outputFilename, requestedDepth)) { + returnCode = 1; + } } } else { printf("ERROR: Failed to decode image: %s\n", avifResultToString(decodeResult));
diff --git a/apps/avifenc.c b/apps/avifenc.c index 7a59b48..9531391 100644 --- a/apps/avifenc.c +++ b/apps/avifenc.c
@@ -3,6 +3,7 @@ #include "avif/avif.h" +#include "avifpng.h" #include "avifutil.h" #include "y4m.h" @@ -19,10 +20,12 @@ static void syntax(void) { - printf("Syntax: avifenc [options] input.y4m output.avif\n"); + printf("Syntax: avifenc [options] input.[png|y4m] output.avif\n"); printf("Options:\n"); printf(" -h,--help : Show syntax help\n"); printf(" -j,--jobs J : Number of jobs (worker threads, default: 1)\n"); + printf(" -d,--depth D : Output depth [8,10,12]. (PNG only; For y4m, depth is retained)\n"); + printf(" -y,--yuv FORMAT : Output format [default=444, 422, 420]. (PNG only; For y4m, format is retained)\n"); printf(" -n,--nclx P/T/M/R : Set nclx colr box values (4 raw numbers)\n"); printf(" P = enum avifNclxColourPrimaries\n"); printf(" T = enum avifNclxTransferCharacteristics\n"); @@ -133,6 +136,8 @@ } int jobs = 1; + avifPixelFormat requestedFormat = AVIF_PIXEL_FORMAT_YUV444; + int requestedDepth = 0; int minQuantizer = AVIF_QUANTIZER_BEST_QUALITY; int maxQuantizer = AVIF_QUANTIZER_BEST_QUALITY; int minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS; @@ -164,6 +169,25 @@ if (jobs < 1) { jobs = 1; } + } else if (!strcmp(arg, "-d") || !strcmp(arg, "--depth")) { + NEXTARG(); + requestedDepth = atoi(arg); + if ((requestedDepth != 8) && (requestedDepth != 10) && (requestedDepth != 12)) { + fprintf(stderr, "ERROR: invalid depth: %s\n", arg); + return 1; + } + } else if (!strcmp(arg, "-y") || !strcmp(arg, "--yuv")) { + NEXTARG(); + if (!strcmp(arg, "444")) { + requestedFormat = AVIF_PIXEL_FORMAT_YUV444; + } else if (!strcmp(arg, "422")) { + requestedFormat = AVIF_PIXEL_FORMAT_YUV422; + } else if (!strcmp(arg, "420")) { + requestedFormat = AVIF_PIXEL_FORMAT_YUV420; + } else { + fprintf(stderr, "ERROR: invalid format: %s\n", arg); + return 1; + } } else if (!strcmp(arg, "--min")) { NEXTARG(); minQuantizer = atoi(arg); @@ -281,9 +305,17 @@ avifImage * avif = avifImageCreateEmpty(); avifRWData raw = AVIF_DATA_EMPTY; - if (!y4mRead(avif, inputFilename)) { - returnCode = 1; - goto cleanup; + const char * fileExt = strrchr(inputFilename, '.'); + if (fileExt && (!strcmp(fileExt, ".y4m"))) { + if (!y4mRead(avif, inputFilename)) { + returnCode = 1; + goto cleanup; + } + } else { + if (!avifPNGRead(avif, inputFilename, requestedFormat, requestedDepth)) { + returnCode = 1; + goto cleanup; + } } printf("Successfully loaded: %s\n", inputFilename);
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c new file mode 100644 index 0000000..a144680 --- /dev/null +++ b/apps/shared/avifpng.c
@@ -0,0 +1,218 @@ +// Copyright 2020 Joe Drago. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#include "avifpng.h" + +#include "png.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, int requestedDepth) +{ + avifBool readResult = AVIF_FALSE; + png_structp png = NULL; + png_infop info = NULL; + png_bytep * rowPointers = NULL; + + avifRGBImage rgb; + memset(&rgb, 0, sizeof(avifRGBImage)); + + FILE * f = fopen(inputFilename, "rb"); + if (!f) { + fprintf(stderr, "Can't open PNG file for read: %s\n", inputFilename); + goto cleanup; + } + + uint8_t header[8]; + size_t bytesRead = fread(header, 1, 8, f); + if (bytesRead != 8) { + fprintf(stderr, "Can't read PNG header: %s\n", inputFilename); + goto cleanup; + } + if (png_sig_cmp(header, 0, 8)) { + fprintf(stderr, "Not a PNG: %s\n", inputFilename); + goto cleanup; + } + + png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + fprintf(stderr, "Cannot init libpng (png): %s\n", inputFilename); + goto cleanup; + } + info = png_create_info_struct(png); + if (!info) { + fprintf(stderr, "Cannot init libpng (info): %s\n", inputFilename); + goto cleanup; + } + + if (setjmp(png_jmpbuf(png))) { + fprintf(stderr, "Error reading PNG: %s\n", inputFilename); + goto cleanup; + } + + png_init_io(png, f); + png_set_sig_bytes(png, 8); + png_read_info(png, info); + + char * iccpProfileName = NULL; + int iccpCompression = 0; + unsigned char * iccpData = NULL; + png_uint_32 iccpDataLen = 0; + if (png_get_iCCP(png, info, &iccpProfileName, &iccpCompression, &iccpData, &iccpDataLen) == PNG_INFO_iCCP) { + avifImageSetProfileICC(avif, iccpData, iccpDataLen); + } + + int rawWidth = png_get_image_width(png, info); + int rawHeight = png_get_image_height(png, info); + png_byte rawColorType = png_get_color_type(png, info); + png_byte rawBitDepth = png_get_bit_depth(png, info); + + if (rawColorType == PNG_COLOR_TYPE_PALETTE) { + png_set_palette_to_rgb(png); + } + + if ((rawColorType == PNG_COLOR_TYPE_GRAY) && (rawBitDepth < 8)) { + png_set_expand_gray_1_2_4_to_8(png); + } + + avifBool hasAlpha = AVIF_FALSE; + if (png_get_valid(png, info, PNG_INFO_tRNS)) { + png_set_tRNS_to_alpha(png); + hasAlpha = AVIF_TRUE; + } + + if ((rawColorType == PNG_COLOR_TYPE_RGB) || (rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_PALETTE)) { + png_set_filler(png, 0xFFFF, PNG_FILLER_AFTER); + } + + if ((rawColorType == PNG_COLOR_TYPE_GRAY) || (rawColorType == PNG_COLOR_TYPE_GRAY_ALPHA)) { + png_set_gray_to_rgb(png); + } + + int imgBitDepth = 8; + if (rawBitDepth == 16) { + png_set_swap(png); + imgBitDepth = 16; + } + + png_read_update_info(png, info); + + avif->width = rawWidth; + avif->height = rawHeight; + avif->yuvFormat = requestedFormat; + avif->depth = requestedDepth; + if (avif->depth == 0) { + if (imgBitDepth == 8) { + avif->depth = 8; + } else { + avif->depth = 12; + } + } + + avifRGBImageSetDefaults(&rgb, avif); + rgb.depth = imgBitDepth; + avifRGBImageAllocatePixels(&rgb); + rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * rgb.height); + for (uint32_t y = 0; y < rgb.height; ++y) { + rowPointers[y] = &rgb.pixels[y * rgb.rowBytes]; + } + png_read_image(png, rowPointers); + avifImageRGBToYUV(avif, &rgb); + readResult = AVIF_TRUE; + +cleanup: + if (f) { + fclose(f); + } + if (png) { + png_destroy_read_struct(&png, &info, NULL); + } + if (rowPointers) { + free(rowPointers); + } + avifRGBImageFreePixels(&rgb); + return readResult; +} + +avifBool avifPNGWrite(avifImage * avif, const char * outputFilename, int requestedDepth) +{ + avifBool writeResult = AVIF_FALSE; + png_structp png = NULL; + png_infop info = NULL; + png_bytep * rowPointers = NULL; + + avifRGBImage rgb; + memset(&rgb, 0, sizeof(avifRGBImage)); + + FILE * f = fopen(outputFilename, "wb"); + if (!f) { + fprintf(stderr, "Can't open PNG file for write: %s\n", outputFilename); + goto cleanup; + } + + png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!png) { + fprintf(stderr, "Cannot init libpng (png): %s\n", outputFilename); + goto cleanup; + } + info = png_create_info_struct(png); + if (!info) { + fprintf(stderr, "Cannot init libpng (info): %s\n", outputFilename); + goto cleanup; + } + + if (setjmp(png_jmpbuf(png))) { + fprintf(stderr, "Error writing PNG: %s\n", outputFilename); + goto cleanup; + } + + png_init_io(png, f); + + int rgbDepth = requestedDepth; + if (rgbDepth == 0) { + if (avif->depth > 8) { + rgbDepth = 16; + } else { + rgbDepth = 8; + } + } + + if (rgbDepth > 8) { + png_set_swap(png); + } + + png_set_IHDR( + png, info, avif->width, avif->height, rgbDepth, PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + if (avif->profileFormat == AVIF_PROFILE_FORMAT_ICC) { + png_set_iCCP(png, info, "libavif", 0, avif->icc.data, (png_uint_32)avif->icc.size); + } + png_write_info(png, info); + + avifRGBImageSetDefaults(&rgb, avif); + rgb.depth = rgbDepth; + avifRGBImageAllocatePixels(&rgb); + avifImageYUVToRGB(avif, &rgb); + rowPointers = (png_bytep *)malloc(sizeof(png_bytep) * rgb.height); + for (uint32_t y = 0; y < rgb.height; ++y) { + rowPointers[y] = &rgb.pixels[y * rgb.rowBytes]; + } + + png_write_image(png, rowPointers); + png_write_end(png, NULL); + + writeResult = AVIF_TRUE; +cleanup: + if (f) { + fclose(f); + } + if (png) { + png_destroy_write_struct(&png, &info); + } + if (rowPointers) { + free(rowPointers); + } + avifRGBImageFreePixels(&rgb); + return writeResult; +}
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h new file mode 100644 index 0000000..a10220e --- /dev/null +++ b/apps/shared/avifpng.h
@@ -0,0 +1,13 @@ +// Copyright 2019 Joe Drago. All rights reserved. +// SPDX-License-Identifier: BSD-2-Clause + +#ifndef LIBAVIF_APPS_SHARED_AVIFPNG_H +#define LIBAVIF_APPS_SHARED_AVIFPNG_H + +#include "avif/avif.h" + +// if (requestedDepth == 0), do best-fit +avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, int requestedDepth); +avifBool avifPNGWrite(avifImage * avif, const char * outputFilename, int requestedDepth); + +#endif // ifndef LIBAVIF_APPS_SHARED_AVIFPNG_H
diff --git a/appveyor.yml b/appveyor.yml index 57b3281..250fad8 100644 --- a/appveyor.yml +++ b/appveyor.yml
@@ -20,12 +20,13 @@ - cd ext - aom.cmd - dav1d.cmd + - zlibpng.cmd - cd .. # Configure with CMake - mkdir build - cd build - cmake --version - - cmake .. -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON -DAVIF_CODEC_DAV1D=ON -DAVIF_LOCAL_DAV1D=ON -DBUILD_SHARED_LIBS=OFF -DAVIF_BUILD_APPS=ON + - cmake .. -DAVIF_CODEC_AOM=ON -DAVIF_LOCAL_AOM=ON -DAVIF_CODEC_DAV1D=ON -DAVIF_LOCAL_DAV1D=ON -DBUILD_SHARED_LIBS=OFF -DAVIF_LOCAL_ZLIBPNG=1 -DAVIF_BUILD_APPS=ON build: project: build/libavif.sln
diff --git a/ext/zlibpng.cmd b/ext/zlibpng.cmd new file mode 100644 index 0000000..5d81b2b --- /dev/null +++ b/ext/zlibpng.cmd
@@ -0,0 +1,8 @@ +: # If you want to use a local build of zlib/libpng, you must clone the repos in this directory first, +: # then enable CMake's AVIF_LOCAL_ZLIBPNG. +: # The git tags below are known to work, and will occasionally be updated. Feel free to use a more recent commit. + +: # The odd choice of comment style in this file is to try to share this script between *nix and win32. + +git clone -b v1.2.11 --depth 1 https://github.com/madler/zlib.git +git clone -b v1.6.37 --depth 1 https://github.com/glennrp/libpng.git