Blob Blame History Raw
From e4e5d752ece581d6ef9fbb8bab0ab2edfde13fc5 Mon Sep 17 00:00:00 2001
From: Joe Drago <jdrago@netflix.com>
Date: Mon, 27 Apr 2020 14:27:31 -0700
Subject: [PATCH] Added avifenc Lossless (--lossless, -l) mode, which sets new
 defaults and warns when anything would cause the AVIF to not be lossless

---
 CHANGELOG.md           |   2 +
 apps/avifenc.c         | 119 ++++++++++++++++++++++++++++++++++++++---
 apps/shared/avifjpeg.c |   2 +-
 apps/shared/avifjpeg.h |   2 +-
 apps/shared/avifpng.c  |   8 ++-
 apps/shared/avifpng.h  |   4 +-
 6 files changed, 123 insertions(+), 14 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 19c8d08..8d05a5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [Unreleased]
+### Added
+- avifenc: Lossless (--lossless, -l) mode, which sets new defaults and warns when anything would cause the AVIF to not be lossless
 
 ## [0.7.2] - 2020-04-24
 ### Added
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 357596c..2d5a591 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -25,6 +25,7 @@ static void syntax(void)
     printf("Options:\n");
     printf("    -h,--help                         : Show syntax help\n");
     printf("    -j,--jobs J                       : Number of jobs (worker threads, default: 1)\n");
+    printf("    -l,--lossless                     : Set all defaults to encode losslessly, and emit warnings when settings/input don't allow for it\n");
     printf("    -d,--depth D                      : Output depth [8,10,12]. (JPEG/PNG only; For y4m, depth is retained)\n");
     printf("    -y,--yuv FORMAT                   : Output format [default=444, 422, 420]. (JPEG/PNG only; For y4m, format is retained)\n");
     printf("    -n,--nclx P/T/M                   : Set nclx colr box values (3 raw numbers, use -r to set range flag)\n");
@@ -156,6 +157,7 @@ int main(int argc, char * argv[])
     avifRange requestedRange = AVIF_RANGE_FULL;
     avifBool requestedRangeSet = AVIF_FALSE;
     avifBool nclxSet = AVIF_FALSE;
+    avifBool lossless = AVIF_FALSE;
     avifEncoder * encoder = NULL;
 
     avifNclxColorProfile nclx;
@@ -300,6 +302,19 @@ int main(int argc, char * argv[])
                 fprintf(stderr, "ERROR: Invalid imir axis: %s\n", arg);
                 return 1;
             }
