/// <summary> /// Function to persist a <see cref="IGorgonImage"/> to a stream. /// </summary> /// <param name="imageData">A <see cref="IGorgonImage"/> to persist to the stream.</param> /// <param name="stream">The stream that will receive the image data.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="stream"/>, or the <paramref name="imageData"/> parameter is <b>null</b>.</exception> /// <exception cref="ArgumentEmptyException">Thrown when the <paramref name="stream"/> is read only.</exception> /// <exception cref="NotSupportedException">Thrown when the image data in the stream has a pixel format that is unsupported by the codec.</exception> /// <remarks> /// <para> /// When persisting image data via a codec, the image must have a format that the codec can recognize. This list of supported formats is provided by the <see cref="SupportedPixelFormats"/> /// property. Applications may convert their image data a supported format before saving the data using a codec. /// </para> /// </remarks> public override void SaveToStream(IGorgonImage imageData, Stream stream) { // Ensure that we can actually read this format. We do not perform total pixel conversion on behalf of the user, they are responsible for that. // We will, however, support swizzling and pixel compression (e.g. 32 -> 24 bit). if (Array.IndexOf(_supportedFormats, imageData.Format) == -1) { throw new NotSupportedException(string.Format(Resources.GORIMG_ERR_FORMAT_NOT_SUPPORTED, imageData.Format)); } using (var writer = new GorgonBinaryWriter(stream, true)) { // Write the header for the file before we dump the file contents. TgaHeader header = GetHeader(imageData, out TGAConversionFlags conversionFlags); GorgonPitchLayout destPitch; if ((conversionFlags & TGAConversionFlags.RGB888) == TGAConversionFlags.RGB888) { destPitch = new GorgonPitchLayout(imageData.Width * 3, imageData.Width * 3 * imageData.Height); } else { var formatInfo = new GorgonFormatInfo(imageData.Format); destPitch = formatInfo.GetPitchForFormat(imageData.Width, imageData.Height); } GorgonPitchLayout srcPitch = imageData.Buffers[0].PitchInformation; // If the two pitches are equal and we have no conversion requirements, then just write out the buffer. if ((destPitch == srcPitch) && (conversionFlags == TGAConversionFlags.None)) { writer.WriteValue(ref header); writer.WriteRange(imageData.Buffers[0].Data, count: srcPitch.SlicePitch); return; } unsafe { // Get the pointer to the first mip/array/depth level. byte *srcPointer = (byte *)imageData.Buffers[0].Data; var lineBuffer = new GorgonNativeBuffer <byte>(srcPitch.RowPitch); try { // Persist the working buffer to the stream. writer.WriteValue(ref header); // Write out each scan line. for (int y = 0; y < imageData.Height; y++) { byte *destPtr = (byte *)lineBuffer; if ((conversionFlags & TGAConversionFlags.RGB888) == TGAConversionFlags.RGB888) { ImageUtilities.Compress24BPPScanLine(srcPointer, srcPitch.RowPitch, destPtr, destPitch.RowPitch, (conversionFlags & TGAConversionFlags.Swizzle) == TGAConversionFlags.Swizzle); } else if ((conversionFlags & TGAConversionFlags.Swizzle) == TGAConversionFlags.Swizzle) { ImageUtilities.SwizzleScanline(srcPointer, srcPitch.RowPitch, destPtr, destPitch.RowPitch, imageData.Format, ImageBitFlags.None); } else { ImageUtilities.CopyScanline(srcPointer, srcPitch.RowPitch, destPtr, destPitch.RowPitch, imageData.Format, ImageBitFlags.None); } srcPointer += srcPitch.RowPitch; writer.WriteRange(lineBuffer, count: destPitch.RowPitch); } } finally { lineBuffer?.Dispose(); } } } }
/// <summary> /// Function to perform the copying of image data into the buffer. /// </summary> /// <param name="reader">A reader used to read the data from the source stream.</param> /// <param name="image">Image data.</param> /// <param name="conversionFlags">Flags used to convert the image.</param> private void CopyImageData(GorgonBinaryReader reader, IGorgonImage image, TGAConversionFlags conversionFlags) { // TGA only supports 1 array level, and 1 mip level, so we only need to get the first buffer. IGorgonImageBuffer buffer = image.Buffers[0]; // Determine how large a row is, in bytes. var formatInfo = new GorgonFormatInfo(image.Format); GorgonPitchLayout srcPitch = (conversionFlags & TGAConversionFlags.Expand) == TGAConversionFlags.Expand ? new GorgonPitchLayout(image.Width * 3, image.Width * 3 * image.Height) : formatInfo.GetPitchForFormat(image.Width, image.Height); unsafe { // Otherwise, allocate a buffer for conversion. byte *destPtr = (byte *)buffer.Data; // Adjust destination for inverted axes. if ((conversionFlags & TGAConversionFlags.InvertX) == TGAConversionFlags.InvertX) { destPtr += buffer.PitchInformation.RowPitch - formatInfo.SizeInBytes; } if ((conversionFlags & TGAConversionFlags.InvertY) != TGAConversionFlags.InvertY) { destPtr += (image.Height - 1) * buffer.PitchInformation.RowPitch; } // Used to counter the number of lines to force as opaque. int opaqueLineCount = 0; // The buffer used to hold an uncompressed scanline. GorgonNativeBuffer <byte> lineBuffer = null; try { for (int y = 0; y < image.Height; y++) { // Indicates that the scanline has an alpha of 0 for the entire run. bool lineHasZeroAlpha; if ((conversionFlags & TGAConversionFlags.RLE) == TGAConversionFlags.RLE) { lineHasZeroAlpha = ReadCompressed(reader, image.Width, destPtr, image.Format, conversionFlags); } else { // Read the current scanline into memory. if (lineBuffer == null) { lineBuffer = new GorgonNativeBuffer <byte>(srcPitch.RowPitch); } reader.ReadRange(lineBuffer, count: srcPitch.RowPitch); lineHasZeroAlpha = ReadUncompressed((byte *)lineBuffer, srcPitch.RowPitch, destPtr, image.Format, conversionFlags); } if ((lineHasZeroAlpha) && ((conversionFlags & TGAConversionFlags.SetOpaqueAlpha) == TGAConversionFlags.SetOpaqueAlpha)) { opaqueLineCount++; } // The components of the pixel data in a TGA file need swizzling for 32 bit. if (formatInfo.BitDepth == 32) { ImageUtilities.SwizzleScanline(destPtr, buffer.PitchInformation.RowPitch, destPtr, buffer.PitchInformation.RowPitch, image.Format, ImageBitFlags.None); } if ((conversionFlags & TGAConversionFlags.InvertY) != TGAConversionFlags.InvertY) { destPtr -= buffer.PitchInformation.RowPitch; } else { destPtr += buffer.PitchInformation.RowPitch; } } } finally { lineBuffer?.Dispose(); } if (opaqueLineCount != image.Height) { return; } // Set the alpha to opaque if we don't have any alpha values (i.e. alpha = 0 for all pixels). destPtr = (byte *)buffer.Data; for (int y = 0; y < image.Height; y++) { ImageUtilities.CopyScanline(destPtr, buffer.PitchInformation.RowPitch, destPtr, buffer.PitchInformation.RowPitch, image.Format, ImageBitFlags.OpaqueAlpha); destPtr += buffer.PitchInformation.RowPitch; } } }