コード例 #1
0
            private static ItemDataBox CreateItemDataBox(ImageGridMetadata imageGridMetadata, IByteArrayPool arrayPool)
            {
                ImageGridDescriptor imageGridDescriptor = new ImageGridDescriptor(imageGridMetadata);

                byte[] dataBoxBuffer = new byte[imageGridDescriptor.GetSize()];

                MemoryStream stream = null;

                try
                {
                    stream = new MemoryStream(dataBoxBuffer);

                    using (BigEndianBinaryWriter writer = new BigEndianBinaryWriter(stream, leaveOpen: false, arrayPool))
                    {
                        stream = null;

                        // The ImageGridDescriptor is shared between the color and alpha image.
                        imageGridDescriptor.Write(writer);
                    }
                }
                finally
                {
                    stream?.Dispose();
                }

                return(new ItemDataBox(dataBoxBuffer));
            }
コード例 #2
0
 public AvifWriter(IReadOnlyList <CompressedAV1Image> colorImages,
                   IReadOnlyList <CompressedAV1Image> alphaImages,
                   AvifMetadata metadata,
                   ImageGridMetadata imageGridMetadata,
                   YUVChromaSubsampling chromaSubsampling,
                   ColorInformationBox colorInformationBox,
                   ProgressEventHandler progressEventHandler,
                   uint progressDone,
                   uint progressTotal,
                   IByteArrayPool arrayPool)
 {
     this.state                 = new AvifWriterState(colorImages, alphaImages, imageGridMetadata, metadata, arrayPool);
     this.arrayPool             = arrayPool;
     this.colorImageIsGrayscale = chromaSubsampling == YUVChromaSubsampling.Subsampling400;
     this.colorInformationBox   = colorInformationBox;
     this.progressCallback      = progressEventHandler;
     this.progressDone          = progressDone;
     this.progressTotal         = progressTotal;
     this.fileTypeBox           = new FileTypeBox(chromaSubsampling);
     this.metaBox               = new MetaBox(this.state.PrimaryItemId,
                                              this.state.Items.Count,
                                              this.state.MediaDataBoxContentSize > uint.MaxValue,
                                              this.state.ItemDataBox);
     PopulateMetaBox();
 }
コード例 #3
0
        private static Rectangle[] GetTileWindowRectangles(ImageGridMetadata imageGridMetadata, Document document)
        {
            Rectangle[] rects;

            if (imageGridMetadata is null)
            {
                rects = new Rectangle[] { document.Bounds };
            }
            else
            {
                rects = new Rectangle[imageGridMetadata.TileCount];

                // The tiles are encoded from top to bottom then left to right.

                int tileWidth  = checked ((int)imageGridMetadata.TileImageWidth);
                int tileHeight = checked ((int)imageGridMetadata.TileImageHeight);

                for (int row = 0; row < imageGridMetadata.TileRowCount; row++)
                {
                    int startIndex = row * imageGridMetadata.TileColumnCount;
                    int y          = row * tileHeight;

                    for (int col = 0; col < imageGridMetadata.TileColumnCount; col++)
                    {
                        int index = startIndex + col;
                        int x     = col * tileWidth;

                        rects[index] = new Rectangle(x, y, tileWidth, tileHeight);
                    }
                }
            }

            return(rects);
        }
コード例 #4
0
            public AvifWriterState(IReadOnlyList <CompressedAV1Image> colorImages,
                                   IReadOnlyList <CompressedAV1Image> alphaImages,
                                   ImageGridMetadata imageGridMetadata,
                                   AvifMetadata metadata,
                                   IByteArrayPool arrayPool)
            {
                if (colorImages is null)
                {
                    ExceptionUtil.ThrowArgumentNullException(nameof(colorImages));
                }

                if (metadata is null)
                {
                    ExceptionUtil.ThrowArgumentNullException(nameof(metadata));
                }

                if (arrayPool is null)
                {
                    ExceptionUtil.ThrowArgumentNullException(nameof(arrayPool));
                }

                this.ImageGrid = imageGridMetadata;
                this.items     = new List <AvifWriterItem>(GetItemCount(colorImages, alphaImages, metadata));
                Initialize(colorImages, alphaImages, imageGridMetadata, metadata, arrayPool);
            }
