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)); }
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(); }
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); }
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); }
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; }
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; }
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); }
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); }
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); } } }
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); }
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); } } }
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)); }