diff --git a/ArcFormats/Ethornell/ArcBGI.cs b/ArcFormats/Ethornell/ArcBGI.cs index 3442d1dc..5263abf7 100644 --- a/ArcFormats/Ethornell/ArcBGI.cs +++ b/ArcFormats/Ethornell/ArcBGI.cs @@ -157,6 +157,8 @@ namespace GameRes.Formats.BGI entry.Type = res.Type; else if (file.View.AsciiEqual (entry.Offset, "BSE 1.")) entry.Type = "image"; + else if (file.View.AsciiEqual (entry.Offset, "CompressedBG")) + entry.Type = "image"; else if (file.View.AsciiEqual (entry.Offset+5, "w ")) entry.Type = "audio"; } diff --git a/ArcFormats/HuneX/ArcHFA.cs b/ArcFormats/HuneX/ArcHFA.cs index 25a4b8bb..c5c611f3 100644 --- a/ArcFormats/HuneX/ArcHFA.cs +++ b/ArcFormats/HuneX/ArcHFA.cs @@ -39,7 +39,7 @@ namespace GameRes.Formats.HuneX { public HfaOpener() { Extensions = new string[] { "hfa" }; - ContainedFormats = new[] { "BGI", "CompressedBG", "BW", "SCR" }; + ContainedFormats = new[] { "BGI", "CompressedBG_MT", "BW", "SCR" }; } public override ArcFile TryOpen(ArcView file) { diff --git a/ArcFormats/HuneX/ArcMZP.cs b/ArcFormats/HuneX/ArcMZP.cs new file mode 100644 index 00000000..210c1e33 --- /dev/null +++ b/ArcFormats/HuneX/ArcMZP.cs @@ -0,0 +1,320 @@ +//! \file ArcMZP.cs +//! \date 2026-02-03 +//! \brief HUNEX General Game Engine multi-frame image container. +// +// Copyright (C) 2026 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using GameRes.Utility; + +namespace GameRes.Formats.HuneX { + internal class MzpMetaData : ImageMetaData { + public uint TileWidth { get; set; } + public uint TileHeight { get; set; } + public uint TileXCount { get; set; } + public uint TileYCount { get; set; } + public uint Characteristics { get; set; } + public uint Depth { get; set; } + public uint TileCrop { get; set; } + public BitmapPalette Palette { get; set; } + } + + internal class MzpArchive : ArcFile { + public MzpMetaData MetaData { get; set; } + + public MzpArchive (ArcView arc, ArchiveFormat impl, ICollection dir, MzpMetaData metadata) : base (arc, impl, dir) { + MetaData = metadata; + } + } + + internal class MzpEntry : Entry { + public int Index { get; set; } + } + + [Export(typeof(ArchiveFormat))] + public class MrgOpener : ArchiveFormat { + public override string Tag { get { return "MZP"; } } + public override string Description { get { return "HuneX general game engine multi-frame image"; } } + public override uint Signature { get { return 0x6467726d; } } // "mrgd" + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + public MrgOpener() { + Extensions = new string[] { "mzp" }; + } + + public override ArcFile TryOpen(ArcView file) { + if (!file.View.AsciiEqual(4, "00")) + return null; + + int count = file.View.ReadInt16(6); + if (!IsSaneCount(count)) + return null; + + var base_name = Path.GetFileNameWithoutExtension(file.Name); + MzpMetaData metadata = null; + + var dir = new List(count); + uint offset = 8; + for (int i = 0; i < count; i++) { + uint section_offset = file.View.ReadUInt16(offset); + uint file_offset = file.View.ReadUInt16(offset + 2); + uint size_boundary = file.View.ReadUInt16(offset + 4); + uint size = file.View.ReadUInt16(offset + 6); + var entry = new MzpEntry { + Offset = 8 * (count + 1) + section_offset * 0x800 + file_offset, + Size = (size_boundary - 1) / 0x20 * 0x800 * 0x20 + size + }; + if (!entry.CheckPlacement(file.MaxOffset)) + return null; + if (i == 0) + metadata = ReadInfo(file, entry); + else { + entry.Name = string.Format("{0}#{1:D3}", base_name, i); + entry.Type = "image"; + entry.Index = i; + dir.Add(entry); + } + offset += 8; + } + + if (metadata == null) + return null; + return new MzpArchive(file, this, dir, metadata); + } + + MzpMetaData ReadInfo(ArcView file, Entry entry) { + if (entry.Size < 16) + return null; + + MzpMetaData metadata = new MzpMetaData(); + metadata.Width = file.View.ReadUInt16(entry.Offset); + metadata.Height = file.View.ReadUInt16(entry.Offset + 2); + metadata.TileWidth = file.View.ReadUInt16(entry.Offset + 4); + metadata.TileHeight = file.View.ReadUInt16(entry.Offset + 6); + metadata.TileXCount = file.View.ReadUInt16(entry.Offset + 8); + metadata.TileYCount = file.View.ReadUInt16(entry.Offset + 10); + ushort type = file.View.ReadUInt16(entry.Offset + 12); + metadata.Characteristics = type; + byte depth = file.View.ReadByte(entry.Offset + 14); + metadata.Depth = depth; + metadata.TileCrop = file.View.ReadByte(entry.Offset + 15); + + if (type == 1 && (depth & 0xf) == 0) + metadata.BPP = 4; + else if (type == 1 && (depth & 0xf) == 1) + metadata.BPP = 8; + else if (type == 8 && depth == 0x14) + metadata.BPP = 24; + else if ((type == 0xb && depth == 0x14) || (type == 0xc && depth == 0x11)) + metadata.BPP = 32; + else + throw new NotImplementedException("[MZP] Unsupported BPP type"); + + if (type == 1) { + int palette_size = (depth & 0xf) == 1 ? 256 : 16; + var raw_palette = new byte[0x400]; + file.View.Read(entry.Offset + 16, raw_palette, 0, (uint)palette_size * 4); + if (depth == 0x11 || depth == 0x91) { + for (int i = 0; i < palette_size; i += 32) { + var block = new byte[32]; + Buffer.BlockCopy(raw_palette, (i + 8) * 4, block, 0, block.Length); + Buffer.BlockCopy(raw_palette, (i + 16) * 4, raw_palette, (i + 8) * 4, block.Length); + Buffer.BlockCopy(block, 0, raw_palette, (i + 16) * 4, block.Length); + } + } + for (int i = palette_size; i < 256; i++) + raw_palette[i * 4 + 3] = 0xFF; + metadata.Palette = MzxImageReader.GetPaletteFromRaw(raw_palette); + } + else { + metadata.Palette = null; + } + + return metadata; + } + + public override IImageDecoder OpenImage(ArcFile arc, Entry entry) { + var marc = arc as MzpArchive; + + byte[] buffer; + if (arc.File.View.AsciiEqual(entry.Offset, "MZX0")) { + buffer = arc.File.View.ReadBytes(entry.Offset + 4, entry.Size - 4); + var decoder = new MzxDecoder(buffer); + buffer = decoder.Unpack(); + } + else + buffer = arc.File.View.ReadBytes(entry.Offset, entry.Size); + + return new MzxImageReader(new BinMemoryStream(buffer), marc.MetaData); + } + } + + internal class MzxImageReader : IImageDecoder { + IBinaryStream m_input; + byte[] m_output; + MzpMetaData m_info; + uint m_height; + uint m_width; + + public byte[] Data { get { return m_output; } } + public PixelFormat Format { get; private set; } + public BitmapPalette Palette { get; private set; } + + public Stream Source { get { m_input.Position = 0; return m_input.AsStream; } } + + public ImageFormat SourceFormat { get { return null; } } + + public ImageMetaData Info { + get { + return new ImageMetaData { + Height = m_height, + Width = m_width, + BPP = Format.BitsPerPixel + }; + } + } + + public ImageData Image { + get { + if (null == m_output) + Unpack(); + return ImageData.Create(Info, Format, Palette, Data); + } + } + + public MzxImageReader(IBinaryStream input, MzpMetaData info) { + m_input = input; + m_info = info; + m_height = m_info.TileHeight; + m_width = m_info.TileWidth; + Palette = m_info.Palette; + switch (m_info.BPP) { + case 4: // Format = PixelFormats.Indexed4; break; + case 8: Format = PixelFormats.Indexed8; break; + case 24: Format = PixelFormats.Bgr24; break; + case 32: Format = PixelFormats.Bgra32; break; + default: throw new InvalidFormatException(); + } + } + + public void Unpack() { + if (m_info.Characteristics == 0xC) { + UnpackHep(); + return; + } + uint tile_size = m_height * m_width; + m_output = new byte[tile_size * (m_info.BPP + 4) / 8]; + + uint index = 0; + switch (m_info.BPP) { + case 4: + byte[] temp4 = new byte[(tile_size + 1) / 2]; + m_input.Read(temp4, 0, temp4.Length); + for (int i = 0; i < temp4.Length; i++) { + m_output[index++] = (byte)(temp4[i] & 0x0F); + if (index < tile_size) + m_output[index++] = (byte)(temp4[i] >> 4); + } + break; + + case 8: + m_input.Read(m_output, 0, m_output.Length); + break; + + case 24: + case 32: + byte[] rgb565 = new byte[tile_size * 2]; + m_input.Read(rgb565, 0, rgb565.Length); + byte[] offsets = new byte[tile_size]; + m_input.Read(offsets, 0, offsets.Length); + byte[] alphas = null; + if (m_info.BPP == 32) { + alphas = new byte[tile_size]; + m_input.Read(alphas, 0, alphas.Length); + } + for (int i = 0; i < tile_size; i++) { + ushort pq = BitConverter.ToUInt16(rgb565, i * 2); + byte offset_byte = offsets[i]; + byte r = (byte)(((pq & 0xF800) >> 8) | ((offset_byte >> 5) & 7)); + byte g = (byte)(((pq & 0x07E0) >> 3) | ((offset_byte >> 3) & 3)); + byte b = (byte)(((pq & 0x001F) << 3) | (offset_byte & 7)); + m_output[index++] = b; + m_output[index++] = g; + m_output[index++] = r; + if (alphas != null) + m_output[index++] = alphas[i]; + } + break; + } + } + + void UnpackHep() { + if (m_input.ReadUInt32() != 0x00504548) // 'HEP\0' + throw new InvalidFormatException(); + m_input.ReadBytes(0x10); + m_width = m_input.ReadUInt32(); + m_height = m_input.ReadUInt32(); + m_input.ReadUInt32(); + Format = PixelFormats.Indexed8; + m_output = new byte[m_height * m_width]; + m_input.Read(m_output, 0, m_output.Length); + + var raw_palette = new byte[0x400]; + m_input.Read(raw_palette, 0, raw_palette.Length); + Palette = GetPaletteFromRaw(raw_palette); + } + + public static BitmapPalette GetPaletteFromRaw(byte[] raw_palette) { + var colors = new Color[raw_palette.Length / 4]; + for (int i = 0; i < raw_palette.Length; i += 4) { + byte r = raw_palette[i]; + byte g = raw_palette[i + 1]; + byte b = raw_palette[i + 2]; + byte a = raw_palette[i + 3]; + + if ((a & 0x80) == 0) + a = (byte)(((a << 1) | (a >> 6)) & 0xFF); + else + a = 0xFF; + + colors[i / 4] = Color.FromArgb(a, r, g, b); + } + return new BitmapPalette(colors); + } + + #region IDisposable Members + bool m_disposed = false; + public void Dispose() { + if (!m_disposed) { + m_input.Dispose(); + m_disposed = true; + } + } + #endregion + } +} diff --git a/ArcFormats/HuneX/Decoder.cs b/ArcFormats/HuneX/Decoder.cs index beab6c52..efbe1129 100644 --- a/ArcFormats/HuneX/Decoder.cs +++ b/ArcFormats/HuneX/Decoder.cs @@ -30,25 +30,25 @@ using System.IO; using System.Linq; namespace GameRes.Formats.HuneX { - internal class HuffmanNode { - public int Weight; - public int Index; - public HuffmanNode Parent; - public HuffmanNode Child0; - public HuffmanNode Child1; - } + internal class HuffmanTree { + internal class HuffmanNode { + public int Weight; + public int Index; + public HuffmanNode Parent; + public HuffmanNode Child0; + public HuffmanNode Child1; + } - internal sealed class HuffmanTree { List m_table; bool m_invert; - public HuffmanTree(int first_real_entry, Dictionary weights, bool invert = false) { - m_table = new List(first_real_entry); + public HuffmanTree(int[] weights, bool invert = false) { + m_table = new List(weights.Length); - for (int i = 0; i < first_real_entry; i++) { + for (int i = 0; i < weights.Length; i++) { m_table.Add(new HuffmanNode { Index = i, - Weight = weights.TryGetValue(i, out int w) ? w : 0 + Weight = weights[i] }); } @@ -84,7 +84,7 @@ namespace GameRes.Formats.HuneX { } } - public int DecodeSequence(MsbBitStream input) { + public int DecodeSequence(IBitStream input) { HuffmanNode node = m_table[m_table.Count - 1]; while (node.Child0 != null || node.Child1 != null) { @@ -138,7 +138,7 @@ namespace GameRes.Formats.HuneX { int fill_entries = ReadIntVL(index_bytes); if (fill_entries == 0) fill_entries = first_real_entry; - var weights = new Dictionary(); + var weights = new int[first_real_entry]; // idk why this can work xD if (first_real_entry * 4 < (index_bits + 4) * fill_entries) { fill_entries = first_real_entry; for (int i = 0; i < fill_entries; i++) { @@ -151,7 +151,7 @@ namespace GameRes.Formats.HuneX { weights[idx] = ReadIntVL(); } } - var tree = new HuffmanTree(first_real_entry, weights, true); + var tree = new HuffmanTree(weights, true); tree.Build(((first_real_entry + 1) * first_real_entry) >> 1); using (var input = new MsbBitStream(m_input, true)) { while (offset < m_unpacked.Length) { @@ -188,4 +188,99 @@ namespace GameRes.Formats.HuneX { return BitConverter.ToInt32(buffer, 0); } } + + internal class RingBuffer { + private readonly T[] m_buffer; + private int m_head; + private int m_tail; + private int m_count; + + public T this[int index] { get { return m_buffer[index]; } } + + public RingBuffer(int capacity) { + m_buffer = new T[capacity]; + } + + public void Append(T item) { + m_buffer[m_head] = item; + m_head = (m_head + 1) % m_buffer.Length; + if (m_count == m_buffer.Length) + m_tail = (m_tail + 1) % m_buffer.Length; + else + m_count++; + } + + public void Append(T[] items) { + foreach (T item in items) + Append(item); + } + } + + internal sealed class MzxDecoder { + Stream m_input; + byte[] m_unpacked; + + public MzxDecoder(byte[] buffer) { + m_unpacked = new byte[BitConverter.ToUInt32(buffer, 0)]; + m_input = new MemoryStream(buffer.Skip(4).ToArray()); + } + + public byte[] Unpack() { + int offset = 0; + int counter = 0; + var ringbuf = new RingBuffer(128); + while (offset < m_unpacked.Length) { + if (counter <= 0) + counter = 0x1000; + byte flag = (byte)m_input.ReadByte(); + int len = flag >> 2; + var buffer = new byte[2]; + switch (flag & 3) { + case 0: // RLE + if (counter != 0x1000) { + buffer[1] = m_unpacked[offset - 1]; + buffer[0] = m_unpacked[offset - 2]; + } + offset = Write2(buffer, offset, len + 1); + break; + case 1: // BACKREF + int k = m_input.ReadByte() * 2 + 2; + buffer = new byte[len * 2 + 2]; + int pos = offset - k; + k = Math.Min(k, buffer.Length); + Buffer.BlockCopy(m_unpacked, pos, buffer, 0, k); + for (pos = k; pos < buffer.Length; pos += k) { + Buffer.BlockCopy(buffer, 0, buffer, pos, Math.Min(k, buffer.Length - pos)); + } + offset = Write2(buffer, offset, 1); + break; + case 2: // RINGBUF + buffer[0] = ringbuf[len * 2]; + buffer[1] = ringbuf[len * 2 + 1]; + offset = Write2(buffer, offset, 1); + counter += len; + break; + case 3: // LITERAL + buffer = new byte[len * 2 + 2]; + m_input.Read(buffer, 0, buffer.Length); + offset = Write2(buffer, offset, 1); + ringbuf.Append(buffer); + break; + } + counter -= len + 1; + } + return m_unpacked; + } + + int Write2(byte[] buffer, int offset, int count) { + for (int i = 0; i < count; i++) { + int bytesToWrite = Math.Min(buffer.Length, m_unpacked.Length - offset); + if (bytesToWrite <= 0) + break; + Buffer.BlockCopy(buffer, 0, m_unpacked, offset, bytesToWrite); + offset += bytesToWrite; + } + return offset; + } + } } diff --git a/ArcFormats/HuneX/ImageCBG.cs b/ArcFormats/HuneX/ImageCBG.cs new file mode 100644 index 00000000..1cc891fe --- /dev/null +++ b/ArcFormats/HuneX/ImageCBG.cs @@ -0,0 +1,240 @@ +//! \file ImageCBG.cs +//! \date 2026-02-19 +//! \brief HUNEX General Game Engine image format. +// +// Copyright (C) 2026 by morkt +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to +// deal in the Software without restriction, including without limitation the +// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +// IN THE SOFTWARE. +// + +using System; +using System.ComponentModel.Composition; +using System.IO; +using System.Windows.Media; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace GameRes.Formats.HuneX +{ + internal class CbgMetaData : ImageMetaData + { + public uint StripeHeight; + } + + [Export(typeof(ImageFormat))] + public class CompressedBGFormat : ImageFormat + { + public override string Tag { get { return "CompressedBG_MT"; } } + public override string Description { get { return "HUNEX General Game Engine compressed image format"; } } + public override uint Signature { get { return 0x706D6F43; } } + + public CompressedBGFormat () + { + Extensions = new string[] { "cbg" }; + } + + public override void Write (Stream file, ImageData image) + { + throw new System.NotImplementedException ("BgiFormat.Write not implemented"); + } + + public override ImageMetaData ReadMetaData (IBinaryStream stream) + { + var header = stream.ReadHeader (0x30); + if (!header.AsciiEqual ("CompressedBG_MT")) + return null; + return new CbgMetaData + { + Width = header.ToUInt32 (0x10), + Height = header.ToUInt32 (0x14), + StripeHeight = header.ToUInt32 (0x18), + BPP = header.ToInt32 (0x1C), + }; + } + + public override ImageData Read (IBinaryStream stream, ImageMetaData info) + { + var meta = (CbgMetaData)info as CbgMetaData; + using (var reader = new CbgReader (stream.AsStream, meta)) + { + reader.Unpack(); + return ImageData.Create (meta, reader.Format, null, reader.Data, reader.Stride); + } + } + } + + internal class CbgReader : LsbBitStream + { + byte[] m_output; + CbgMetaData m_info; + int m_pixel_size; + + public byte[] Data { get { return m_output; } } + public PixelFormat Format { get; private set; } + public int Stride { get; private set; } + + public CbgReader (Stream input, CbgMetaData info) : base (input, true) + { + m_info = info; + m_pixel_size = m_info.BPP / 8; + Stride = (int)info.Width * m_pixel_size; + switch (m_info.BPP) + { + case 32: Format = PixelFormats.Bgra32; break; + case 24: Format = PixelFormats.Bgr24; break; + case 8: Format = PixelFormats.Gray8; break; + default: throw new InvalidFormatException(); + } + } + + public void Unpack () + { + uint count = (m_info.Height + m_info.StripeHeight - 1) / m_info.StripeHeight; + var len = new byte[4]; + var offsets = new uint[count]; + m_output = new byte[Stride * m_info.Height]; + m_input.Position = 0x30; + for (int i = 0; i < count; i++) + { + m_input.Read (len, 0, 4); + offsets[i] = BitConverter.ToUInt32 (len, 0); + } + for (int i = 0; i < count; i++) + { + m_input.Seek (offsets[i], SeekOrigin.Begin); + uint height = (uint)Math.Min (m_info.StripeHeight, m_info.Height - m_info.StripeHeight * i); + m_input.Read (len, 0, 4); + var packed = new byte[BitConverter.ToUInt32 (len, 0)]; + var weights = ReadWeightTable (m_input, 0x100); + var tree = new HuffmanTree (weights); + tree.Build(0x1ff); + HuffmanDecompress (tree, packed); + var stripe_output = new byte[Stride * height]; + UnpackZeros (packed, stripe_output); + ReverseAverageSampling (stripe_output, height); + Buffer.BlockCopy (stripe_output, 0, m_output, (int)(Stride * m_info.StripeHeight * i), stripe_output.Length); + } + } + + static internal int ReadInteger (Stream input) + { + int v = 0; + int code; + int code_length = 0; + do + { + code = input.ReadByte(); + if (-1 == code || code_length >= 32) + return -1; + v |= (code & 0x7f) << code_length; + code_length += 7; + } + while (0 != (code & 0x80)); + return v; + } + + static protected int[] ReadWeightTable (Stream input, int length) + { + int[] leaf_nodes_weight = new int[length]; + for (int i = 0; i < length; ++i) + { + int weight = ReadInteger (input); + if (-1 == weight) + throw new InvalidFormatException ("Invalid compressed stream"); + leaf_nodes_weight[i] = weight; + } + return leaf_nodes_weight; + } + + void HuffmanDecompress (HuffmanTree tree, byte[] output) + { + this.Reset(); + for (int dst = 0; dst < output.Length; dst++) + { + output[dst] = (byte)tree.DecodeSequence (this); + } + } + + void UnpackZeros (byte[] input, byte[] output) + { + int dst = 0; + int dec_zero = 0; + int src = 0; + while (dst < output.Length) + { + int code_length = 0; + int count = 0; + byte code; + do + { + if (src >= input.Length) + return; + + code = input[src++]; + count |= (code & 0x7f) << code_length; + code_length += 7; + } + while (0 != (code & 0x80)); + + if (dst + count > output.Length) + break; + + if (0 == dec_zero) + { + if (src + count > input.Length) + break; + Buffer.BlockCopy (input, src, output, dst, count); + src += count; + } + else + { + for (int i = 0; i < count; ++i) + output[dst+i] = 0; + } + dec_zero ^= 1; + dst += count; + } + } + + void ReverseAverageSampling (byte[] output, uint height) + { + for (int y = 0; y < height; ++y) + { + int line = y * Stride; + for (int x = 0; x < m_info.Width; ++x) + { + int pixel = line + x * m_pixel_size; + for (int p = 0; p < m_pixel_size; p++) + { + int avg = 0; + if (x > 0) + avg += output[pixel + p - m_pixel_size]; + if (y > 0) + avg += output[pixel + p - Stride]; + if (x > 0 && y > 0) + avg /= 2; + if (0 != avg) + output[pixel + p] += (byte)avg; + } + } + } + } + } +}