| // Copyright 2019 Joe Drago. All rights reserved. | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| #include "avifutil.h" | |
| #include <assert.h> | |
| #include <ctype.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include "avifjpeg.h" | |
| #include "avifpng.h" | |
| #include "y4m.h" | |
| char * avifFileFormatToString(avifAppFileFormat format) | |
| { | |
| switch (format) { | |
| case AVIF_APP_FILE_FORMAT_UNKNOWN: | |
| return "unknown"; | |
| case AVIF_APP_FILE_FORMAT_AVIF: | |
| return "AVIF"; | |
| case AVIF_APP_FILE_FORMAT_JPEG: | |
| return "JPEG"; | |
| case AVIF_APP_FILE_FORMAT_PNG: | |
| return "PNG"; | |
| case AVIF_APP_FILE_FORMAT_Y4M: | |
| return "Y4M"; | |
| } | |
| return "unknown"; | |
| } | |
| // |a| and |b| hold int32_t values. The int64_t type is used so that we can negate INT32_MIN without | |
| // overflowing int32_t. | |
| static int64_t calcGCD(int64_t a, int64_t b) | |
| { | |
| if (a < 0) { | |
| a *= -1; | |
| } | |
| if (b < 0) { | |
| b *= -1; | |
| } | |
| while (b != 0) { | |
| int64_t r = a % b; | |
| a = b; | |
| b = r; | |
| } | |
| return a; | |
| } | |
| static void printClapFraction(const char * name, int32_t n, int32_t d) | |
| { | |
| printf("%s: %d/%d", name, n, d); | |
| if (d != 0) { | |
| int64_t gcd = calcGCD(n, d); | |
| if (gcd > 1) { | |
| int32_t rn = (int32_t)(n / gcd); | |
| int32_t rd = (int32_t)(d / gcd); | |
| printf(" (%d/%d)", rn, rd); | |
| } | |
| } | |
| } | |
| static void avifImageDumpInternal(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifBool alphaPresent, avifProgressiveState progressiveState) | |
| { | |
| uint32_t width = avif->width; | |
| uint32_t height = avif->height; | |
| if (gridCols && gridRows) { | |
| width *= gridCols; | |
| height *= gridRows; | |
| } | |
| printf(" * Resolution : %ux%u\n", width, height); | |
| printf(" * Bit Depth : %u\n", avif->depth); | |
| printf(" * Format : %s\n", avifPixelFormatToString(avif->yuvFormat)); | |
| if (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV420) { | |
| printf(" * Chroma Sam. Pos: %u\n", avif->yuvChromaSamplePosition); | |
| } | |
| printf(" * Alpha : %s\n", alphaPresent ? (avif->alphaPremultiplied ? "Premultiplied" : "Not premultiplied") : "Absent"); | |
| printf(" * Range : %s\n", (avif->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited"); | |
| printf(" * Color Primaries: %u\n", avif->colorPrimaries); | |
| printf(" * Transfer Char. : %u\n", avif->transferCharacteristics); | |
| printf(" * Matrix Coeffs. : %u\n", avif->matrixCoefficients); | |
| if (avif->icc.size != 0) { | |
| printf(" * ICC Profile : Present (%" AVIF_FMT_ZU " bytes)\n", avif->icc.size); | |
| } else { | |
| printf(" * ICC Profile : Absent\n"); | |
| } | |
| if (avif->xmp.size != 0) { | |
| printf(" * XMP Metadata : Present (%" AVIF_FMT_ZU " bytes)\n", avif->xmp.size); | |
| } else { | |
| printf(" * XMP Metadata : Absent\n"); | |
| } | |
| if (avif->exif.size != 0) { | |
| printf(" * Exif Metadata : Present (%" AVIF_FMT_ZU " bytes)\n", avif->exif.size); | |
| } else { | |
| printf(" * Exif Metadata : Absent\n"); | |
| } | |
| if (avif->transformFlags == AVIF_TRANSFORM_NONE) { | |
| printf(" * Transformations: None\n"); | |
| } else { | |
| printf(" * Transformations:\n"); | |
| if (avif->transformFlags & AVIF_TRANSFORM_PASP) { | |
| printf(" * pasp (Aspect Ratio) : %d/%d\n", (int)avif->pasp.hSpacing, (int)avif->pasp.vSpacing); | |
| } | |
| if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { | |
| printf(" * clap (Clean Aperture): "); | |
| printClapFraction("W", (int32_t)avif->clap.widthN, (int32_t)avif->clap.widthD); | |
| printf(", "); | |
| printClapFraction("H", (int32_t)avif->clap.heightN, (int32_t)avif->clap.heightD); | |
| printf(", "); | |
| printClapFraction("hOff", (int32_t)avif->clap.horizOffN, (int32_t)avif->clap.horizOffD); | |
| printf(", "); | |
| printClapFraction("vOff", (int32_t)avif->clap.vertOffN, (int32_t)avif->clap.vertOffD); | |
| printf("\n"); | |
| avifCropRect cropRect; | |
| avifDiagnostics diag; | |
| avifDiagnosticsClearError(&diag); | |
| avifBool validClap = avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag); | |
| if (validClap) { | |
| printf(" * Valid, derived crop rect: X: %d, Y: %d, W: %d, H: %d%s\n", | |
| cropRect.x, | |
| cropRect.y, | |
| cropRect.width, | |
| cropRect.height, | |
| avifCropRectRequiresUpsampling(&cropRect, avif->yuvFormat) ? " (upsample before cropping)" : ""); | |
| } else { | |
| printf(" * Invalid: %s\n", diag.error); | |
| } | |
| } | |
| if (avif->transformFlags & AVIF_TRANSFORM_IROT) { | |
| printf(" * irot (Rotation) : %u\n", avif->irot.angle); | |
| } | |
| if (avif->transformFlags & AVIF_TRANSFORM_IMIR) { | |
| printf(" * imir (Mirror) : %u (%s)\n", avif->imir.axis, (avif->imir.axis == 0) ? "top-to-bottom" : "left-to-right"); | |
| } | |
| } | |
| printf(" * Progressive : %s\n", avifProgressiveStateToString(progressiveState)); | |
| if (avif->clli.maxCLL > 0 || avif->clli.maxPALL > 0) { | |
| printf(" * CLLI : %hu, %hu\n", avif->clli.maxCLL, avif->clli.maxPALL); | |
| } | |
| printf(" * Gain map : "); | |
| avifImage * gainMapImage = avif->gainMap ? avif->gainMap->image : NULL; | |
| if (gainMapImage != NULL) { | |
| uint32_t gainMapWidth = gainMapImage->width; | |
| uint32_t gainMapHeight = gainMapImage->height; | |
| if (gridCols && gridRows) { | |
| gainMapWidth *= gridCols; | |
| gainMapHeight *= gridRows; | |
| } | |
| printf("%ux%u pixels, %u bit, %s, %s Range, Matrix Coeffs. %u, Base Headroom %.2f (%s), Alternate Headroom %.2f (%s)\n", | |
| gainMapWidth, | |
| gainMapHeight, | |
| gainMapImage->depth, | |
| avifPixelFormatToString(gainMapImage->yuvFormat), | |
| (gainMapImage->yuvRange == AVIF_RANGE_FULL) ? "Full" : "Limited", | |
| gainMapImage->matrixCoefficients, | |
| avif->gainMap->baseHdrHeadroom.d == 0 ? 0 | |
| : (double)avif->gainMap->baseHdrHeadroom.n / avif->gainMap->baseHdrHeadroom.d, | |
| (avif->gainMap->baseHdrHeadroom.n == 0) ? "SDR" : "HDR", | |
| avif->gainMap->alternateHdrHeadroom.d == 0 | |
| ? 0 | |
| : (double)avif->gainMap->alternateHdrHeadroom.n / avif->gainMap->alternateHdrHeadroom.d, | |
| (avif->gainMap->alternateHdrHeadroom.n == 0) ? "SDR" : "HDR"); | |
| printf(" * Alternate image:\n"); | |
| printf(" * Color Primaries: %u\n", avif->gainMap->altColorPrimaries); | |
| printf(" * Transfer Char. : %u\n", avif->gainMap->altTransferCharacteristics); | |
| printf(" * Matrix Coeffs. : %u\n", avif->gainMap->altMatrixCoefficients); | |
| if (avif->gainMap->altICC.size != 0) { | |
| printf(" * ICC Profile : Present (%" AVIF_FMT_ZU " bytes)\n", avif->gainMap->altICC.size); | |
| } else { | |
| printf(" * ICC Profile : Absent\n"); | |
| } | |
| if (avif->gainMap->altDepth) { | |
| printf(" * Bit Depth : %u\n", avif->gainMap->altDepth); | |
| } | |
| if (avif->gainMap->altPlaneCount) { | |
| printf(" * Planes : %u\n", avif->gainMap->altPlaneCount); | |
| } | |
| if (avif->gainMap->altCLLI.maxCLL > 0 || avif->gainMap->altCLLI.maxPALL > 0) { | |
| printf(" * CLLI : %hu, %hu\n", avif->gainMap->altCLLI.maxCLL, avif->gainMap->altCLLI.maxPALL); | |
| } | |
| printf("\n"); | |
| } else if (avif->gainMap != NULL) { | |
| printf("Present (but ignored)\n"); | |
| } else { | |
| printf("Absent\n"); | |
| } | |
| } | |
| void avifImageDump(const avifImage * avif, uint32_t gridCols, uint32_t gridRows, avifProgressiveState progressiveState) | |
| { | |
| const avifBool alphaPresent = avif->alphaPlane && (avif->alphaRowBytes > 0); | |
| avifImageDumpInternal(avif, gridCols, gridRows, alphaPresent, progressiveState); | |
| } | |
| void avifContainerDump(const avifDecoder * decoder) | |
| { | |
| avifImageDumpInternal(decoder->image, 0, 0, decoder->alphaPresent, decoder->progressiveState); | |
| if (decoder->imageSequenceTrackPresent) { | |
| if (decoder->repetitionCount == AVIF_REPETITION_COUNT_INFINITE) { | |
| printf(" * Repeat Count : Infinite\n"); | |
| } else if (decoder->repetitionCount == AVIF_REPETITION_COUNT_UNKNOWN) { | |
| printf(" * Repeat Count : Unknown\n"); | |
| } else { | |
| printf(" * Repeat Count : %d\n", decoder->repetitionCount); | |
| } | |
| } | |
| } | |
| void avifPrintVersions(void) | |
| { | |
| char codecVersions[256]; | |
| avifCodecVersions(codecVersions); | |
| printf("Version: %s (%s)\n", avifVersion(), codecVersions); | |
| unsigned int libyuvVersion = avifLibYUVVersion(); | |
| if (libyuvVersion == 0) { | |
| printf("libyuv : unavailable\n"); | |
| } else { | |
| printf("libyuv : available (%u)\n", libyuvVersion); | |
| } | |
| printf("\n"); | |
| } | |
| avifAppFileFormat avifGuessFileFormat(const char * filename) | |
| { | |
| // Guess from the file header | |
| FILE * f = fopen(filename, "rb"); | |
| if (f) { | |
| uint8_t headerBuffer[144]; | |
| size_t bytesRead = fread(headerBuffer, 1, sizeof(headerBuffer), f); | |
| fclose(f); | |
| if (bytesRead > 0) { | |
| // If the file could be read, use the first bytes to guess the file format. | |
| return avifGuessBufferFileFormat(headerBuffer, bytesRead); | |
| } | |
| } | |
| // If we get here, the file header couldn't be read for some reason. Guess from the extension. | |
| const char * fileExt = strrchr(filename, '.'); | |
| if (!fileExt) { | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| ++fileExt; // skip past the dot | |
| char lowercaseExt[8]; // This only needs to fit up to "jpeg", so this is plenty | |
| const size_t fileExtLen = strlen(fileExt); | |
| if (fileExtLen >= sizeof(lowercaseExt)) { // >= accounts for NULL terminator | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| for (size_t i = 0; i < fileExtLen; ++i) { | |
| lowercaseExt[i] = (char)tolower((unsigned char)fileExt[i]); | |
| } | |
| lowercaseExt[fileExtLen] = 0; | |
| if (!strcmp(lowercaseExt, "avif")) { | |
| return AVIF_APP_FILE_FORMAT_AVIF; | |
| } else if (!strcmp(lowercaseExt, "y4m")) { | |
| return AVIF_APP_FILE_FORMAT_Y4M; | |
| } else if (!strcmp(lowercaseExt, "jpg") || !strcmp(lowercaseExt, "jpeg")) { | |
| return AVIF_APP_FILE_FORMAT_JPEG; | |
| } else if (!strcmp(lowercaseExt, "png")) { | |
| return AVIF_APP_FILE_FORMAT_PNG; | |
| } | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| avifAppFileFormat avifGuessBufferFileFormat(const uint8_t * data, size_t size) | |
| { | |
| if (size == 0) { | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| avifROData header; | |
| header.data = data; | |
| header.size = size; | |
| if (avifPeekCompatibleFileType(&header)) { | |
| return AVIF_APP_FILE_FORMAT_AVIF; | |
| } | |
| static const uint8_t signatureJPEG[2] = { 0xFF, 0xD8 }; | |
| static const uint8_t signaturePNG[8] = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; | |
| static const uint8_t signatureY4M[9] = { 0x59, 0x55, 0x56, 0x34, 0x4D, 0x50, 0x45, 0x47, 0x32 }; // "YUV4MPEG2" | |
| struct avifHeaderSignature | |
| { | |
| avifAppFileFormat format; | |
| const uint8_t * magic; | |
| size_t magicSize; | |
| } signatures[] = { { AVIF_APP_FILE_FORMAT_JPEG, signatureJPEG, sizeof(signatureJPEG) }, | |
| { AVIF_APP_FILE_FORMAT_PNG, signaturePNG, sizeof(signaturePNG) }, | |
| { AVIF_APP_FILE_FORMAT_Y4M, signatureY4M, sizeof(signatureY4M) } }; | |
| const size_t signaturesCount = sizeof(signatures) / sizeof(signatures[0]); | |
| for (size_t signatureIndex = 0; signatureIndex < signaturesCount; ++signatureIndex) { | |
| const struct avifHeaderSignature * const signature = &signatures[signatureIndex]; | |
| if (header.size < signature->magicSize) { | |
| continue; | |
| } | |
| if (!memcmp(header.data, signature->magic, signature->magicSize)) { | |
| return signature->format; | |
| } | |
| } | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| avifAppFileFormat avifReadImage(const char * filename, | |
| avifAppFileFormat inputFormat, | |
| avifPixelFormat requestedFormat, | |
| int requestedDepth, | |
| avifChromaDownsampling chromaDownsampling, | |
| avifBool ignoreColorProfile, | |
| avifBool ignoreExif, | |
| avifBool ignoreXMP, | |
| avifBool ignoreAlpha, | |
| avifBool ignoreGainMap, | |
| uint32_t imageSizeLimit, | |
| avifImage * image, | |
| uint32_t * outDepth, | |
| avifAppSourceTiming * sourceTiming, | |
| struct y4mFrameIterator ** frameIter) | |
| { | |
| if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { | |
| inputFormat = avifGuessFileFormat(filename); | |
| } | |
| if (inputFormat == AVIF_APP_FILE_FORMAT_Y4M) { | |
| if (!y4mRead(filename, ignoreAlpha, imageSizeLimit, image, sourceTiming, frameIter)) { | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| if (outDepth) { | |
| *outDepth = image->depth; | |
| } | |
| } else if (inputFormat == AVIF_APP_FILE_FORMAT_JPEG) { | |
| // imageSizeLimit is also used to limit Exif and XMP metadata here. | |
| if (!avifJPEGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP, ignoreGainMap, imageSizeLimit)) { | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| if (outDepth) { | |
| *outDepth = 8; | |
| } | |
| } else if (inputFormat == AVIF_APP_FILE_FORMAT_PNG) { | |
| if (!avifPNGRead(filename, image, requestedFormat, requestedDepth, chromaDownsampling, ignoreColorProfile, ignoreExif, ignoreXMP, ignoreAlpha, imageSizeLimit, outDepth)) { | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| } else if (inputFormat == AVIF_APP_FILE_FORMAT_UNKNOWN) { | |
| fprintf(stderr, "Unrecognized file format for input file: %s\n", filename); | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } else { | |
| fprintf(stderr, "Unsupported file format %s for input file: %s\n", avifFileFormatToString(inputFormat), filename); | |
| return AVIF_APP_FILE_FORMAT_UNKNOWN; | |
| } | |
| return inputFormat; | |
| } | |
| avifBool avifReadEntireFile(const char * filename, avifRWData * raw) | |
| { | |
| FILE * f = fopen(filename, "rb"); | |
| if (!f) { | |
| return AVIF_FALSE; | |
| } | |
| fseek(f, 0, SEEK_END); | |
| long pos = ftell(f); | |
| if (pos <= 0) { | |
| fclose(f); | |
| return AVIF_FALSE; | |
| } | |
| size_t fileSize = (size_t)pos; | |
| fseek(f, 0, SEEK_SET); | |
| if (avifRWDataRealloc(raw, fileSize) != AVIF_RESULT_OK) { | |
| fclose(f); | |
| return AVIF_FALSE; | |
| } | |
| size_t bytesRead = fread(raw->data, 1, fileSize, f); | |
| fclose(f); | |
| if (bytesRead != fileSize) { | |
| avifRWDataFree(raw); | |
| return AVIF_FALSE; | |
| } | |
| return AVIF_TRUE; | |
| } | |
| void avifImageFixXMP(avifImage * image) | |
| { | |
| // Zero bytes are forbidden in UTF-8 XML: https://en.wikipedia.org/wiki/Valid_characters_in_XML | |
| // Keeping zero bytes in XMP may lead to issues at encoding or decoding. | |
| // For example, the PNG specification forbids null characters in XMP. See avifPNGWrite(). | |
| // The XMP Specification Part 3 says "When XMP is encoded as UTF-8, | |
| // there are no zero bytes in the XMP packet" for GIF. | |
| // Consider a single trailing null character following a non-null character | |
| // as a programming error. Leave other null characters as is. | |
| // See the discussion at https://github.com/AOMediaCodec/libavif/issues/1333. | |
| if (image->xmp.size >= 2 && image->xmp.data[image->xmp.size - 1] == '\0' && image->xmp.data[image->xmp.size - 2] != '\0') { | |
| --image->xmp.size; | |
| } | |
| } | |
| void avifDumpDiagnostics(const avifDiagnostics * diag) | |
| { | |
| if (!*diag->error) { | |
| return; | |
| } | |
| printf("Diagnostics:\n"); | |
| printf(" * %s\n", diag->error); | |
| } | |
| // --------------------------------------------------------------------------- | |
| // avifQueryCPUCount (separated into OS implementations) | |
| #if defined(_WIN32) | |
| // Windows | |
| #include <windows.h> | |
| int avifQueryCPUCount(void) | |
| { | |
| int numCPU; | |
| SYSTEM_INFO sysinfo; | |
| GetSystemInfo(&sysinfo); | |
| numCPU = sysinfo.dwNumberOfProcessors; | |
| return numCPU; | |
| } | |
| #elif defined(__APPLE__) | |
| // Apple | |
| #include <sys/sysctl.h> | |
| int avifQueryCPUCount(void) | |
| { | |
| int mib[4]; | |
| int numCPU; | |
| size_t len = sizeof(numCPU); | |
| /* set the mib for hw.ncpu */ | |
| mib[0] = CTL_HW; | |
| mib[1] = HW_AVAILCPU; // alternatively, try HW_NCPU; | |
| /* get the number of CPUs from the system */ | |
| sysctl(mib, 2, &numCPU, &len, NULL, 0); | |
| if (numCPU < 1) { | |
| mib[1] = HW_NCPU; | |
| sysctl(mib, 2, &numCPU, &len, NULL, 0); | |
| if (numCPU < 1) | |
| numCPU = 1; | |
| } | |
| return numCPU; | |
| } | |
| #elif defined(__EMSCRIPTEN__) | |
| // Emscripten | |
| int avifQueryCPUCount(void) | |
| { | |
| return 1; | |
| } | |
| #else | |
| // POSIX | |
| #include <unistd.h> | |
| int avifQueryCPUCount(void) | |
| { | |
| int numCPU = (int)sysconf(_SC_NPROCESSORS_ONLN); | |
| return (numCPU > 0) ? numCPU : 1; | |
| } | |
| #endif | |
| // Returns the best cell size for a given horizontal or vertical dimension. | |
| avifBool avifGetBestCellSize(const char * dimensionStr, uint32_t numPixels, uint32_t numCells, avifBool isSubsampled, uint32_t * cellSize) | |
| { | |
| assert(numPixels); | |
| assert(numCells); | |
| // ISO/IEC 23008-12:2017, Section 6.6.2.3.1: | |
| // The reconstructed image is formed by tiling the input images into a grid with a column width | |
| // (potentially excluding the right-most column) equal to tile_width and a row height (potentially | |
| // excluding the bottom-most row) equal to tile_height, without gap or overlap, and then | |
| // trimming on the right and the bottom to the indicated output_width and output_height. | |
| // The priority could be to use a cell size that is a multiple of 64, but there is not always a valid one, | |
| // even though it is recommended by MIAF. Just use ceil(numPixels/numCells) for simplicity and to avoid | |
| // as much padding in the right-most and bottom-most cells as possible. | |
| // Use uint64_t computation to avoid a potential uint32_t overflow. | |
| *cellSize = (uint32_t)(((uint64_t)numPixels + numCells - 1) / numCells); | |
| // ISO/IEC 23000-22:2019, Section 7.3.11.4.2: | |
| // - the tile_width shall be greater than or equal to 64, and should be a multiple of 64 | |
| // - the tile_height shall be greater than or equal to 64, and should be a multiple of 64 | |
| if (*cellSize < 64) { | |
| *cellSize = 64; | |
| if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) { | |
| // Some cells would be entirely off-canvas. | |
| fprintf(stderr, "ERROR: There are too many cells %s (%u) to have at least 64 pixels per cell.\n", dimensionStr, numCells); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| // The maximum AV1 frame size is 65536 pixels inclusive. | |
| if (*cellSize > 65536) { | |
| fprintf(stderr, "ERROR: Cell size %u is bigger %s than the maximum frame size 65536.\n", *cellSize, dimensionStr); | |
| return AVIF_FALSE; | |
| } | |
| // ISO/IEC 23000-22:2019, Section 7.3.11.4.2: | |
| // - when the images are in the 4:2:2 chroma sampling format the horizontal tile offsets and widths, | |
| // and the output width, shall be even numbers; | |
| // - when the images are in the 4:2:0 chroma sampling format both the horizontal and vertical tile | |
| // offsets and widths, and the output width and height, shall be even numbers. | |
| if (isSubsampled && (*cellSize & 1)) { | |
| ++*cellSize; | |
| if ((uint64_t)(numCells - 1) * *cellSize >= (uint64_t)numPixels) { | |
| // Some cells would be entirely off-canvas. | |
| fprintf(stderr, "ERROR: Odd cell size %u is forbidden on a %s subsampled image.\n", *cellSize - 1, dimensionStr); | |
| return AVIF_FALSE; | |
| } | |
| } | |
| // Each pixel is covered by exactly one cell, and each cell contains at least one pixel. | |
| assert(((uint64_t)(numCells - 1) * *cellSize < (uint64_t)numPixels) && ((uint64_t)numCells * *cellSize >= (uint64_t)numPixels)); | |
| return AVIF_TRUE; | |
| } | |
| avifBool avifImageSplitGrid(const avifImage * gridSplitImage, uint32_t gridCols, uint32_t gridRows, avifImage ** gridCells) | |
| { | |
| avifBool success = AVIF_FALSE; | |
| uint32_t cellWidth, cellHeight; | |
| avifPixelFormatInfo formatInfo; | |
| avifGetPixelFormatInfo(gridSplitImage->yuvFormat, &formatInfo); | |
| const avifBool isSubsampledX = !formatInfo.monochrome && formatInfo.chromaShiftX; | |
| const avifBool isSubsampledY = !formatInfo.monochrome && formatInfo.chromaShiftY; | |
| if (!avifGetBestCellSize("horizontally", gridSplitImage->width, gridCols, isSubsampledX, &cellWidth) || | |
| !avifGetBestCellSize("vertically", gridSplitImage->height, gridRows, isSubsampledY, &cellHeight)) { | |
| return AVIF_FALSE; | |
| } | |
| const avifBool hasGainMap = gridSplitImage->gainMap && gridSplitImage->gainMap->image; | |
| uint32_t createdCells = 0; | |
| for (uint32_t gridY = 0; gridY < gridRows; ++gridY) { | |
| for (uint32_t gridX = 0; gridX < gridCols; ++gridX) { | |
| uint32_t gridIndex = gridX + (gridY * gridCols); | |
| avifImage * cellImage = avifImageCreateEmpty(); | |
| if (!cellImage) { | |
| fprintf(stderr, "ERROR: Cell creation failed: out of memory\n"); | |
| goto cleanup; | |
| } | |
| gridCells[gridIndex] = cellImage; | |
| assert(gridIndex == createdCells); | |
| createdCells++; | |
| avifCropRect cellRect = { gridX * cellWidth, gridY * cellHeight, cellWidth, cellHeight }; | |
| if (cellRect.x + cellRect.width > gridSplitImage->width) { | |
| cellRect.width = gridSplitImage->width - cellRect.x; | |
| } | |
| if (cellRect.y + cellRect.height > gridSplitImage->height) { | |
| cellRect.height = gridSplitImage->height - cellRect.y; | |
| } | |
| const avifResult copyResult = avifImageSetViewRect(cellImage, gridSplitImage, &cellRect); | |
| if (copyResult != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Cell creation failed: %s\n", avifResultToString(copyResult)); | |
| goto cleanup; | |
| } | |
| if (hasGainMap) { | |
| cellImage->gainMap = avifGainMapCreate(); | |
| if (!cellImage->gainMap) { | |
| fprintf(stderr, "ERROR: Gain map creation failed: out of memory\n"); | |
| goto cleanup; | |
| } | |
| // Copy gain map metadata. | |
| memcpy(cellImage->gainMap, gridSplitImage->gainMap, sizeof(avifGainMap)); | |
| cellImage->gainMap->altICC.data = NULL; // Copied later in this function. | |
| cellImage->gainMap->altICC.size = 0; | |
| cellImage->gainMap->image = NULL; // Set later in this function. | |
| } | |
| } | |
| } | |
| if (hasGainMap) { | |
| avifImage ** gainMapGridCells = NULL; | |
| gainMapGridCells = (avifImage **)calloc(gridCols * gridRows, sizeof(avifImage *)); | |
| if (!gainMapGridCells) { | |
| fprintf(stderr, "ERROR: Memory allocation failed for gain map grid cells\n"); | |
| goto cleanup; | |
| } | |
| if (!avifImageSplitGrid(gridSplitImage->gainMap->image, gridCols, gridRows, gainMapGridCells)) { | |
| free(gainMapGridCells); | |
| goto cleanup; | |
| } | |
| for (uint32_t gridIndex = 0; gridIndex < gridCols * gridRows; ++gridIndex) { | |
| // Ownership of the gain map cell is transferred. | |
| gridCells[gridIndex]->gainMap->image = gainMapGridCells[gridIndex]; | |
| } | |
| free(gainMapGridCells); | |
| } | |
| // Copy over metadata blobs to the first cell since avifImageSetViewRect() does not copy any | |
| // properties that require an allocation. | |
| avifImage * firstCell = gridCells[0]; | |
| if (gridSplitImage->icc.size > 0) { | |
| const avifResult result = avifImageSetProfileICC(firstCell, gridSplitImage->icc.data, gridSplitImage->icc.size); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to set ICC profile on grid cell: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| } | |
| if (gridSplitImage->exif.size > 0) { | |
| const avifResult result = avifRWDataSet(&firstCell->exif, gridSplitImage->exif.data, gridSplitImage->exif.size); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to set Exif metadata on grid cell: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| } | |
| if (gridSplitImage->xmp.size > 0) { | |
| const avifResult result = avifImageSetMetadataXMP(firstCell, gridSplitImage->xmp.data, gridSplitImage->xmp.size); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to set XMP metadata on grid cell: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| } | |
| if (gridSplitImage->gainMap && gridSplitImage->gainMap->image && gridSplitImage->gainMap->altICC.size > 0) { | |
| for (uint32_t i = 0; i < gridCols * gridRows; ++i) { | |
| avifImage * cellImage = gridCells[i]; | |
| const avifResult result = | |
| avifRWDataSet(&cellImage->gainMap->altICC, gridSplitImage->gainMap->altICC.data, gridSplitImage->gainMap->altICC.size); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "ERROR: Failed to set ICC profile on gain map grid cell: %s\n", avifResultToString(result)); | |
| goto cleanup; | |
| } | |
| } | |
| } | |
| success = AVIF_TRUE; | |
| cleanup: | |
| if (!success) { | |
| for (uint32_t i = 0; i < createdCells; ++i) { | |
| avifImageDestroy(gridCells[i]); | |
| gridCells[i] = NULL; | |
| } | |
| } | |
| return success; | |
| } | |
| void avifRGBImageSetViewRect(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifCropRect * cropRect) | |
| { | |
| memset(dstImage, 0, sizeof(avifRGBImage)); | |
| dstImage->width = cropRect->width; | |
| dstImage->height = cropRect->height; | |
| dstImage->depth = srcImage->depth; | |
| dstImage->format = srcImage->format; | |
| dstImage->alphaPremultiplied = srcImage->alphaPremultiplied; | |
| dstImage->isFloat = srcImage->isFloat; | |
| const uint32_t bytesPerPixel = avifRGBImagePixelSize(srcImage); | |
| // This should not overflow if cropRect is a valid crop of the image. | |
| const size_t offset = (size_t)cropRect->y * srcImage->rowBytes + (size_t)cropRect->x * bytesPerPixel; | |
| dstImage->pixels = srcImage->pixels + offset; | |
| dstImage->rowBytes = srcImage->rowBytes; | |
| } | |
| // NOTE: this saves the rotated pixels to a different image. Rotating an image in place is possible, but can be non trivial depending on the angle. | |
| // A 90° rotation can be implemented as a transposition operation followed by mirroring. | |
| // It's the transposition step that is non trivial for non-square images, see https://en.wikipedia.org/wiki/In-place_matrix_transposition | |
| avifResult avifRGBImageRotate(avifRGBImage * dstImage, const avifRGBImage * srcImage, const avifImageRotation * rotation) | |
| { | |
| const uint32_t bytesPerPixel = avifRGBImagePixelSize(srcImage); | |
| const uint8_t angle = rotation->angle; | |
| const uint32_t newWidth = (angle == 0 || angle == 2) ? srcImage->width : srcImage->height; | |
| const uint32_t newHeight = (angle == 0 || angle == 2) ? srcImage->height : srcImage->width; | |
| *dstImage = *srcImage; | |
| dstImage->width = newWidth; | |
| dstImage->height = newHeight; | |
| dstImage->pixels = NULL; | |
| avifResult result = avifRGBImageAllocatePixels(dstImage); | |
| if (result != AVIF_RESULT_OK) { | |
| return result; | |
| } | |
| if (rotation->angle == 0) { | |
| const size_t bytesPerRow = (size_t)bytesPerPixel * srcImage->width; | |
| // 0 degrees. Just copy the rows as is. | |
| for (uint32_t j = 0; j < srcImage->height; ++j) { | |
| memcpy(dstImage->pixels + ((size_t)j * dstImage->rowBytes), srcImage->pixels + ((size_t)j * srcImage->rowBytes), bytesPerRow); | |
| } | |
| } else if (rotation->angle == 1) { | |
| // 90 degrees anti-clockwise. | |
| for (uint32_t j = 0; j < srcImage->height; ++j) { | |
| for (uint32_t i = 0; i < srcImage->width; ++i) { | |
| // Source pixel at (i, j) goes to destination pixel at (j, srcImage->width - 1 - i). | |
| memcpy(dstImage->pixels + ((size_t)(srcImage->width - 1 - i) * dstImage->rowBytes) + ((size_t)j * bytesPerPixel), | |
| srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel), | |
| bytesPerPixel); | |
| } | |
| } | |
| } else if (rotation->angle == 2) { | |
| // 180 degrees. | |
| for (uint32_t j = 0; j < srcImage->height; ++j) { | |
| for (uint32_t i = 0; i < srcImage->width; ++i) { | |
| // Source pixel at (i, j) goes to destination pixel at (srcImage->width - 1 - i, srcImage->height - 1 - j). | |
| memcpy(dstImage->pixels + ((size_t)(srcImage->height - 1 - j) * dstImage->rowBytes) + | |
| ((size_t)(srcImage->width - 1 - i) * bytesPerPixel), | |
| srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel), | |
| bytesPerPixel); | |
| } | |
| } | |
| } else if (rotation->angle == 3) { | |
| // 90 degrees clockwise. | |
| for (uint32_t j = 0; j < srcImage->height; ++j) { | |
| for (uint32_t i = 0; i < srcImage->width; ++i) { | |
| // Source pixel at (i, j) goes to destination pixel at (srcImage->width - 1 - i, j). | |
| memcpy(dstImage->pixels + ((size_t)i * dstImage->rowBytes) + ((size_t)(srcImage->height - 1 - j) * bytesPerPixel), | |
| srcImage->pixels + ((size_t)j * srcImage->rowBytes) + ((size_t)i * bytesPerPixel), | |
| bytesPerPixel); | |
| } | |
| } | |
| } else { | |
| return AVIF_RESULT_INVALID_ARGUMENT; // Invalid angle. | |
| } | |
| return AVIF_RESULT_OK; | |
| } | |
| avifResult avifRGBImageMirror(avifRGBImage * image, const avifImageMirror * mirror) | |
| { | |
| if (mirror->axis == 0) { // Horizontal axis. | |
| const uint32_t bytesPerPixel = avifRGBImagePixelSize(image); | |
| // May be less than image->rowBytes e.g. if image is a cropped view. | |
| const size_t bytesPerRowToMove = (size_t)bytesPerPixel * image->width; | |
| // Top-to-bottom | |
| uint8_t * tempRow = (uint8_t *)avifAlloc(bytesPerRowToMove); | |
| if (!tempRow) { | |
| return AVIF_RESULT_OUT_OF_MEMORY; | |
| } | |
| for (uint32_t y = 0; y < image->height / 2; ++y) { | |
| uint8_t * row1 = &image->pixels[(size_t)y * image->rowBytes]; | |
| uint8_t * row2 = &image->pixels[(size_t)(image->height - 1 - y) * image->rowBytes]; | |
| memcpy(tempRow, row1, bytesPerRowToMove); | |
| memcpy(row1, row2, bytesPerRowToMove); | |
| memcpy(row2, tempRow, bytesPerRowToMove); | |
| } | |
| avifFree(tempRow); | |
| } else if (mirror->axis == 1) { // Vertical axis. | |
| const uint32_t bytesPerPixel = avifRGBImagePixelSize(image); | |
| uint8_t tempPixel[8]; // Max pixel size should be 8 bytes (RGBA 16-bit). | |
| if (bytesPerPixel > sizeof(tempPixel)) { | |
| return AVIF_RESULT_INVALID_ARGUMENT; | |
| } | |
| for (uint32_t y = 0; y < image->height; ++y) { | |
| uint8_t * row = &image->pixels[(size_t)y * image->rowBytes]; | |
| for (uint32_t x = 0; x < image->width / 2; ++x) { | |
| uint8_t * pixel1 = &row[(size_t)x * bytesPerPixel]; | |
| uint8_t * pixel2 = &row[(size_t)(image->width - 1 - x) * bytesPerPixel]; | |
| memcpy(tempPixel, pixel1, bytesPerPixel); | |
| memcpy(pixel1, pixel2, bytesPerPixel); | |
| memcpy(pixel2, tempPixel, bytesPerPixel); | |
| } | |
| } | |
| } else { | |
| return AVIF_RESULT_INVALID_ARGUMENT; // Invalid axis value. | |
| } | |
| return AVIF_RESULT_OK; | |
| } | |
| avifResult avifApplyTransforms(avifRGBImage * dstView, avifRGBImage * srcImage, const avifImage * avif) | |
| { | |
| // ISO/IEC 23000-22 (MIAF), Section 7.3.6.7: | |
| // These properties, if used, shall be indicated to be applied in the following order: | |
| // clean aperture first, then rotation, then mirror. | |
| *dstView = *srcImage; | |
| if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { | |
| avifCropRect cropRect; | |
| avifDiagnostics diag; | |
| if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) && | |
| (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) { | |
| avifRGBImageSetViewRect(dstView, srcImage, &cropRect); | |
| } else { | |
| fprintf(stderr, "Invalid clean aperture box\n"); | |
| return AVIF_RESULT_INVALID_ARGUMENT; | |
| } | |
| } | |
| if (avif->transformFlags & AVIF_TRANSFORM_IROT && avif->irot.angle != 0) { | |
| avifRGBImage tmpRgbImage; | |
| avifResult result = avifRGBImageRotate(&tmpRgbImage, dstView, &avif->irot); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Failed to apply rotation\n"); | |
| avifRGBImageFreePixels(&tmpRgbImage); | |
| return result; | |
| } | |
| // We assume that srcImage owned its pixels and free them before replacing it with tmpRgbImage. | |
| avifRGBImageFreePixels(srcImage); | |
| *srcImage = tmpRgbImage; | |
| *dstView = *srcImage; | |
| } | |
| if (avif->transformFlags & AVIF_TRANSFORM_IMIR) { | |
| avifResult result = avifRGBImageMirror(dstView, &avif->imir); | |
| if (result != AVIF_RESULT_OK) { | |
| fprintf(stderr, "Failed to apply mirror\n"); | |
| return result; | |
| } | |
| } | |
| return AVIF_RESULT_OK; | |
| } | |
| avifResult avifImageCreateView(avifImage * dstImage, const avifImage * srcImage) | |
| { | |
| avifResult res = AVIF_RESULT_OK; | |
| if (!dstImage || !srcImage) { | |
| return AVIF_RESULT_INVALID_ARGUMENT; | |
| } | |
| avifCropRect rect = { 0, 0, srcImage->width, srcImage->height }; | |
| res = avifImageSetViewRect(dstImage, srcImage, &rect); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| if (srcImage->icc.size > 0) { | |
| res = avifImageSetProfileICC(dstImage, srcImage->icc.data, srcImage->icc.size); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| } | |
| // Using avifRWDataSet directly to match the internal avifImageCopy() behavior. | |
| // This avoids re-extracting Exif orientation into irot/imir on an already decoded image. | |
| res = avifRWDataSet(&dstImage->exif, srcImage->exif.data, srcImage->exif.size); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| res = avifImageSetMetadataXMP(dstImage, srcImage->xmp.data, srcImage->xmp.size); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| for (size_t i = 0; i < srcImage->numProperties; ++i) { | |
| if (memcmp(srcImage->properties[i].boxtype, "uuid", 4) == 0) { | |
| res = avifImageAddUUIDProperty(dstImage, | |
| srcImage->properties[i].usertype, | |
| srcImage->properties[i].boxPayload.data, | |
| srcImage->properties[i].boxPayload.size); | |
| } else { | |
| res = avifImageAddOpaqueProperty(dstImage, | |
| srcImage->properties[i].boxtype, | |
| srcImage->properties[i].boxPayload.data, | |
| srcImage->properties[i].boxPayload.size); | |
| } | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| } | |
| if (srcImage->gainMap) { | |
| dstImage->gainMap = avifGainMapCreate(); | |
| if (!dstImage->gainMap) { | |
| return AVIF_RESULT_OUT_OF_MEMORY; | |
| } | |
| // Copy all gain map scalars at once | |
| *dstImage->gainMap = *srcImage->gainMap; | |
| // Reset pointers to prevent shared ownership and double-free | |
| dstImage->gainMap->image = NULL; | |
| dstImage->gainMap->altICC.data = NULL; | |
| dstImage->gainMap->altICC.size = 0; | |
| if (srcImage->gainMap->altICC.size > 0) { | |
| res = avifRWDataSet(&dstImage->gainMap->altICC, srcImage->gainMap->altICC.data, srcImage->gainMap->altICC.size); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| } | |
| if (srcImage->gainMap->image) { | |
| dstImage->gainMap->image = avifImageCreateEmpty(); | |
| if (!dstImage->gainMap->image) { | |
| return AVIF_RESULT_OUT_OF_MEMORY; | |
| } | |
| res = avifImageCreateView(dstImage->gainMap->image, srcImage->gainMap->image); | |
| if (res != AVIF_RESULT_OK) { | |
| return res; | |
| } | |
| } | |
| } | |
| return AVIF_RESULT_OK; | |
| } |