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