/// <summary> /// Function to load an image from a stream. /// </summary> /// <param name="stream">The stream containing the image data to read.</param> /// <param name="size">The size of the image within the stream, in bytes.</param> /// <returns>A <see cref="IGorgonImage"/> containing the image data from the stream.</returns> /// <exception cref="GorgonException">Thrown when the image data in the stream has a pixel format that is unsupported.</exception> protected override IGorgonImage OnDecodeFromStream(Stream stream, long size) { if (Unsafe.SizeOf <TgaHeader>() >= size) { throw new EndOfStreamException(); } using (var reader = new GorgonBinaryReader(stream, true)) { IGorgonImageInfo info = ReadHeader(reader, out TGAConversionFlags flags); IGorgonImage image = new GorgonImage(info); if (DecodingOptions?.SetZeroAlphaAsOpaque ?? true) { flags |= TGAConversionFlags.SetOpaqueAlpha; } CopyImageData(reader, image, flags); stream.Position = size; return(image); } }
/// <summary>Function to determine if the content plugin can open the specified file.</summary> /// <param name="file">The content file to evaluate.</param> /// <param name="fileManager">The content file manager.</param> /// <returns> /// <b>true</b> if the plugin can open the file, or <b>false</b> if not.</returns> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="file" />, or the <paramref name="fileManager"/> parameter is <b>null</b>.</exception> public bool CanOpenContent(IContentFile file, IContentFileManager fileManager) { if (file == null) { throw new ArgumentNullException(nameof(file)); } if (fileManager == null) { throw new ArgumentNullException(nameof(fileManager)); } using (Stream stream = file.OpenRead()) { if (!_ddsCodec.IsReadable(stream)) { return(false); } IGorgonImageInfo metadata = _ddsCodec.GetMetaData(stream); // We won't be supporting 1D images in this editor. if ((metadata.ImageType == ImageType.Image1D) || (metadata.ImageType == ImageType.Unknown)) { return(false); } UpdateFileMetadataAttributes(file.Metadata.Attributes); return(true); } }
/// <summary>Function to load the image file into memory.</summary> /// <param name="file">The stream for the file to load.</param> /// <param name="name">The name of the file.</param> /// <returns>The image data, the virtual file entry for the working file and the original pixel format of the file.</returns> public (IGorgonImage image, IGorgonVirtualFile workingFile, BufferFormat originalFormat) LoadImageFile(Stream file, string name) { IGorgonImage result = null; IGorgonVirtualFile workFile; BufferFormat originalFormat; IGorgonImageInfo imageInfo = DefaultCodec.GetMetaData(file); originalFormat = imageInfo.Format; var formatInfo = new GorgonFormatInfo(imageInfo.Format); // We absolutely need to have an extension, or else the texconv tool will not work. if ((DefaultCodec.CodecCommonExtensions.Count > 0) && (!string.Equals(Path.GetExtension(name), DefaultCodec.CodecCommonExtensions[0], System.StringComparison.OrdinalIgnoreCase))) { _log.Print("Adding DDS extension to working file or else external tools may not be able to read it.", LoggingLevel.Verbose); name = Path.ChangeExtension(name, DefaultCodec.CodecCommonExtensions[0]); } _log.Print($"Copying content file {name} to {ScratchArea.FileSystem.MountPoints.First().PhysicalPath} as working file...", LoggingLevel.Intermediate); // Copy to a working file. using (Stream outStream = ScratchArea.OpenStream(name, FileMode.Create)) { file.CopyTo(outStream); workFile = ScratchArea.FileSystem.GetFile(name); } _log.Print($"{workFile.FullPath} is now the working file for the image editor.", LoggingLevel.Intermediate); if (formatInfo.IsCompressed) { _log.Print($"Image is compressed using [{formatInfo.Format}] as its pixel format.", LoggingLevel.Intermediate); if (_compressor == null) { throw new GorgonException(GorgonResult.CannotRead, string.Format(Resources.GORIMG_ERR_COMPRESSED_FILE, formatInfo.Format)); } _log.Print($"Loading image '{workFile.FullPath}'...", LoggingLevel.Simple); result = _compressor.Decompress(ref workFile, imageInfo); if (result == null) { throw new GorgonException(GorgonResult.CannotRead, string.Format(Resources.GORIMG_ERR_COMPRESSED_FILE, formatInfo.Format)); } _log.Print($"Loaded compressed ([{formatInfo.Format}]) image data as [{result.Format}]", LoggingLevel.Intermediate); } else { _log.Print($"Loading image '{workFile.FullPath}'...", LoggingLevel.Simple); using (Stream workingFileStream = workFile.OpenStream()) { result = DefaultCodec.LoadFromStream(workingFileStream); } } return(result, workFile, originalFormat); }
/// <summary> /// Function to write out the DDS header to the stream. /// </summary> /// <param name="settings">Meta data for the image header.</param> /// <param name="conversionFlags">Flags required for image conversion.</param> /// <returns>A TGA header value, populated with the correct settings.</returns> private TgaHeader GetHeader(IGorgonImageInfo settings, out TGAConversionFlags conversionFlags) { TgaHeader header = default; conversionFlags = TGAConversionFlags.None; if ((settings.Width > 0xFFFF) || (settings.Height > 0xFFFF)) { throw new IOException(string.Format(Resources.GORIMG_ERR_FILE_FORMAT_NOT_CORRECT, Codec)); } header.Width = (ushort)(settings.Width); header.Height = (ushort)(settings.Height); switch (settings.Format) { case BufferFormat.R8G8B8A8_UNorm: case BufferFormat.R8G8B8A8_UNorm_SRgb: header.ImageType = TgaImageType.TrueColor; header.BPP = 32; header.Descriptor = TgaDescriptor.InvertY | TgaDescriptor.RGB888A8; conversionFlags |= TGAConversionFlags.Swizzle; break; case BufferFormat.B8G8R8A8_UNorm: case BufferFormat.B8G8R8A8_UNorm_SRgb: header.ImageType = TgaImageType.TrueColor; header.BPP = 32; header.Descriptor = TgaDescriptor.InvertY | TgaDescriptor.RGB888A8; break; case BufferFormat.B8G8R8X8_UNorm: case BufferFormat.B8G8R8X8_UNorm_SRgb: header.ImageType = TgaImageType.TrueColor; header.BPP = 24; header.Descriptor = TgaDescriptor.InvertY; conversionFlags |= TGAConversionFlags.RGB888; break; case BufferFormat.R8_UNorm: case BufferFormat.A8_UNorm: header.ImageType = TgaImageType.BlackAndWhite; header.BPP = 8; header.Descriptor = TgaDescriptor.InvertY; break; case BufferFormat.B5G5R5A1_UNorm: header.ImageType = TgaImageType.TrueColor; header.BPP = 16; header.Descriptor = TgaDescriptor.InvertY | TgaDescriptor.RGB555A1; break; default: throw new IOException(string.Format(Resources.GORIMG_ERR_FORMAT_NOT_SUPPORTED, settings.Format)); } // Persist to stream. return(header); }
/// <summary> /// Function to decompress a block compressed file into a standard pixel format. /// </summary> /// <param name="imageFile">The file to decompress</param> /// <param name="metadata">The metadata for the file.</param> /// <returns>The decompressed image.</returns> public IGorgonImage Decompress(ref IGorgonVirtualFile imageFile, IGorgonImageInfo metadata) { string directory = Path.GetDirectoryName(imageFile.PhysicalFile.FullPath); IGorgonImage result = null; Process texConvProcess = null; Stream inStream = null; IGorgonVirtualFile decodedFile = null; var info = new ProcessStartInfo { Arguments = $"-f {_d3dFormats[metadata.Format]} -y -m {metadata.MipCount} -fl 12.0 -ft DDS -o \"{directory}\" -nologo -px decoded {imageFile.PhysicalFile.FullPath}\"", ErrorDialog = true, FileName = _texConv.FullName, WorkingDirectory = directory, UseShellExecute = false, #if DEBUG CreateNoWindow = false, #else CreateNoWindow = true, #endif RedirectStandardError = true, RedirectStandardOutput = true }; try { texConvProcess = Process.Start(info); if (texConvProcess == null) { return(null); } texConvProcess.WaitForExit(); _writer.FileSystem.Refresh(); imageFile = _writer.FileSystem.GetFile(imageFile.FullPath); decodedFile = _writer.FileSystem.GetFile(imageFile.Directory.FullPath + "decoded" + imageFile.Name); inStream = decodedFile.OpenStream(); result = _codec.LoadFromStream(inStream); return(result); } finally { texConvProcess?.Dispose(); inStream?.Dispose(); if (decodedFile != null) { _writer.DeleteFile(decodedFile.FullPath); } } }
/// <summary> /// Function to decode an image from a <see cref="Stream"/>. /// </summary> /// <param name="stream">The stream containing the image data to read.</param> /// <param name="size">The size of the image within the stream, in bytes.</param> /// <returns>A <see cref="IGorgonImage"/> containing the image data from the stream.</returns> /// <exception cref="GorgonException">Thrown when the image data in the stream has a pixel format that is unsupported.</exception> /// <remarks> /// <para> /// A codec must implement this method in order to decode the image data. /// </para> /// </remarks> protected override IGorgonImage OnDecodeFromStream(Stream stream, long size) { // Read the image meta data so we'll know how large the data should be. IGorgonImageInfo settings = ReadMetaData(stream); // Calculate the expected size of the image. int dataSize = settings.Width * settings.Height * sizeof(ushort); if ((size - TvHeader.SizeInBytes) != dataSize) { throw new GorgonException(GorgonResult.CannotRead, "The data in the stream is not the same size as the proposed image size."); } // Create our resulting image buffer. var result = new GorgonImage(settings); using (var reader = new GorgonBinaryReader(stream, true)) { // Write each scanline. for (int y = 0; y < settings.Height; ++y) { // Ensure that we move to the next line by the row pitch and not the amount of pixels. // Some images put padding in for alignment reasons which can throw off the data offsets. int ptrPosition = (y * result.Buffers[0].PitchInformation.RowPitch); // Decode the pixels in the scan line for our resulting image. for (int x = 0; x < settings.Width; ++x) { // Get our current pixel. ushort pixel = reader.ReadUInt16(); // Since we encode 1 byte per color component for each pixel, we need to bump up the bit shift // by 8 bits. Once we get above 24 bits we'll start over since we're only working with 4 bytes // per pixel in the destination. // We determine how many bits to shift the pixel based on horizontal positioning. // We assume that the image is based on 4 bytes/pixel. In most cases this value should be // determined by dividing the row pitch by the image width. // Write the color by shifting the byte in the source data to the appropriate byte position. uint color = (uint)(((pixel >> 8) & 0xff) << (8 * (x % 3))); uint alpha = (uint)((pixel & 0xff) << 24); ref uint imagePixel = ref result.ImageData.ReadAs <uint>(ptrPosition); imagePixel = color | alpha; ptrPosition += sizeof(uint); } } }
/// <summary> /// Function to determine if the form can be shown. /// </summary> /// <returns><b>true</b> if the form can be shown, <b>false</b> if not.</returns> private bool CanShowForm() { if ((string.IsNullOrWhiteSpace(_fileManager?.CurrentDirectory)) || (_fileManager.SelectedFile == null)) { return(false); } if ((_fileManager.SelectedFile.Metadata == null) || (!_fileManager.SelectedFile.Metadata.Attributes.TryGetValue(CommonEditorConstants.ContentTypeAttr, out string contentType)) || (!string.Equals(contentType, CommonEditorContentTypes.ImageType, StringComparison.OrdinalIgnoreCase))) { return(false); } Stream fileStream = null; try { fileStream = _fileManager.SelectedFile.OpenRead(); if (!_defaultImageCodec.IsReadable(fileStream)) { return(false); } IGorgonImageInfo metaData = _defaultImageCodec.GetMetaData(fileStream); if ((metaData.ImageType != ImageType.Image2D) && (metaData.ImageType != ImageType.ImageCube)) { return(false); } } catch (Exception ex) { CommonServices.Log.Print($"[ERROR] Cannot open the selected file {_fileManager.SelectedFile.Name}.", LoggingLevel.Simple); CommonServices.Log.LogException(ex); } finally { fileStream?.Dispose(); } return(true); }
/// <summary> /// Initializes a new instance of the <see cref="GorgonImageInfo"/> class. /// </summary> /// <param name="info">The initial image information to copy into this instance.</param> /// <param name="imageType">[Optional] An updated image type.</param> /// <param name="format">[Optional] An updated image pixel format.</param> /// <exception cref="ArgumentNullException">Thrown when the <paramref name="info"/> parameter is <b>null</b>.</exception> /// <exception cref="ArgumentException">Thrown when the <see cref="Format"/> value of the <paramref name="info"/> parameter is set to <see cref="BufferFormat.Unknown"/>.</exception> /// <remarks> /// <para> /// Use this constructor to create a copy of an existing <see cref="IGorgonImageInfo"/> object. /// </para> /// </remarks> public GorgonImageInfo(IGorgonImageInfo info, ImageType?imageType = null, BufferFormat?format = null) { if (info == null) { throw new ArgumentNullException(nameof(info)); } if (info.Format == BufferFormat.Unknown) { throw new ArgumentException(string.Format(Resources.GORIMG_ERR_FORMAT_NOT_SUPPORTED, info.Format), nameof(info.Format)); } Format = format ?? info.Format; ArrayCount = info.ArrayCount.Max(1); Depth = info.Depth; Height = info.Height; Width = info.Width; ImageType = imageType ?? info.ImageType; MipCount = info.MipCount.Max(1); HasPreMultipliedAlpha = info.HasPreMultipliedAlpha; }
/// <summary> /// Function to import an image file from the physical file system into the current image. /// </summary> /// <param name="codec">The codec used to open the file.</param> /// <param name="filePath">The path to the file to import.</param> /// <returns>The source file information, image data, the virtual file entry for the working file and the original pixel format of the file.</returns> public (FileInfo file, IGorgonImage image, IGorgonVirtualFile workingFile, BufferFormat originalFormat) ImportImage(IGorgonImageCodec codec, string filePath) { var file = new FileInfo(filePath); IGorgonImageCodec importCodec = codec; IGorgonImageInfo metaData = null; IGorgonVirtualFile workFile = null; IGorgonImage importImage = null; string workFilePath = $"{Path.GetFileNameWithoutExtension(filePath)}_import_{Guid.NewGuid().ToString("N")}"; // Try to determine if we can actually read the file using an installed codec, if we can't, then try to find a suitable codec. using (FileStream stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { if ((importCodec == null) || (!importCodec.IsReadable(stream))) { importCodec = null; foreach (IGorgonImageCodec newCodec in InstalledCodecs.Codecs.Where(item => (item.CodecCommonExtensions.Count > 0) && (item.CanDecode))) { if (newCodec.IsReadable(stream)) { importCodec = newCodec; break; } } } if (importCodec == null) { throw new GorgonException(GorgonResult.CannotRead, string.Format(Resources.GORIMG_ERR_NO_CODEC, filePath)); } metaData = importCodec.GetMetaData(stream); // We absolutely need to have an extension, or else the texconv tool will not work. var codecExtension = new GorgonFileExtension(importCodec.CodecCommonExtensions[0]); _log.Print($"Adding {codecExtension.Extension} extension to working file or else external tools may not be able to read it.", LoggingLevel.Verbose); workFilePath = $"{workFilePath}.{codecExtension.Extension}"; using (Stream outStream = ScratchArea.OpenStream(workFilePath, FileMode.Create)) { stream.CopyTo(outStream); } } workFile = ScratchArea.FileSystem.GetFile(workFilePath); var formatInfo = new GorgonFormatInfo(metaData.Format); // This is always in DDS format. if (formatInfo.IsCompressed) { _log.Print($"Image is compressed using [{formatInfo.Format}] as its pixel format.", LoggingLevel.Intermediate); if (_compressor == null) { throw new GorgonException(GorgonResult.CannotRead, string.Format(Resources.GORIMG_ERR_COMPRESSED_FILE, formatInfo.Format)); } _log.Print($"Loading image '{workFile.FullPath}'...", LoggingLevel.Simple); importImage = _compressor.Decompress(ref workFile, metaData); if (importImage == null) { throw new GorgonException(GorgonResult.CannotRead, string.Format(Resources.GORIMG_ERR_COMPRESSED_FILE, formatInfo.Format)); } _log.Print($"Loaded compressed ([{formatInfo.Format}]) image data as [{importImage.Format}]", LoggingLevel.Intermediate); } else { using (Stream workStream = workFile.OpenStream()) { importImage = importCodec.LoadFromStream(workStream); } } return(file, importImage, workFile, metaData.Format); }
/// <summary> /// Function to retrieve custom metadata when encoding an image frame. /// </summary> /// <param name="frameIndex">The index of the frame being encoded.</param> /// <param name="settings">The settings for the image being encoded.</param> /// <returns>A dictionary containing the key/value pair describing the metadata to write to the frame, or <b>null</b> if the frame contains no metadata.</returns> protected virtual IReadOnlyDictionary <string, object> GetCustomEncodingMetadata(int frameIndex, IGorgonImageInfo settings) => null;
/// <summary> /// Function to retrieve custom metadata when encoding an image frame. /// </summary> /// <param name="frameIndex">The index of the frame being encoded.</param> /// <param name="settings">The settings for the image being encoded.</param> /// <returns>A dictionary containing the key/value pair describing the metadata to write to the frame, or <b>null</b> if the frame contains no metadata.</returns> protected override IReadOnlyDictionary <string, object> GetCustomEncodingMetadata(int frameIndex, IGorgonImageInfo settings) { var result = new Dictionary <string, object>(); if (EncodingOptions?.Palette != null) { for (int i = 0; i < EncodingOptions.Palette.Count; ++i) { if (!EncodingOptions.Palette[i].Alpha.EqualsEpsilon(0)) { continue; } result["/grctlext/TransparencyFlag"] = true; result["/grctlext/TransparentColorIndex"] = i; } } bool saveAllFrames = EncodingOptions?.SaveAllFrames ?? true; if ((settings == null) || (settings.ArrayCount < 2) || (!saveAllFrames)) { return(result.Count == 0 ? null : result); } // Write out frame delays. ushort delayValue = 0; if ((EncodingOptions?.FrameDelays != null) && (frameIndex >= 0) && (frameIndex < EncodingOptions.FrameDelays.Count)) { delayValue = (ushort)EncodingOptions.FrameDelays[frameIndex]; } result["/grctlext/Delay"] = delayValue; result["/grctlext/Disposal"] = (ushort)1; return(result); }