コード例 #5
0
 public ImageGridDescriptor(ImageGridMetadata imageGridMetadata)
 {
     this.RowsMinusOne      = (byte)(imageGridMetadata.TileRowCount - 1);
     this.ColumnsMinusOne   = (byte)(imageGridMetadata.TileColumnCount - 1);
     this.OutputWidth       = imageGridMetadata.OutputWidth;
     this.OutputHeight      = imageGridMetadata.OutputHeight;
     this.LargeOutputFields = this.OutputWidth > ushort.MaxValue || this.OutputHeight > ushort.MaxValue;
 }
コード例 #6
0
            private void Initialize(IReadOnlyList <CompressedAV1Image> colorImages,
                                    IReadOnlyList <CompressedAV1Image> alphaImages,
                                    ImageGridMetadata imageGridMetadata,
                                    AvifMetadata metadata,
                                    IByteArrayPool arrayPool)
            {
                ImageStateInfo result;

                if (imageGridMetadata != null)
                {
                    result           = InitializeFromImageGrid(colorImages, alphaImages, imageGridMetadata);
                    this.ItemDataBox = CreateItemDataBox(imageGridMetadata, arrayPool);
                }
                else
                {
                    result           = InitializeFromSingleImage(colorImages[0], alphaImages?[0]);
                    this.ItemDataBox = null;
                }

                uint  itemId = result.NextId;
                ulong mediaDataBoxContentSize = result.MediaDataBoxContentSize;

                byte[] exif = metadata.GetExifBytesReadOnly();
                if (exif != null && exif.Length > 0)
                {
                    AvifWriterItem exifItem = AvifWriterItem.CreateFromExif(itemId, exif);
                    itemId++;
                    exifItem.ItemReferences.Add(new ItemReferenceEntryBox(exifItem.Id, ReferenceTypes.ContentDescription, this.PrimaryItemId));

                    this.items.Add(exifItem);
                    mediaDataBoxContentSize += (ulong)exifItem.ContentBytes.Length;
                }

                byte[] xmp = metadata.GetXmpBytesReadOnly();
                if (xmp != null && xmp.Length > 0)
                {
                    AvifWriterItem xmpItem = AvifWriterItem.CreateFromXmp(itemId, xmp);
                    xmpItem.ItemReferences.Add(new ItemReferenceEntryBox(xmpItem.Id, ReferenceTypes.ContentDescription, this.PrimaryItemId));

                    this.items.Add(xmpItem);
                    mediaDataBoxContentSize += (ulong)xmpItem.ContentBytes.Length;
                }

                this.MediaDataBoxContentSize = mediaDataBoxContentSize;
            }
コード例 #7
0
        private void FillColorImageGrid(CICPColorData?colorInfo, Surface fullSurface)
        {
            this.colorGridInfo.CheckAvailableTileCount();
            DecodeInfo decodeInfo = new DecodeInfo
            {
                expectedWidth  = 0,
                expectedHeight = 0
            };

            IReadOnlyList <uint> childImageIds = this.colorGridInfo.ChildImageIds;
            bool firstTile = true;

            // The tiles are encoded from top to bottom then left to right.

            for (int row = 0; row < this.colorGridInfo.TileRowCount; row++)
            {
                decodeInfo.tileRowIndex = (uint)row;
                int startIndex = row * this.colorGridInfo.TileColumnCount;

                for (int col = 0; col < this.colorGridInfo.TileColumnCount; col++)
                {
                    decodeInfo.tileColumnIndex = (uint)col;

                    DecodeColorImage(childImageIds[startIndex + col], decodeInfo, colorInfo, fullSurface);

                    if (firstTile)
                    {
                        firstTile = false;
                        CheckImageGridAndTileBounds(decodeInfo.expectedWidth,
                                                    decodeInfo.expectedHeight,
                                                    decodeInfo.chromaSubsampling,
                                                    this.colorGridInfo);
                    }
                }
            }

            this.ImageGridMetadata = new ImageGridMetadata(this.colorGridInfo, decodeInfo.expectedHeight, decodeInfo.expectedWidth);
            SetImageColorData(colorInfo, decodeInfo);
        }