+        } else if (!strcmp(arg, "-l") || !strcmp(arg, "--lossless")) {
+            lossless = AVIF_TRUE;
+
+            // Set defaults, and warn later on if anything looks incorrect
+            requestedFormat = AVIF_PIXEL_FORMAT_YUV444;  // don't subsample GBR
+            minQuantizer = AVIF_QUANTIZER_LOSSLESS;      // lossless
+            maxQuantizer = AVIF_QUANTIZER_LOSSLESS;      // lossless
+            minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS; // lossless
+            maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS; // lossless
+            codecChoice = AVIF_CODEC_CHOICE_AOM;         // rav1e doesn't support lossless transform yet:
+                                                         // https://github.com/xiph/rav1e/issues/151
+            requestedRange = AVIF_RANGE_FULL;            // avoid limited range
+            requestedRangeSet = AVIF_TRUE;
         } else {
             // Positional argument
             if (!inputFilename) {
@@ -325,6 +340,26 @@ int main(int argc, char * argv[])
     avifImage * avif = avifImageCreateEmpty();
     avifRWData raw = AVIF_DATA_EMPTY;
 
+    uint32_t sourceDepth = 0;
+    avifBool sourceWasRGB = AVIF_TRUE;
+    avifAppFileFormat inputFormat = avifGuessFileFormat(inputFilename);
+    if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
+        fprintf(stderr, "Cannot determine input file extension: %s\n", inputFilename);
+        returnCode = 1;
+        goto cleanup;
+    }
+
+    if (lossless && (inputFormat != AVIF_APP_FILE_FORMAT_Y4M)) {
+        if (!nclxSet) { // don't stomp on the user's cmdline nclx values
+            // Assume SRGB unless they tell us otherwise via --nclx
+            nclx.colourPrimaries = AVIF_NCLX_TRANSFER_CHARACTERISTICS_BT709;
+            nclx.transferCharacteristics = AVIF_NCLX_TRANSFER_CHARACTERISTICS_SRGB;
+            nclx.matrixCoefficients = AVIF_NCLX_MATRIX_COEFFICIENTS_IDENTITY; // this is key for lossless
+            nclx.range = AVIF_RANGE_FULL;
+            nclxSet = AVIF_TRUE;
+        }
+    }
+
     // Set range and nclx in advance so any upcoming RGB -> YUV use the proper coefficients
     if (requestedRangeSet) {
         avif->yuvRange = requestedRange;
@@ -334,12 +369,6 @@ int main(int argc, char * argv[])
         avifImageSetProfileNCLX(avif, &nclx);
     }
 
-    avifAppFileFormat inputFormat = avifGuessFileFormat(inputFilename);
-    if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) {
-        fprintf(stderr, "Cannot determine input file extension: %s\n", inputFilename);
-        returnCode = 1;
-        goto cleanup;
-    }
     if (inputFormat == AVIF_APP_FILE_FORMAT_Y4M) {
         if (requestedRangeSet) {
             fprintf(stderr, "WARNING: Ignoring range (-r) value when encoding from y4m content.\n");
@@ -353,13 +382,16 @@ int main(int argc, char * argv[])
             nclx.range = avif->yuvRange;
             avifImageSetProfileNCLX(avif, &nclx);
         }
+        sourceDepth = avif->depth;
+        sourceWasRGB = AVIF_FALSE;
     } else if (inputFormat == AVIF_APP_FILE_FORMAT_JPEG) {
         if (!avifJPEGRead(avif, inputFilename, requestedFormat, requestedDepth)) {
             returnCode = 1;
             goto cleanup;
         }
+        sourceDepth = 8;
     } else if (inputFormat == AVIF_APP_FILE_FORMAT_PNG) {
-        if (!avifPNGRead(avif, inputFilename, requestedFormat, requestedDepth)) {
+        if (!avifPNGRead(avif, inputFilename, requestedFormat, requestedDepth, &sourceDepth)) {
             returnCode = 1;
             goto cleanup;
         }
@@ -395,7 +427,78 @@ int main(int argc, char * argv[])
         avif->imir.axis = imirAxis;
     }
 
-    printf("AVIF to be written:\n");
+    avifBool usingAOM = AVIF_FALSE;
+    const char * codecName = avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE);
+    if (codecName && !strcmp(codecName, "aom")) {
+        usingAOM = AVIF_TRUE;
+    }
+    avifBool hasAlpha = (avif->alphaPlane && avif->alphaRowBytes);
+    avifBool losslessColorQP = (minQuantizer == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizer == AVIF_QUANTIZER_LOSSLESS);
+    avifBool losslessAlphaQP = (minQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizerAlpha == AVIF_QUANTIZER_LOSSLESS);
+    avifBool depthMatches = (sourceDepth == avif->depth);
+    avifBool using444 = (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444);
+    avifBool usingFullRange = (avif->yuvRange == AVIF_RANGE_FULL);
+    avifBool usingIdentityMatrix = (nclxSet && (nclx.matrixCoefficients == AVIF_NCLX_MATRIX_COEFFICIENTS_IDENTITY));
+
+    // Guess if the enduser is asking for lossless and enable it so that warnings can be emitted
+    if (losslessColorQP && (!hasAlpha || losslessAlphaQP)) {
+        // The enduser is probably expecting lossless. Turn it on and emit warnings
+        printf("Min/max QPs set to %d, assuming --lossless to enable warnings on potential lossless issues.\n", AVIF_QUANTIZER_LOSSLESS);
+        lossless = AVIF_TRUE;
+    }
+
+    // Check for any reasons lossless will fail, and complain loudly
+    if (lossless) {
+        if (!usingAOM) {
+            fprintf(stderr, "WARNING: [--lossless] Only aom (-c) supports lossless transforms. Output might not be lossless.\n");
+            lossless = AVIF_FALSE;
+        }
+
+        if (!losslessColorQP) {
+            fprintf(stderr,
+                    "WARNING: [--lossless] Color quantizer range (--min, --max) not set to %d. Color output might not be lossless.\n",
+                    AVIF_QUANTIZER_LOSSLESS);
+            lossless = AVIF_FALSE;
+        }
+
+        if (hasAlpha && !losslessAlphaQP) {
+            fprintf(stderr,
+                    "WARNING: [--lossless] Alpha present and alpha quantizer range (--minalpha, --maxalpha) not set to %d. Alpha output might not be lossless.\n",
+                    AVIF_QUANTIZER_LOSSLESS);
+            lossless = AVIF_FALSE;
+        }
+
+        if (!depthMatches) {
+            fprintf(stderr,
+                    "WARNING: [--lossless] Input depth (%d) does not match output depth (%d). Output might not be lossless.\n",
+                    sourceDepth,
+                    avif->depth);
+            lossless = AVIF_FALSE;
+        }
+
+        if (sourceWasRGB) {
+            if (!using444) {
+                fprintf(stderr, "WARNING: [--lossless] Input data was RGB and YUV subsampling (-y) isn't YUV444. Output might not be lossless.\n");
+                lossless = AVIF_FALSE;
+            }
+
+            if (!usingFullRange) {
+                fprintf(stderr, "WARNING: [--lossless] Input data was RGB and output range (-r) isn't full. Output might not be lossless.\n");
+                lossless = AVIF_FALSE;
+            }
+
+            if (!usingIdentityMatrix) {
+                fprintf(stderr, "WARNING: [--lossless] Input data was RGB and nclx matrixCoefficients isn't set to identity (--nclx x/x/0); Output might not be lossless.\n");
+                lossless = AVIF_FALSE;
+            }
+        }
+    }
+
+    const char * lossyHint = " (Lossy)";
+    if (lossless) {
+        lossyHint = " (Lossless)";
+    }
+    printf("AVIF to be written:%s\n", lossyHint);
     avifImageDump(avif);
 
     printf("Encoding with AV1 codec '%s' speed [%d], color QP [%d (%s) <-> %d (%s)], alpha QP [%d (%s) <-> %d (%s)], %d worker thread(s), please wait...\n",
diff --git a/apps/shared/avifjpeg.c b/apps/shared/avifjpeg.c
index dcefb41..39bd0cd 100644
--- a/apps/shared/avifjpeg.c
+++ b/apps/shared/avifjpeg.c
@@ -30,7 +30,7 @@ static void my_error_exit(j_common_ptr cinfo)
     longjmp(myerr->setjmp_buffer, 1);
 }
 
