Support premultiply alpha
diff --git a/src/alpha.c b/src/alpha.c
index 0dbdf80..fa1d551 100644
--- a/src/alpha.c
+++ b/src/alpha.c
@@ -4,6 +4,7 @@
#include "avif/internal.h"
#include <string.h>
+#include <assert.h>
static int calcMaxChannel(uint32_t depth, avifRange range)
{
@@ -380,3 +381,248 @@
return AVIF_TRUE;
}
+
+avifResult avifRGBImagePremultiplyAlpha(avifRGBImage * rgb) {
+ // no data
+ if (!rgb->pixels || !rgb->rowBytes) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+
+ // no alpha.
+ if (!avifRGBFormatHasAlpha(rgb->format)) {
+ return AVIF_RESULT_INVALID_ARGUMENT;
+ }
+
+ // already premultiplied. No-op.
+ if (rgb->alphaPremultiplied) {
+ return AVIF_RESULT_OK;
+ }
+
+ rgb->alphaPremultiplied = AVIF_TRUE;
+
+ avifResult libyuvResult = avifRGBImagePremultiplyAlphaLibYUV(rgb);
+ if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) {
+ return libyuvResult;
+ }
+
+ assert(rgb->depth >= 8 && rgb->depth <= 16);
+
+ uint32_t max = (1 << rgb->depth) - 1;
+ float maxF = (float)max;
+
+ if (rgb->depth > 8) {
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint16_t * pixel = (uint16_t *)&row[i * 8];
+ uint16_t a = pixel[3];
+ if (a >= max) {
+ // opaque is no-op
+ continue;
+ } else if (a == 0) {
+ // result must be zero
+ pixel[0] = 0;
+ pixel[1] = 0;
+ pixel[2] = 0;
+ } else {
+ // a < maxF is always true now, so we don't need clamp here
+ pixel[0] = (uint16_t)avifRoundf((float)pixel[0] * (float)a / maxF);
+ pixel[1] = (uint16_t)avifRoundf((float)pixel[1] * (float)a / maxF);
+ pixel[2] = (uint16_t)avifRoundf((float)pixel[2] * (float)a / maxF);
+ }
+ }
+ }
+ } else {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint16_t * pixel = (uint16_t *)&row[i * 8];
+ uint16_t a = pixel[0];
+ if (a >= max) {
+ continue;
+ } else if (a == 0) {
+ pixel[1] = 0;
+ pixel[2] = 0;
+ pixel[3] = 0;
+ } else {
+ pixel[1] = (uint16_t)avifRoundf((float)pixel[1] * (float)a / maxF);
+ pixel[2] = (uint16_t)avifRoundf((float)pixel[2] * (float)a / maxF);
+ pixel[3] = (uint16_t)avifRoundf((float)pixel[3] * (float)a / maxF);
+ }
+ }
+ }
+ }
+ } else {
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint8_t * pixel = &row[i * 4];
+ uint8_t a = pixel[3];
+ // uint8_t can't exceed 255
+ if (a == max) {
+ continue;
+ } else if (a == 0) {
+ pixel[0] = 0;
+ pixel[1] = 0;
+ pixel[2] = 0;
+ } else {
+ pixel[0] = (uint8_t)avifRoundf((float)pixel[0] * (float)a / maxF);
+ pixel[1] = (uint8_t)avifRoundf((float)pixel[1] * (float)a / maxF);
+ pixel[2] = (uint8_t)avifRoundf((float)pixel[2] * (float)a / maxF);
+ }
+ }
+ }
+ } else {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint8_t * pixel = &row[i * 4];
+ uint8_t a = pixel[0];
+ if (a == max) {
+ continue;
+ } else if (a == 0) {
+ pixel[1] = 0;
+ pixel[2] = 0;
+ pixel[3] = 0;
+ } else {
+ pixel[1] = (uint8_t)avifRoundf((float)pixel[1] * (float)a / maxF);
+ pixel[2] = (uint8_t)avifRoundf((float)pixel[2] * (float)a / maxF);
+ pixel[3] = (uint8_t)avifRoundf((float)pixel[3] * (float)a / maxF);
+ }
+ }
+ }
+ }
+ }
+
+ return AVIF_RESULT_OK;
+}
+
+avifResult avifRGBImageUnpremultiplyAlpha(avifRGBImage * rgb)
+{
+ // no data
+ if (!rgb->pixels || !rgb->rowBytes) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+
+ // no alpha.
+ if (!avifRGBFormatHasAlpha(rgb->format)) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+
+ // already premultiplied. No-op.
+ if (!rgb->alphaPremultiplied) {
+ return AVIF_RESULT_OK;
+ }
+
+ rgb->alphaPremultiplied = AVIF_FALSE;
+
+ avifResult libyuvResult = avifRGBImageUnpremultiplyAlphaLibYUV(rgb);
+ if (libyuvResult != AVIF_RESULT_NOT_IMPLEMENTED) {
+ return libyuvResult;
+ }
+
+ assert(rgb->depth >= 8 && rgb->depth <= 16);
+
+ uint32_t max = (1 << rgb->depth) - 1;
+ float maxF = (float)max;
+
+ if (rgb->depth > 8) {
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint16_t * pixel = (uint16_t *)&row[i * 8];
+ uint16_t a = pixel[3];
+ if (a >= max) {
+ // opaque is no-op
+ continue;
+ } else if (a == 0) {
+ // prevent divide by zero
+ pixel[0] = 0;
+ pixel[1] = 0;
+ pixel[2] = 0;
+ } else {
+ float c1 = avifRoundf((float)pixel[0] * maxF / (float)a);
+ float c2 = avifRoundf((float)pixel[1] * maxF / (float)a);
+ float c3 = avifRoundf((float)pixel[2] * maxF / (float)a);
+ pixel[0] = (uint16_t)AVIF_CLAMP(c1, 0, maxF);
+ pixel[1] = (uint16_t)AVIF_CLAMP(c2, 0, maxF);
+ pixel[2] = (uint16_t)AVIF_CLAMP(c3, 0, maxF);
+ }
+ }
+ }
+ } else {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint16_t * pixel = (uint16_t *)&row[i * 8];
+ uint16_t a = pixel[0];
+ if (a >= max) {
+ continue;
+ } else if (a == 0) {
+ pixel[1] = 0;
+ pixel[2] = 0;
+ pixel[3] = 0;
+ } else {
+ float c1 = avifRoundf((float)pixel[1] * maxF / (float)a);
+ float c2 = avifRoundf((float)pixel[2] * maxF / (float)a);
+ float c3 = avifRoundf((float)pixel[3] * maxF / (float)a);
+ pixel[1] = (uint16_t)AVIF_CLAMP(c1, 0, maxF);
+ pixel[2] = (uint16_t)AVIF_CLAMP(c2, 0, maxF);
+ pixel[3] = (uint16_t)AVIF_CLAMP(c3, 0, maxF);
+ }
+ }
+ }
+ }
+ } else {
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint8_t * pixel = &row[i * 4];
+ uint8_t a = pixel[3];
+ if (a == max) {
+ continue;
+ } else if (a == 0) {
+ pixel[0] = 0;
+ pixel[1] = 0;
+ pixel[2] = 0;
+ } else {
+ float c1 = avifRoundf((float)pixel[0] * maxF / (float)a);
+ float c2 = avifRoundf((float)pixel[1] * maxF / (float)a);
+ float c3 = avifRoundf((float)pixel[2] * maxF / (float)a);
+ pixel[0] = (uint8_t)AVIF_CLAMP(c1, 0, maxF);
+ pixel[1] = (uint8_t)AVIF_CLAMP(c2, 0, maxF);
+ pixel[2] = (uint8_t)AVIF_CLAMP(c3, 0, maxF);
+ }
+ }
+ }
+ } else {
+ for (uint32_t j = 0; j < rgb->height; ++j) {
+ uint8_t * row = &rgb->pixels[j * rgb->rowBytes];
+ for (uint32_t i = 0; i < rgb->width; ++i) {
+ uint8_t * pixel = &row[i * 4];
+ uint8_t a = pixel[0];
+ if (a == max) {
+ continue;
+ } else if (a == 0) {
+ pixel[1] = 0;
+ pixel[2] = 0;
+ pixel[3] = 0;
+ } else {
+ float c1 = avifRoundf((float)pixel[1] * maxF / (float)a);
+ float c2 = avifRoundf((float)pixel[2] * maxF / (float)a);
+ float c3 = avifRoundf((float)pixel[3] * maxF / (float)a);
+ pixel[1] = (uint8_t)AVIF_CLAMP(c1, 0, maxF);
+ pixel[2] = (uint8_t)AVIF_CLAMP(c2, 0, maxF);
+ pixel[3] = (uint8_t)AVIF_CLAMP(c3, 0, maxF);
+ }
+ }
+ }
+ }
+ }
+
+ return AVIF_RESULT_OK;
+}
diff --git a/src/avif.c b/src/avif.c
index f4f0fcd..470174d 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -139,6 +139,7 @@
dstImage->yuvRange = srcImage->yuvRange;
dstImage->yuvChromaSamplePosition = srcImage->yuvChromaSamplePosition;
dstImage->alphaRange = srcImage->alphaRange;
+ dstImage->alphaPremultiplied = srcImage->alphaPremultiplied;
dstImage->colorPrimaries = srcImage->colorPrimaries;
dstImage->transferCharacteristics = srcImage->transferCharacteristics;
@@ -360,6 +361,7 @@
rgb->ignoreAlpha = AVIF_FALSE;
rgb->pixels = NULL;
rgb->rowBytes = 0;
+ rgb->alphaPremultiplied = image->alphaPremultiplied;
}
void avifRGBImageAllocatePixels(avifRGBImage * rgb)
diff --git a/src/read.c b/src/read.c
index 5398a4e..941b20d 100644
--- a/src/read.c
+++ b/src/read.c
@@ -140,6 +140,7 @@
uint32_t auxForID; // if non-zero, this item is an auxC plane for Item #{auxForID}
uint32_t descForID; // if non-zero, this item is a content description for Item #{descForID}
uint32_t dimgForID; // if non-zero, this item is a derived image for Item #{dimgForID}
+ uint32_t premByID; // if non-zero, this item is premultiplied by Item #{premByID}
avifBool hasUnsupportedEssentialProperty; // If true, this item cites a property flagged as 'essential' that libavif doesn't support (yet). Ignore the item, if so.
avifBool ipmaSeen; // if true, this item already received a property association
} avifDecoderItem;
@@ -295,6 +296,7 @@
{
uint32_t id;
uint32_t auxForID; // if non-zero, this item is an auxC plane for Track #{auxForID}
+ uint32_t premByID; // if non-zero, this item is premultiplied by Item #{premByID}
uint32_t mediaTimescale;
uint64_t mediaDuration;
uint32_t width;
@@ -1699,6 +1701,9 @@
dimg->dimgForID = fromID;
}
+ if (!memcmp(irefHeader.type, "prem", 4)) {
+ item->premByID = toID;
+ }
}
}
}
@@ -2031,6 +2036,10 @@
CHECK(avifROStreamReadU32(&s, &toID)); // unsigned int(32) track_IDs[]
CHECK(avifROStreamSkip(&s, header.size - sizeof(uint32_t))); // just take the first one
track->auxForID = toID;
+ } else if (!memcmp(header.type, "prem", 4)) {
+ uint32_t byID;
+ CHECK(avifROStreamReadU32(&s, &byID)); // unsigned int(32) to_item_ID
+ track->premByID = byID;
} else {
CHECK(avifROStreamSkip(&s, header.size));
}
@@ -2604,6 +2613,7 @@
decoder->image->width = colorTrack->width;
decoder->image->height = colorTrack->height;
decoder->alphaPresent = (alphaTrack != NULL);
+ decoder->image->alphaPremultiplied = decoder->alphaPresent && colorTrack->premByID == alphaTrack->id;
} else {
// Create from items
@@ -2760,6 +2770,7 @@
decoder->image->height = 0;
}
decoder->alphaPresent = (alphaItem != NULL);
+ decoder->image->alphaPremultiplied = decoder->alphaPresent && colorItem->premByID == alphaItem->id;
}
// Sanity check tiles
diff --git a/src/reformat.c b/src/reformat.c
index 57c9d7e..4d2a863 100644
--- a/src/reformat.c
+++ b/src/reformat.c
@@ -159,6 +159,10 @@
return AVIF_RESULT_REFORMAT_FAILED;
}
+ if (image->alphaPremultiplied != rgb->alphaPremultiplied) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+
avifReformatState state;
if (!avifPrepareReformatState(image, rgb, &state)) {
return AVIF_RESULT_REFORMAT_FAILED;
@@ -965,6 +969,10 @@
return AVIF_RESULT_REFORMAT_FAILED;
}
+ if (image->alphaPremultiplied != rgb->alphaPremultiplied) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+
avifReformatState state;
if (!avifPrepareReformatState(image, rgb, &state)) {
return AVIF_RESULT_REFORMAT_FAILED;
diff --git a/src/reformat_libyuv.c b/src/reformat_libyuv.c
index 273e7ad..061a579 100644
--- a/src/reformat_libyuv.c
+++ b/src/reformat_libyuv.c
@@ -12,6 +12,16 @@
(void)rgb;
return AVIF_RESULT_NOT_IMPLEMENTED;
}
+avifResult avifRGBImagePremultiplyLibYUV(avifRGBImage * rgb)
+{
+ (void)rgb;
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+}
+avifResult avifRGBImageUnpremultiplyLibYUV(avifRGBImage * rgb)
+{
+ (void)rgb;
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+}
unsigned int avifLibYUVVersion(void)
{
return 0;
@@ -422,6 +432,49 @@
return AVIF_RESULT_NOT_IMPLEMENTED;
}
+avifResult avifRGBImagePremultiplyAlphaLibYUV(avifRGBImage * rgb)
+{
+ // See if the current settings can be accomplished with libyuv, and use it (if possible).
+
+ if (rgb->depth != 8) {
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
+ // libavif uses byte-order when describing pixel formats, such that the R in RGBA is the lowest address,
+ // similar to PNG. libyuv orders in word-order, so libavif's RGBA would be referred to in libyuv as ABGR.
+
+ // order of RGB doesn't matter here.
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ if (ARGBAttenuate(rgb->pixels, rgb->rowBytes, rgb->pixels, rgb->rowBytes, rgb->width, rgb->height) != 0) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+ return AVIF_RESULT_OK;
+ }
+
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+}
+
+avifResult avifRGBImageUnpremultiplyAlphaLibYUV(avifRGBImage * rgb)
+{
+ // See if the current settings can be accomplished with libyuv, and use it (if possible).
+
+ if (rgb->depth != 8) {
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+ }
+
+ // libavif uses byte-order when describing pixel formats, such that the R in RGBA is the lowest address,
+ // similar to PNG. libyuv orders in word-order, so libavif's RGBA would be referred to in libyuv as ABGR.
+
+ if (rgb->format == AVIF_RGB_FORMAT_RGBA || rgb->format == AVIF_RGB_FORMAT_BGRA) {
+ if (ARGBUnattenuate(rgb->pixels, rgb->rowBytes, rgb->pixels, rgb->rowBytes, rgb->width, rgb->height) != 0) {
+ return AVIF_RESULT_REFORMAT_FAILED;
+ }
+ return AVIF_RESULT_OK;
+ }
+
+ return AVIF_RESULT_NOT_IMPLEMENTED;
+}
+
unsigned int avifLibYUVVersion(void)
{
return (unsigned int)LIBYUV_VERSION;
diff --git a/src/write.c b/src/write.c
index bb8e546..700c5f9 100644
--- a/src/write.c
+++ b/src/write.c
@@ -417,7 +417,8 @@
for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
const avifImage * cellImage = cellImages[cellIndex];
if ((cellImage->depth != firstCell->depth) || (cellImage->width != firstCell->width) ||
- (cellImage->height != firstCell->height) || (!!cellImage->alphaPlane != !!firstCell->alphaPlane)) {
+ (cellImage->height != firstCell->height) || (!!cellImage->alphaPlane != !!firstCell->alphaPlane) ||
+ (cellImage->alphaPremultiplied != firstCell->alphaPremultiplied)) {
return AVIF_RESULT_INVALID_IMAGE_GRID;
}
@@ -460,6 +461,9 @@
// Prepare all AV1 items
+ const char ** pColorIrefType;
+ uint16_t * pColorIrefToID;
+
uint16_t gridColorID = 0;
if (cellCount > 1) {
avifEncoderItem * gridColorItem = avifEncoderDataCreateItem(encoder->data, "grid", "Color", 6, 0);
@@ -469,6 +473,8 @@
gridColorID = gridColorItem->id;
encoder->data->primaryItemID = gridColorID;
+ pColorIrefType = &gridColorItem->irefType;
+ pColorIrefToID = &gridColorItem->irefToID;
}
for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
@@ -484,6 +490,8 @@
item->dimgFromID = gridColorID;
} else {
encoder->data->primaryItemID = item->id;
+ pColorIrefType = &item->irefType;
+ pColorIrefToID = &item->irefToID;
}
}
@@ -519,6 +527,11 @@
gridAlphaItem->gridCols = gridCols;
gridAlphaItem->gridRows = gridRows;
gridAlphaID = gridAlphaItem->id;
+
+ if (encoder->data->imageMetadata->alphaPremultiplied) {
+ *pColorIrefType = "prem";
+ *pColorIrefToID = gridAlphaID;
+ }
}
for (uint32_t cellIndex = 0; cellIndex < cellCount; ++cellIndex) {
@@ -535,6 +548,11 @@
} else {
item->irefToID = encoder->data->primaryItemID;
item->irefType = "auxl";
+
+ if (encoder->data->imageMetadata->alphaPremultiplied) {
+ *pColorIrefType = "prem";
+ *pColorIrefToID = item->id;
+ }
}
}
}