コード例 #8
0
        private static ImageGridMetadata TryGetImageGridMetadata(
            Document document,
            CompressionSpeed compressionSpeed,
            YUVChromaSubsampling yuvFormat,
            bool preserveExistingTileSize)
        {
            ImageGridMetadata metadata = null;

            // The VerySlow compression speed always encodes the image as a single tile.
            if (compressionSpeed != CompressionSpeed.VerySlow)
            {
                // The image must have an even size to be eligible for tiling.
                if ((document.Width & 1) == 0 && (document.Height & 1) == 0)
                {
                    if (preserveExistingTileSize)
                    {
                        string value = document.Metadata.GetUserValue(ImageGridName);

                        if (!string.IsNullOrEmpty(value))
                        {
                            ImageGridMetadata serializedData = ImageGridMetadata.TryDeserialize(value);

                            if (serializedData != null &&
                                serializedData.IsValidForImage((uint)document.Width, (uint)document.Height, yuvFormat))
                            {
                                metadata = serializedData;
                            }
                        }
                    }

                    if (metadata is null)
                    {
                        metadata = TryCalculateBestTileSize(document, compressionSpeed);
                    }
                }
            }

            return(metadata);
        }
コード例 #9
0
        public static void Save(Document document,
                                Stream output,
                                int quality,
                                CompressionSpeed compressionSpeed,
                                YUVChromaSubsampling chromaSubsampling,
                                bool preserveExistingTileSize,
                                int?maxEncoderThreadsOverride,
                                Surface scratchSurface,
                                ProgressEventHandler progressCallback,
                                IByteArrayPool arrayPool)
        {
            using (RenderArgs args = new RenderArgs(scratchSurface))
            {
                document.Render(args, true);
            }

            bool grayscale = IsGrayscaleImage(scratchSurface);

            AvifMetadata   metadata = CreateAvifMetadata(document);
            EncoderOptions options  = new EncoderOptions
            {
                quality          = quality,
                compressionSpeed = compressionSpeed,
                // YUV 4:0:0 is always used for gray-scale images because it
                // produces the smallest file size with no quality loss.
                yuvFormat  = grayscale ? YUVChromaSubsampling.Subsampling400 : chromaSubsampling,
                maxThreads = maxEncoderThreadsOverride ?? Environment.ProcessorCount
            };

            // Use BT.709 with sRGB transfer characteristics as the default.
            CICPColorData colorConversionInfo = new CICPColorData
            {
                colorPrimaries          = CICPColorPrimaries.BT709,
                transferCharacteristics = CICPTransferCharacteristics.Srgb,
                matrixCoefficients      = CICPMatrixCoefficients.BT709,
                fullRange = true
            };

            if (quality == 100 && !grayscale)
            {
                // The Identity matrix coefficient places the RGB values into the YUV planes without any conversion.
                // This reduces the compression efficiency, but allows for fully lossless encoding.

                options.yuvFormat = YUVChromaSubsampling.IdentityMatrix;

                // These CICP color values are from the AV1 Bitstream & Decoding Process Specification.
                colorConversionInfo = new CICPColorData
                {
                    colorPrimaries          = CICPColorPrimaries.BT709,
                    transferCharacteristics = CICPTransferCharacteristics.Srgb,
                    matrixCoefficients      = CICPMatrixCoefficients.Identity,
                    fullRange = true
                };
            }
            else
            {
                Metadata docMetadata = document.Metadata;

                // Look for NCLX meta-data if the CICP meta-data was not found.
                // This preserves backwards compatibility with PDN files created by
                // previous versions of this plugin.
                string serializedData = docMetadata.GetUserValue(CICPMetadataName) ?? docMetadata.GetUserValue(NclxMetadataName);

                if (serializedData != null)
                {
                    CICPColorData?colorData = CICPSerializer.TryDeserialize(serializedData);

                    if (colorData.HasValue)
                    {
                        colorConversionInfo = colorData.Value;
                    }
                }
            }

            ImageGridMetadata imageGridMetadata = TryGetImageGridMetadata(document,
                                                                          options.compressionSpeed,
                                                                          options.yuvFormat,
                                                                          preserveExistingTileSize);

            bool hasTransparency = HasTransparency(scratchSurface);

            CompressedAV1ImageCollection colorImages = new CompressedAV1ImageCollection(imageGridMetadata?.TileCount ?? 1);
            CompressedAV1ImageCollection alphaImages = hasTransparency ? new CompressedAV1ImageCollection(colorImages.Capacity) : null;

            // Progress is reported at the following stages:
            // 1. Before converting the image to the YUV color space
            // 2. Before compressing the color image
            // 3. After compressing the color image
            // 4. After compressing the alpha image (if present)
            // 5. After writing the color image to the file
            // 6. After writing the alpha image to the file (if present)

            uint progressDone  = 0;
            uint progressTotal = hasTransparency ? 6U : 4U;

            if (colorImages.Capacity > 1)
            {
                progressTotal *= (uint)colorImages.Capacity;
            }

            try
            {
                Rectangle[] windowRectangles = GetTileWindowRectangles(imageGridMetadata, document);

                for (int i = 0; i < colorImages.Capacity; i++)
                {
                    CompressedAV1Image color = null;
                    CompressedAV1Image alpha = null;

                    try
                    {
                        Rectangle windowRect = windowRectangles[i];
                        using (Surface window = scratchSurface.CreateWindow(windowRect))
                        {
                            if (hasTransparency)
                            {
                                AvifNative.CompressWithTransparency(window,
                                                                    options,
                                                                    ReportCompressionProgress,
                                                                    ref progressDone,
                                                                    progressTotal,
                                                                    colorConversionInfo,
                                                                    out color,
                                                                    out alpha);
                            }
                            else
                            {
                                AvifNative.CompressWithoutTransparency(window,
                                                                       options,
                                                                       ReportCompressionProgress,
                                                                       ref progressDone,
                                                                       progressTotal,
                                                                       colorConversionInfo,
                                                                       out color);
                            }
                        }

                        colorImages.Add(color);
                        color = null;
                        if (hasTransparency)
                        {
                            alphaImages.Add(alpha);
                            alpha = null;
                        }
                    }
                    finally
                    {
                        color?.Dispose();
                        alpha?.Dispose();
                    }
                }


                ColorInformationBox colorInformationBox;

                byte[] iccProfileBytes = metadata.GetICCProfileBytesReadOnly();
                if (iccProfileBytes != null && iccProfileBytes.Length > 0)
                {
                    colorInformationBox = new IccProfileColorInformation(iccProfileBytes);
                }
                else
                {
                    colorInformationBox = new NclxColorInformation(colorConversionInfo.colorPrimaries,
                                                                   colorConversionInfo.transferCharacteristics,
                                                                   colorConversionInfo.matrixCoefficients,
                                                                   colorConversionInfo.fullRange);
                }

                AvifWriter writer = new AvifWriter(colorImages,
                                                   alphaImages,
                                                   metadata,
                                                   imageGridMetadata,
                                                   options.yuvFormat,
                                                   colorInformationBox,
                                                   progressCallback,
                                                   progressDone,
                                                   progressTotal,
                                                   arrayPool);
                writer.WriteTo(output);
            }
            finally
            {
                colorImages?.Dispose();
                alphaImages?.Dispose();
            }

            bool ReportCompressionProgress(uint done, uint total)
            {
                try
                {
                    progressCallback?.Invoke(null, new ProgressEventArgs(((double)done / total) * 100.0, true));
                    return(true);
                }
                catch (OperationCanceledException)
                {
                    return(false);
                }
            }
        }