-avifBool avifJPEGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, int requestedDepth)
+avifBool avifJPEGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, uint32_t requestedDepth)
 {
     avifBool ret = AVIF_FALSE;
     FILE * f = NULL;
diff --git a/apps/shared/avifjpeg.h b/apps/shared/avifjpeg.h
index 13c54dd..3824aea 100644
--- a/apps/shared/avifjpeg.h
+++ b/apps/shared/avifjpeg.h
@@ -6,7 +6,7 @@
 
 #include "avif/avif.h"
 
-avifBool avifJPEGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, int requestedDepth);
+avifBool avifJPEGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, uint32_t requestedDepth);
 avifBool avifJPEGWrite(avifImage * avif, const char * outputFilename, int jpegQuality);
 
 #endif // ifndef LIBAVIF_APPS_SHARED_AVIFJPEG_H
diff --git a/apps/shared/avifpng.c b/apps/shared/avifpng.c
index 843d6b0..3573a46 100644
--- a/apps/shared/avifpng.c
+++ b/apps/shared/avifpng.c
@@ -14,7 +14,7 @@
 #pragma GCC diagnostic ignored "-Wclobbered"
 #endif
 
-avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, int requestedDepth)
+avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, uint32_t requestedDepth, uint32_t * outPNGDepth)
 {
     avifBool readResult = AVIF_FALSE;
     png_structp png = NULL;
@@ -104,6 +104,10 @@ avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelForm
         imgBitDepth = 16;
     }
 
+    if (outPNGDepth) {
+        *outPNGDepth = imgBitDepth;
+    }
+
     png_read_update_info(png, info);
 
     avif->width = rawWidth;
@@ -143,7 +147,7 @@ avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelForm
     return readResult;
 }
 
-avifBool avifPNGWrite(avifImage * avif, const char * outputFilename, int requestedDepth)
+avifBool avifPNGWrite(avifImage * avif, const char * outputFilename, uint32_t requestedDepth)
 {
     avifBool writeResult = AVIF_FALSE;
     png_structp png = NULL;
diff --git a/apps/shared/avifpng.h b/apps/shared/avifpng.h
index a10220e..12b68ad 100644
--- a/apps/shared/avifpng.h
+++ b/apps/shared/avifpng.h
@@ -7,7 +7,7 @@
 #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);
+avifBool avifPNGRead(avifImage * avif, const char * inputFilename, avifPixelFormat requestedFormat, uint32_t requestedDepth, uint32_t * outPNGDepth);
+avifBool avifPNGWrite(avifImage * avif, const char * outputFilename, uint32_t requestedDepth);
 
 #endif // ifndef LIBAVIF_APPS_SHARED_AVIFPNG_H