/// <summary> /// Saves fully formatted image in specified format to byte array. /// </summary> /// <param name="destFormatDetails">Details about destination format.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="mipToSave">Index of mipmap to save directly.</param> /// <param name="removeAlpha">True = Alpha removed. False = Uses threshold value and alpha values to mask RGB FOR DXT1, otherwise completely removed.</param> /// <returns></returns> public byte[] Save(ImageFormats.ImageEngineFormatDetails destFormatDetails, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true) { if (destFormatDetails.Format == ImageEngineFormat.Unknown) { throw new InvalidOperationException("Save format cannot be 'Unknown'"); } AlphaSettings alphaSetting = AlphaSettings.KeepAlpha; if (removeAlpha) { alphaSetting = AlphaSettings.RemoveAlphaChannel; } else if (destFormatDetails.Format == ImageEngineFormat.DDS_DXT2 || destFormatDetails.Format == ImageEngineFormat.DDS_DXT4) { alphaSetting = AlphaSettings.Premultiply; } // If same format and stuff, can just return original data, or chunks of it. if (destFormatDetails.Format == Format) { return(AttemptSaveUsingOriginalData(destFormatDetails, GenerateMips, desiredMaxDimension, mipToSave, alphaSetting)); } else { return(ImageEngine.Save(MipMaps, destFormatDetails, GenerateMips, alphaSetting, desiredMaxDimension, mipToSave)); } }
/// <summary> /// Saves image to byte[]. /// </summary> /// <param name="MipMaps">Mipmaps to save.</param> /// <param name="format">Format to save image as.</param> /// <param name="generateMips">Determines how to handle mipmaps.</param> /// <param name="desiredMaxDimension">Maximum dimension to allow. Resizes if required.</param> /// <param name="mipToSave">Mipmap to save. If > 0, all other mipmaps removed, and this mipmap saved.</param> /// <param name="mergeAlpha">True = Flattens alpha into RGB.</param> /// <returns>Byte[] containing fully formatted image.</returns> internal static byte[] Save(List <MipMap> MipMaps, ImageEngineFormat format, MipHandling generateMips, int desiredMaxDimension, int mipToSave, bool mergeAlpha) { using (MemoryStream ms = new MemoryStream()) { Save(MipMaps, format, ms, generateMips, mergeAlpha, desiredMaxDimension, mipToSave); return(ms.ToArray()); } }
/// <summary> /// Performs a bulk conversion of a bunch of images given conversion parameters. /// </summary> /// <param name="files">List of supported files to be converted.</param> /// <param name="saveFolder">Destination folder of all textures. Can be null if <paramref name="useSourceAsDestination"/> is set.</param> /// <param name="saveMipType">Determines how to handle mipmaps for converted images.</param> /// <param name="useSourceAsDestination">True = Converted images are saved next to the originals.</param> /// <param name="removeAlpha">True = Alpha is removed from converted images.</param> /// <param name="destFormatDetails">Details about destination format.</param> /// <param name="progressReporter">Progress reporting callback.</param> /// <param name="useSourceFormat">No format conversion is performed if possible.</param> /// <returns>Errors</returns> public static async Task <ConcurrentBag <string> > BulkConvert(IEnumerable <string> files, ImageFormats.ImageEngineFormatDetails destFormatDetails, bool useSourceFormat, string saveFolder, MipHandling saveMipType = MipHandling.Default, bool useSourceAsDestination = false, bool removeAlpha = false, IProgress <int> progressReporter = null) { ConcurrentBag <string> Failures = new ConcurrentBag <string>(); // Test if can parallelise uncompressed saving // Below says: Only formats that don't support mips or do but aren't block compressed - can be parallised. Also don't parallelise if using source formats. bool supportsParallel = false; if (!useSourceFormat) { supportsParallel = !useSourceFormat && !destFormatDetails.IsMippable; supportsParallel |= !supportsParallel && !destFormatDetails.IsBlockCompressed; } if (EnableThreading && supportsParallel) { Failures = await DoBulkParallel(files, destFormatDetails, saveFolder, saveMipType, useSourceAsDestination, removeAlpha, progressReporter); } else { foreach (var file in files) { using (ImageEngineImage img = new ImageEngineImage(file)) { // Using source format can only come into this leg of the operation. var saveFormatDetails = useSourceFormat ? img.FormatDetails : destFormatDetails; string filename = useSourceFormat ? Path.GetFileName(file) : Path.GetFileNameWithoutExtension(file) + "." + destFormatDetails.Extension; // This can stay destFormatDetails instead of saveFormatDetails as it shouldn't be able to get here if destFormatDetails not set. string path = Path.Combine(useSourceAsDestination ? Path.GetDirectoryName(file) : saveFolder, filename); path = UsefulThings.General.FindValidNewFileName(path); try { await img.Save(path, saveFormatDetails, saveMipType, removeAlpha : removeAlpha); } catch (Exception e) { Failures.Add(path + " Reason: " + e.ToString()); } } progressReporter?.Report(1); // Value not relevent. } } return(Failures); }
/// <summary> /// Saves image in specified format to file. If file exists, it will be overwritten. /// </summary> /// <param name="destination">File to save to.</param> /// <param name="format">Desired image format.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="mergeAlpha">DXT1 only. True = Uses threshold value and alpha values to mask RGB.</param> /// <param name="mipToSave">Index of mipmap to save as single image.</param> /// <returns>True if success.</returns> public bool Save(string destination, ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool mergeAlpha = false) { using (FileStream fs = new FileStream(destination, FileMode.Create)) return(Save(fs, format, GenerateMips, desiredMaxDimension, mipToSave, mergeAlpha)); }
/// <summary> /// Save mipmaps as given format to stream. /// </summary> /// <param name="MipMaps">List of Mips to save.</param> /// <param name="format">Desired format.</param> /// <param name="destination">Stream to save to.</param> /// <param name="mipChoice">Determines how to handle mipmaps.</param> /// <param name="maxDimension">Maximum value for either image dimension.</param> /// <param name="mergeAlpha">True = alpha flattened down, directly affecting RGB.</param> /// <param name="mipToSave">0 based index on which mipmap to make top of saved image.</param> /// <returns>True on success.</returns> internal static bool Save(List <MipMap> MipMaps, ImageEngineFormat format, Stream destination, MipHandling mipChoice, bool mergeAlpha, int maxDimension = 0, int mipToSave = 0) { Format temp = new Format(format); List <MipMap> newMips = new List <MipMap>(MipMaps); if ((temp.IsMippable && mipChoice == MipHandling.GenerateNew) || (temp.IsMippable && newMips.Count == 1 && mipChoice == MipHandling.Default)) { DDSGeneral.BuildMipMaps(newMips, mergeAlpha); } // KFreon: Resize if asked if (maxDimension != 0 && maxDimension < newMips[0].Width && maxDimension < newMips[0].Height) { if (!UsefulThings.General.IsPowerOfTwo(maxDimension)) { throw new ArgumentException($"{nameof(maxDimension)} must be a power of 2. Got {nameof(maxDimension)} = {maxDimension}"); } // KFreon: Check if there's a mipmap suitable, removes all larger mipmaps var validMipmap = newMips.Where(img => (img.Width == maxDimension && img.Height <= maxDimension) || (img.Height == maxDimension && img.Width <= maxDimension)); // Check if a mip dimension is maxDimension and that the other dimension is equal or smaller if (validMipmap?.Count() != 0) { int index = newMips.IndexOf(validMipmap.First()); newMips.RemoveRange(0, index); } else { // KFreon: Get the amount the image needs to be scaled. Find largest dimension and get it's scale. double scale = maxDimension * 1f / (newMips[0].Width > newMips[0].Height ? newMips[0].Width: newMips[0].Height); // KFreon: No mip. Resize. newMips[0] = Resize(newMips[0], scale, mergeAlpha); } } // KFreon: Ensure we have a power of two for dimensions double fixScale = 0; if (!UsefulThings.General.IsPowerOfTwo(newMips[0].Width) || !UsefulThings.General.IsPowerOfTwo(newMips[0].Height)) { int newWidth = UsefulThings.General.RoundToNearestPowerOfTwo(newMips[0].Width); int newHeigh = UsefulThings.General.RoundToNearestPowerOfTwo(newMips[0].Height); // KFreon: Assuming same scale in both dimensions... fixScale = 1.0 * newWidth / newMips[0].Width; newMips[0] = Resize(newMips[0], fixScale, mergeAlpha); } if (fixScale != 0 || mipChoice == MipHandling.KeepTopOnly) { DestroyMipMaps(newMips, mipToSave); } if (fixScale != 0 && temp.IsMippable && mipChoice != MipHandling.KeepTopOnly) { DDSGeneral.BuildMipMaps(newMips, mergeAlpha); } bool result = false; if (temp.SurfaceFormat.ToString().Contains("DDS")) { result = DDSGeneral.Save(newMips, destination, temp); } else { // KFreon: Try saving with built in codecs var mip = newMips[0]; if (WindowsWICCodecsAvailable) { result = WIC_Codecs.SaveWithCodecs(mip.BaseImage, destination, format); } } if (mipChoice != MipHandling.KeepTopOnly && temp.IsMippable) { // KFreon: Necessary. Must be how I handle the lowest mip levels. i.e. WRONGLY :( // Figure out how big the file should be and make it that size int size = 0; int width = newMips[0].Width; int height = newMips[0].Height; int divisor = 1; if (temp.IsBlockCompressed) { divisor = 4; } while (width >= 1 && height >= 1) { int tempWidth = width; int tempHeight = height; if (temp.IsBlockCompressed) { if (tempWidth < 4) { tempWidth = 4; } if (tempHeight < 4) { tempHeight = 4; } } size += tempWidth / divisor * tempHeight / divisor * temp.BlockSize; width /= 2; height /= 2; } if (size > destination.Length - 128) { byte[] blanks = new byte[size - (destination.Length - 128)]; destination.Write(blanks, 0, blanks.Length); } } return(result); }
static async Task <ConcurrentBag <string> > DoBulkParallel(IEnumerable <string> files, ImageFormats.ImageEngineFormatDetails destFormatDetails, string saveFolder, MipHandling saveMipType = MipHandling.Default, bool useSourceAsDestination = false, bool removeAlpha = false, IProgress <int> progressReporter = null) { ConcurrentBag <string> failures = new ConcurrentBag <string>(); BufferBlock <string> fileNameStore = new BufferBlock <string>(); int maxParallelism = ImageEngine.NumThreads == 1 ? 1 : (ImageEngine.NumThreads == -1 ? Environment.ProcessorCount : ImageEngine.NumThreads); // Define block to perform each conversion var encoder = new TransformBlock <string, Tuple <byte[], string> >(file => { byte[] data = null; string filename = Path.GetFileNameWithoutExtension(file) + "." + destFormatDetails.Extension; string path = Path.Combine(useSourceAsDestination ? Path.GetDirectoryName(file) : saveFolder, filename); path = UsefulThings.General.FindValidNewFileName(path); using (ImageEngineImage img = new ImageEngineImage(file)) { try { data = img.Save(destFormatDetails, saveMipType, removeAlpha: removeAlpha); } catch (Exception e) { failures.Add(path + " Reason: " + e.ToString()); } } progressReporter.Report(1); // Value not relevent. return(new Tuple <byte[], string>(data, path)); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = maxParallelism, BoundedCapacity = maxParallelism }); // Define block to write converted data to disk var diskWriter = new ActionBlock <Tuple <byte[], string> >(tuple => { string path = UsefulThings.General.FindValidNewFileName(tuple.Item2); try { File.WriteAllBytes(path, tuple.Item1); } catch (Exception e) { failures.Add(path + " Reason: " + e.ToString()); } }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 2, BoundedCapacity = maxParallelism }); // Limit to 2 disk write operations at a time, but allow many to be stored in it's buffer. // Link blocks together fileNameStore.LinkTo(encoder, new DataflowLinkOptions { PropagateCompletion = true }); encoder.LinkTo(diskWriter, new DataflowLinkOptions { PropagateCompletion = true }); // Begin production new Action(async() => { foreach (var file in files) { await fileNameStore.SendAsync(file); } fileNameStore.Complete(); }).Invoke(); await diskWriter.Completion; return(failures); }
internal static void TestDDSMipSize(List <MipMap> newMips, ImageFormats.ImageEngineFormatDetails destFormatDetails, int width, int height, out double fixXScale, out double fixYScale, MipHandling mipChoice) { fixXScale = 0; fixYScale = 0; if (destFormatDetails.IsBlockCompressed && (!UsefulThings.General.IsPowerOfTwo(width) || !UsefulThings.General.IsPowerOfTwo(height))) { // If only keeping top mip, and that mip is divisible by 4, it's ok. if ((mipChoice == MipHandling.KeepTopOnly || mipChoice == MipHandling.KeepExisting) && DDSGeneral.CheckSize_DXT(width, height)) { return; } double newWidth = 0; double newHeight = 0; // Takes into account aspect ratio (a little bit) double aspect = width / height; if (aspect > 1) { newWidth = UsefulThings.General.RoundToNearestPowerOfTwo(width); var tempScale = newWidth / width; newHeight = UsefulThings.General.RoundToNearestPowerOfTwo((int)(height * tempScale)); } else { newHeight = UsefulThings.General.RoundToNearestPowerOfTwo(height); var tempScale = newHeight / height; newWidth = UsefulThings.General.RoundToNearestPowerOfTwo((int)(width * tempScale)); } // Little extra bit to allow integer cast from Double with the correct answer. Occasionally dimensions * scale would be 511.99999999998 instead of 512, so adding a little allows the integer cast to return correct value. fixXScale = 1d * newWidth / width + 0.001; fixYScale = 1d * newHeight / height + 0.001; newMips[0] = Resize(newMips[0], fixXScale, fixYScale); } }
/// <summary> /// Saves image in specified format to stream. /// Stream position not reset before or after. /// </summary> /// <param name="destination">Stream to write to at current position.</param> /// <param name="format">Format to save to.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum dimension of saved image. Keeps aspect.</param> /// <param name="mipToSave">Specifies a mipmap to save within the whole.</param> /// <param name="removeAlpha">True = removes alpha. False = Uses threshold value and alpha values to mask RGB FOR DXT1 ONLY, otherwise removes completely.</param> /// <param name="customMasks">Custom user defined masks for DDS colours.</param> public void Save(Stream destination, ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true, List<uint> customMasks = null) { var data = Save(format, GenerateMips, desiredMaxDimension, mipToSave, removeAlpha, customMasks); destination.Write(data, 0, data.Length); }
byte[] AttemptSaveUsingOriginalData(ImageFormats.ImageEngineFormatDetails destFormatDetails, MipHandling GenerateMips, int desiredMaxDimension, int mipToSave, AlphaSettings alphaSetting) { int start = 0; int destStart = 0; int length = OriginalData.Length; int newWidth = Width; int newHeight = Height; DDS_Header tempHeader = null; byte[] data = null; byte[] tempOriginalData = OriginalData; if (destFormatDetails.IsDDS) { destStart = destFormatDetails.HeaderSize; start = destStart; int mipCount = 0; if (mipToSave != 0) { mipCount = 1; newWidth = MipMaps[mipToSave].Width; newHeight = MipMaps[mipToSave].Height; start = ImageFormats.GetCompressedSize(mipToSave, destFormatDetails, Width, Height); length = ImageFormats.GetCompressedSize(1, destFormatDetails, newWidth, newHeight); } else if (desiredMaxDimension != 0 && desiredMaxDimension < Width && desiredMaxDimension < Height) { int index = MipMaps.FindIndex(t => t.Width < desiredMaxDimension && t.Height < desiredMaxDimension); // If none found, do a proper save and see what happens. if (index == -1) { return(ImageEngine.Save(MipMaps, destFormatDetails, GenerateMips, alphaSetting, desiredMaxDimension, mipToSave)); } mipCount -= index; newWidth = MipMaps[index].Width; newHeight = MipMaps[index].Height; start = ImageFormats.GetCompressedSize(index, destFormatDetails, Width, Height); length = ImageFormats.GetCompressedSize(mipCount, destFormatDetails, newWidth, newHeight); } else { if (alphaSetting == AlphaSettings.RemoveAlphaChannel) { // Can't edit alpha directly in premultiplied formats. Not easily anyway. if (destFormatDetails.IsPremultipliedFormat) { return(ImageEngine.Save(MipMaps, destFormatDetails, GenerateMips, alphaSetting, desiredMaxDimension, mipToSave)); } // DDS Formats only switch (destFormatDetails.Format) { // Excluded cos they have no true alpha case ImageEngineFormat.DDS_A8: case ImageEngineFormat.DDS_A8L8: case ImageEngineFormat.DDS_ATI1: case ImageEngineFormat.DDS_ATI2_3Dc: case ImageEngineFormat.DDS_V8U8: case ImageEngineFormat.DDS_G16_R16: case ImageEngineFormat.DDS_G8_L8: case ImageEngineFormat.DDS_R5G6B5: case ImageEngineFormat.DDS_RGB_8: case ImageEngineFormat.DDS_DXT1: break; // Exluded cos they're alpha isn't easily edited case ImageEngineFormat.DDS_DXT2: case ImageEngineFormat.DDS_DXT4: break; // Excluded cos they're currently unsupported case ImageEngineFormat.DDS_CUSTOM: case ImageEngineFormat.DDS_DX10: case ImageEngineFormat.DDS_ARGB_4: break; case ImageEngineFormat.DDS_ABGR_8: case ImageEngineFormat.DDS_ARGB_32F: case ImageEngineFormat.DDS_ARGB_8: case ImageEngineFormat.DDS_DXT3: case ImageEngineFormat.DDS_DXT5: tempOriginalData = new byte[OriginalData.Length]; Array.Copy(OriginalData, tempOriginalData, OriginalData.Length); // Edit alpha values int alphaStart = 128; int alphaJump = 0; byte[] alphaBlock = null; if (destFormatDetails.IsBlockCompressed) { alphaJump = 16; alphaBlock = new byte[8]; for (int i = 0; i < 8; i++) { alphaBlock[i] = 255; } } else { alphaJump = destFormatDetails.ComponentSize * 4; alphaBlock = new byte[destFormatDetails.ComponentSize]; switch (destFormatDetails.ComponentSize) { case 1: alphaBlock[0] = 255; break; case 2: alphaBlock = BitConverter.GetBytes(ushort.MaxValue); break; case 4: alphaBlock = BitConverter.GetBytes(1f); break; } } for (int i = alphaStart; i < OriginalData.Length; i += alphaJump) { Array.Copy(alphaBlock, 0, tempOriginalData, i, alphaBlock.Length); } break; } } switch (GenerateMips) { case MipHandling.KeepExisting: mipCount = NumMipMaps; break; case MipHandling.Default: if (NumMipMaps > 1) { mipCount = NumMipMaps; } else { goto case MipHandling.GenerateNew; // Eww goto... } break; case MipHandling.GenerateNew: ImageEngine.DestroyMipMaps(MipMaps); ImageEngine.TestDDSMipSize(MipMaps, destFormatDetails, Width, Height, out double fixXScale, out double fixYScale, GenerateMips); // Wrong sizing, so can't use original data anyway. if (fixXScale != 0 || fixYScale != 0) { return(ImageEngine.Save(MipMaps, destFormatDetails, GenerateMips, alphaSetting, desiredMaxDimension, mipToSave)); } mipCount = DDSGeneral.BuildMipMaps(MipMaps); // Compress mipmaps excl top byte[] formattedMips = DDSGeneral.Save(MipMaps.GetRange(1, MipMaps.Count - 1), destFormatDetails, alphaSetting); if (formattedMips == null) { return(null); } // Get top mip size and create destination array length = ImageFormats.GetCompressedSize(0, destFormatDetails, newWidth, newHeight); // Should be the length of the top mipmap. data = new byte[formattedMips.Length + length]; // Copy smaller mips to destination Array.Copy(formattedMips, destFormatDetails.HeaderSize, data, length, formattedMips.Length - destFormatDetails.HeaderSize); break; case MipHandling.KeepTopOnly: mipCount = 1; length = ImageFormats.GetCompressedSize(1, destFormatDetails, newWidth, newHeight); break; } } // Header tempHeader = new DDS_Header(mipCount, newHeight, newWidth, destFormatDetails.Format, destFormatDetails.DX10Format); } // Use existing array, otherwise create one. data = data ?? new byte[length]; Array.Copy(tempOriginalData, start, data, destStart, length - destStart); // Write header if existing (DDS Only) if (tempHeader != null) { tempHeader.WriteToArray(data, 0); } return(data); }
/// <summary> /// Saves image in specified format to stream. /// Stream position not reset before or after. /// </summary> /// <param name="destination">Stream to write to at current position.</param> /// <param name="destFormatDetails">Details of destination format</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum dimension of saved image. Keeps aspect.</param> /// <param name="mipToSave">Specifies a mipmap to save within the whole.</param> /// <param name="removeAlpha">True = removes alpha. False = Uses threshold value and alpha values to mask RGB FOR DXT1 ONLY, otherwise removes completely.</param> public void Save(Stream destination, ImageFormats.ImageEngineFormatDetails destFormatDetails, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true) { var data = Save(destFormatDetails, GenerateMips, desiredMaxDimension, mipToSave, removeAlpha); destination.Write(data, 0, data.Length); }
/// <summary> /// Saves image in specified format to file. If file exists, it will be overwritten. /// </summary> /// <param name="destination">File to save to.</param> /// <param name="destFormatDetails">Details of destination format.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="removeAlpha">True = Alpha removed. False = Uses threshold value and alpha values to mask RGB FOR DXT1 ONLY, otherwise removes completely.</param> /// <param name="mipToSave">Index of mipmap to save as single image.</param> public async Task Save(string destination, ImageFormats.ImageEngineFormatDetails destFormatDetails, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true) { var data = Save(destFormatDetails, GenerateMips, desiredMaxDimension, mipToSave, removeAlpha); using (FileStream fs = new FileStream(destination, FileMode.Create)) await fs.WriteAsync(data, 0, data.Length); }
internal static byte[] Save(List <MipMap> mipMaps, DDSFormatDetails destFormatDetails, AlphaSettings alphaSetting, MipHandling mipChoice) { DDSFormatDetails loadedFormatDetails = new DDSFormatDetails(DDSFormat.DDS_ARGB_8); if ((destFormatDetails.IsMippable && mipChoice == MipHandling.GenerateNew) || (destFormatDetails.IsMippable && mipMaps.Count == 1 && mipChoice == MipHandling.Default)) { BuildMipMaps(mipMaps); } // Set compressor for Block Compressed textures Action <byte[], int, int, byte[], int, AlphaSettings, DDSFormatDetails> compressor = destFormatDetails.BlockEncoder; bool needCheckSize = destFormatDetails.IsBlockCompressed; int height = mipMaps[0].Height; int width = mipMaps[0].Width; if (needCheckSize && !CheckSize_DXT(width, height)) { throw new InvalidOperationException($"DXT compression formats require dimensions to be multiples of 4. Got: {width}x{height}."); } // Create header and write to destination DDS_Header header = new DDS_Header(mipMaps.Count, height, width, destFormatDetails.Format, destFormatDetails.DX10Format); int headerLength = destFormatDetails.HeaderSize; int fullSize = GetCompressedSizeOfImage(mipMaps.Count, destFormatDetails, width, height); /*if (destFormatDetails.ComponentSize != 1) * fullSize += (fullSize - headerLength) * destFormatDetails.ComponentSize;*/// Size adjustment for destination to allow for different component sizes. byte[] destination = new byte[fullSize]; header.WriteToArray(destination, 0); int blockSize = destFormatDetails.BlockSize; if (destFormatDetails.IsBlockCompressed) { int mipOffset = headerLength; foreach (MipMap mipmap in mipMaps) { var temp = WriteCompressedMipMap(destination, mipOffset, mipmap, blockSize, compressor, alphaSetting, loadedFormatDetails); if (temp != -1) // When dimensions too low. { mipOffset = temp; } } } else { // UNCOMPRESSED var action = new Action <int>(mipIndex => { if (alphaSetting == AlphaSettings.RemoveAlphaChannel) { // Remove alpha by setting AMask = 0 var ddspf = header.ddspf; ddspf.dwABitMask = 0; header.ddspf = ddspf; } // Get MipOffset int offset = GetMipOffset(mipIndex, destFormatDetails, width, height); WriteUncompressedMipMap(destination, offset, mipMaps[mipIndex], loadedFormatDetails, destFormatDetails, header.ddspf); }); if (EnableThreading) { Parallel.For(0, mipMaps.Count, new ParallelOptions { MaxDegreeOfParallelism = ThreadCount }, (mip, loopState) => { action(mip); }); } else { for (int i = 0; i < mipMaps.Count; i++) { action(i); } } } return(destination); }
/// <summary> /// Saves fully formatted image in specified format to byte array. /// </summary> /// <param name="format">Format to save as.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="mipToSave">Index of mipmap to save directly.</param> /// <param name="removeAlpha">True = Alpha removed. False = Uses threshold value and alpha values to mask RGB FOR DXT1, otherwise completely removed.</param> /// <param name="customMasks">Custom user defined masks for colours.</param> /// <returns></returns> public byte[] Save(ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true, List<uint> customMasks = null) { if (format == ImageEngineFormat.Unknown) throw new InvalidOperationException("Save format cannot be 'Unknown'"); AlphaSettings alphaSetting = AlphaSettings.KeepAlpha; if (removeAlpha) alphaSetting = AlphaSettings.RemoveAlphaChannel; else if (format == ImageEngineFormat.DDS_DXT2 || format == ImageEngineFormat.DDS_DXT4) alphaSetting = AlphaSettings.Premultiply; return ImageEngine.Save(MipMaps, format, GenerateMips, alphaSetting, desiredMaxDimension, mipToSave, customMasks); }
/// <summary> /// Saves fully formatted image in specified format to stream. /// </summary> /// <param name="destination">Stream to save to.</param> /// <param name="format">Format to save as.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="mergeAlpha">ONLY valid when desiredMaxDimension != 0. True = alpha flattened, directly affecting RGB.</param> /// <param name="mipToSave">Selects a certain mip to save. 0 based.</param> /// <returns>True if success</returns> public bool Save(Stream destination, ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool mergeAlpha = false) { return(ImageEngine.Save(MipMaps, format, destination, GenerateMips, mergeAlpha, desiredMaxDimension, mipToSave)); }
/// <summary> /// Save mipmaps as given format to stream. /// </summary> /// <param name="MipMaps">List of Mips to save.</param> /// <param name="mipChoice">Determines how to handle mipmaps.</param> /// <param name="maxDimension">Maximum value for either image dimension.</param> /// <param name="alphaSetting">Determines how to handle alpha.</param> /// <param name="mipToSave">0 based index on which mipmap to make top of saved image.</param> /// <param name="destFormatDetails">Details about the destination format.</param> /// <returns>True on success.</returns> internal static byte[] Save(List <MipMap> MipMaps, ImageFormats.ImageEngineFormatDetails destFormatDetails, MipHandling mipChoice, AlphaSettings alphaSetting, int maxDimension = 0, int mipToSave = 0) { List <MipMap> newMips = new List <MipMap>(MipMaps); int width = newMips[0].Width; int height = newMips[0].Height; if ((destFormatDetails.IsMippable && mipChoice == MipHandling.GenerateNew) || (destFormatDetails.IsMippable && newMips.Count == 1 && mipChoice == MipHandling.Default)) { DDSGeneral.BuildMipMaps(newMips); } // KFreon: Resize if asked if (maxDimension != 0 && maxDimension < width && maxDimension < height) { if (!UsefulThings.General.IsPowerOfTwo(maxDimension)) { throw new ArgumentException($"{nameof(maxDimension)} must be a power of 2. Got {nameof(maxDimension)} = {maxDimension}"); } // KFreon: Check if there's a mipmap suitable, removes all larger mipmaps var validMipmap = newMips.Where(img => (img.Width == maxDimension && img.Height <= maxDimension) || (img.Height == maxDimension && img.Width <= maxDimension)); // Check if a mip dimension is maxDimension and that the other dimension is equal or smaller if (validMipmap?.Count() != 0) { int index = newMips.IndexOf(validMipmap.First()); newMips.RemoveRange(0, index); } else { // KFreon: Get the amount the image needs to be scaled. Find largest dimension and get it's scale. double scale = maxDimension * 1d / (width > height ? width : height); // KFreon: No mip. Resize. newMips[0] = Resize(newMips[0], scale); } } // KFreon: Ensure we have a power of two for dimensions FOR DDS ONLY TestDDSMipSize(newMips, destFormatDetails, width, height, out double fixXScale, out double fixYScale, mipChoice); if (fixXScale != 0 || fixYScale != 0 || mipChoice == MipHandling.KeepTopOnly) { DestroyMipMaps(newMips, mipToSave); } if ((fixXScale != 0 || fixXScale != 0) && destFormatDetails.IsMippable && mipChoice != MipHandling.KeepTopOnly) { DDSGeneral.BuildMipMaps(newMips); } byte[] destination = null; if (destFormatDetails.IsDDS) { destination = DDSGeneral.Save(newMips, destFormatDetails, alphaSetting); } else { // KFreon: Try saving with built in codecs var mip = newMips[0]; // Fix formatting byte[] newPixels = new byte[mip.Width * mip.Height * 4]; for (int i = 0, j = 0; i < newPixels.Length; i++, j += mip.LoadedFormatDetails.ComponentSize) { newPixels[i] = mip.LoadedFormatDetails.ReadByte(mip.Pixels, j); } destination = WIC_Codecs.SaveWithCodecs(newPixels, destFormatDetails.Format, mip.Width, mip.Height, alphaSetting); } return(destination); }
/// <summary> /// Saves fully formatted image in specified format to byte array. /// </summary> /// <param name="format">Format to save as.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="mipToSave">Index of mipmap to save directly.</param> /// <param name="mergeAlpha">ONLY valid when desiredMaxDimension != 0. True = alpha flattened, directly affecting RGB.</param> /// <returns></returns> public byte[] Save(ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool mergeAlpha = false) { return(ImageEngine.Save(MipMaps, format, GenerateMips, desiredMaxDimension, mipToSave, mergeAlpha)); }
/// <summary> /// Saves image in specified format to file. If file exists, it will be overwritten. /// </summary> /// <param name="destination">File to save to.</param> /// <param name="format">Desired image format.</param> /// <param name="GenerateMips">Determines how mipmaps are handled during saving.</param> /// <param name="desiredMaxDimension">Maximum size for saved image. Resizes if required, but uses mipmaps if available.</param> /// <param name="removeAlpha">True = Alpha removed. False = Uses threshold value and alpha values to mask RGB FOR DXT1 ONLY, otherwise removes completely.</param> /// <param name="mipToSave">Index of mipmap to save as single image.</param> /// <param name="customMasks">Custom user defined masks for colours.</param> public async Task Save(string destination, ImageEngineFormat format, MipHandling GenerateMips, int desiredMaxDimension = 0, int mipToSave = 0, bool removeAlpha = true, List<uint> customMasks = null) { var data = Save(format, GenerateMips, desiredMaxDimension, mipToSave, removeAlpha, customMasks); using (FileStream fs = new FileStream(destination, FileMode.Create)) await fs.WriteAsync(data, 0, data.Length); }