コード例 #10
0
        private static ImageGridMetadata TryCalculateBestTileSize(
            Document document,
            CompressionSpeed compressionSpeed)
        {
            // Although the HEIF specification (ISO/IEC 23008-12:2017) allows an image grid to have up to 256 tiles
            // in each direction (65536 total), the ISO base media file format (ISO/IEC 14496-12:2015) limits
            // an item reference box to 65535 items.
            // Because of this we limit the maximum number of tiles to 250.
            //
            // While this would result in the image using 62500 tiles in the worst case, it allows
            // memory usage to be minimized when encoding extremely wide and/or tall images.
            //
            // For example, a 65536x65536 pixel image would use a 128x128 grid of 512x512 pixel tiles.
            const int MaxTileCount = 250;
            // The MIAF specification (ISO/IEC 23000-22:2019) requires that the tile size be at least 64x64 pixels.
            const int MinTileSize = 64;

            int maxTileSize;

            switch (compressionSpeed)
            {
            case CompressionSpeed.Fast:
                maxTileSize = 512;
                break;

            case CompressionSpeed.Medium:
                maxTileSize = 1280;
                break;

            case CompressionSpeed.Slow:
                maxTileSize = 1920;
                break;

            case CompressionSpeed.VerySlow:
                // Tiles are not used for the very slow compression speed.
                return(null);

            default:
                throw new InvalidEnumArgumentException(nameof(compressionSpeed), (int)compressionSpeed, typeof(CompressionSpeed));
            }

            int bestTileColumnCount = 1;
            int bestTileWidth       = document.Width;
            int bestTileRowCount    = 1;
            int bestTileHeight      = document.Height;

            if (document.Width > maxTileSize)
            {
                for (int tileColumnCount = 2; tileColumnCount <= MaxTileCount; tileColumnCount++)
                {
                    int tileWidth = document.Width / tileColumnCount;

                    if (tileWidth < MinTileSize)
                    {
                        break;
                    }

                    if ((tileWidth & 1) == 0 && (tileWidth * tileColumnCount) == document.Width)
                    {
                        bestTileWidth       = tileWidth;
                        bestTileColumnCount = tileColumnCount;

                        if (tileWidth <= maxTileSize)
                        {
                            break;
                        }
                    }
                }
            }

            if (document.Height > maxTileSize)
            {
                if (document.Width == document.Height)
                {
                    // Square images use the same number of horizontal and vertical tiles.
                    bestTileHeight   = bestTileWidth;
                    bestTileRowCount = bestTileColumnCount;
                }
                else
                {
                    for (int tileRowCount = 2; tileRowCount <= MaxTileCount; tileRowCount++)
                    {
                        int tileHeight = document.Height / tileRowCount;

                        if (tileHeight < MinTileSize)
                        {
                            break;
                        }

                        if ((tileHeight & 1) == 0 && (tileHeight * tileRowCount) == document.Height)
                        {
                            bestTileHeight   = tileHeight;
                            bestTileRowCount = tileRowCount;

                            if (tileHeight <= maxTileSize)
                            {
                                break;
                            }
                        }
                    }
                }
            }

            ImageGridMetadata metadata = null;

            if (bestTileColumnCount > 1 || bestTileRowCount > 1)
            {
                metadata = new ImageGridMetadata(bestTileColumnCount,
                                                 bestTileRowCount,
                                                 (uint)document.Height,
                                                 (uint)document.Width,
                                                 (uint)bestTileHeight,
                                                 (uint)bestTileWidth);
            }

            return(metadata);
        }
コード例 #11
0
        private static void AddAvifMetadataToDocument(Document doc, AvifReader reader, IByteArrayPool arrayPool)
        {
            byte[] exifBytes = reader.GetExifData();

            if (exifBytes != null)
            {
                ExifValueCollection exifValues = ExifParser.Parse(exifBytes, arrayPool);

                if (exifValues != null)
                {
                    exifValues.Remove(MetadataKeys.Image.InterColorProfile);
                    // The HEIF specification states that the EXIF orientation tag is only
                    // informational and should not be used to rotate the image.
                    // See https://github.com/strukturag/libheif/issues/227#issuecomment-642165942
                    exifValues.Remove(MetadataKeys.Image.Orientation);

                    foreach (MetadataEntry entry in exifValues)
                    {
                        doc.Metadata.AddExifPropertyItem(entry.CreateExifPropertyItem());
                    }
                }
            }

            CICPColorData?imageColorData = reader.ImageColorData;

            if (imageColorData.HasValue)
            {
                string serializedValue = CICPSerializer.TrySerialize(imageColorData.Value);

                if (serializedValue != null)
                {
                    doc.Metadata.SetUserValue(CICPMetadataName, serializedValue);
                }
            }

            ImageGridMetadata imageGridMetadata = reader.ImageGridMetadata;

            if (imageGridMetadata != null)
            {
                string serializedValue = imageGridMetadata.SerializeToString();

                if (serializedValue != null)
                {
                    doc.Metadata.SetUserValue(ImageGridName, serializedValue);
                }
            }

            byte[] iccProfileBytes = reader.GetICCProfile();

            if (iccProfileBytes != null)
            {
                doc.Metadata.AddExifPropertyItem(ExifSection.Image,
                                                 unchecked ((ushort)ExifTagID.IccProfileData),
                                                 new ExifValue(ExifValueType.Undefined,
                                                               iccProfileBytes));
            }

            byte[] xmpBytes = reader.GetXmpData();

            if (xmpBytes != null)
            {
                XmpPacket xmpPacket = XmpPacket.TryParse(xmpBytes);
                if (xmpPacket != null)
                {
                    doc.Metadata.SetXmpPacket(xmpPacket);
                }
            }
        }
コード例 #12
0
            private ImageStateInfo InitializeFromImageGrid(IReadOnlyList <CompressedAV1Image> colorImages,
                                                           IReadOnlyList <CompressedAV1Image> alphaImages,
                                                           ImageGridMetadata imageGridMetadata)
            {
                ulong mediaDataBoxContentSize = 0;
                uint  itemId = FirstItemId;

                List <uint> colorImageIds = new List <uint>(colorImages.Count);
                List <uint> alphaImageIds = alphaImages != null ? new List <uint>(alphaImages.Count) : null;

                for (int i = 0; i < colorImages.Count; i++)
                {
                    CompressedAV1Image color     = colorImages[i];
                    AvifWriterItem     colorItem = AvifWriterItem.CreateFromImage(itemId, null, color, false);
                    itemId++;
                    colorImageIds.Add(colorItem.Id);
                    this.items.Add(colorItem);
                    mediaDataBoxContentSize += color.Data.ByteLength;

                    if (alphaImages != null)
                    {
                        CompressedAV1Image alpha     = alphaImages[i];
                        AvifWriterItem     alphaItem = AvifWriterItem.CreateFromImage(itemId, null, alpha, true);
                        itemId++;
                        alphaItem.ItemReferences.Add(new ItemReferenceEntryBox(alphaItem.Id, ReferenceTypes.AuxiliaryImage, colorItem.Id));
                        alphaImageIds.Add(alphaItem.Id);

                        this.items.Add(alphaItem);
                        mediaDataBoxContentSize += alpha.Data.ByteLength;
                    }
                }

                ulong gridDescriptorLength;

                if (imageGridMetadata.OutputHeight > ushort.MaxValue || imageGridMetadata.OutputWidth > ushort.MaxValue)
                {
                    gridDescriptorLength = ImageGridDescriptor.LargeDescriptorLength;
                }
                else
                {
                    gridDescriptorLength = ImageGridDescriptor.SmallDescriptorLength;
                }

                AvifWriterItem colorGridItem = AvifWriterItem.CreateFromImageGrid(itemId, "Color", 0, gridDescriptorLength);

                itemId++;
                colorGridItem.ItemReferences.Add(new ItemReferenceEntryBox(colorGridItem.Id, ReferenceTypes.DerivedImage, colorImageIds));

                this.PrimaryItemId = colorGridItem.Id;
                this.items.Add(colorGridItem);

                if (alphaImages != null)
                {
                    // The ImageGridDescriptor is shared between the color and alpha image.
                    AvifWriterItem alphaGridItem = AvifWriterItem.CreateFromImageGrid(itemId, "Alpha", 0, gridDescriptorLength);
                    itemId++;
                    alphaGridItem.ItemReferences.Add(new ItemReferenceEntryBox(alphaGridItem.Id, ReferenceTypes.AuxiliaryImage, colorGridItem.Id));
                    alphaGridItem.ItemReferences.Add(new ItemReferenceEntryBox(alphaGridItem.Id, ReferenceTypes.DerivedImage, alphaImageIds));

                    this.AlphaItemId = alphaGridItem.Id;
                    this.items.Add(alphaGridItem);
                }

                return(new ImageStateInfo(mediaDataBoxContentSize, itemId));
            }