Repository: pgrit/SimpleImageIO (language: python)

Issue:
Unicode symbols path or file name cause ERROR: Could not load image file
Hi,
Seems to be you should add and use CharSet.Unicode+ MarshalAs(UnmanagedType.LPWStr) for strings in all pinvoke's declare that send path to native dll and fix fuctions in native dll's
(Maybe I'm wrong and this is not the problem, but simply renaming the file solves the problem, I didn't look at all your code (you better do it)


Code to reproduce (.Net\Net6):
Just 
`New RgbImage("file name or path with unicode symbols")`

Example - name with unicode 'X' symbol (correct Radiance Rgbe\HDR for test):
https://drive.google.com/file/d/1_QqwroWibzEI6C_MVbHakumAMw3FZy1s/view?usp=sharing

This error will occur with all non latin paths or names and this is bad (chiness, cyrillic, etc)

Thx for your project


Discussion / hints:
Hi,
I am aware of this limitation, it has been on my backlog for a while. A proper cross-platform solution will need some tinkering, since there are multiple third-party dependencies that each may or may not have some form of Unicode support, and since Windows requires UTF-16 and Linux/OSX work with UTF-8.
Since I don't have much time at the moment, this will have to remain on the backlog for the time being.

Fix patch:
diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt
index 829d9bd..5cd2ac5 100644
--- a/Core/CMakeLists.txt
+++ b/Core/CMakeLists.txt
@@ -37,7 +37,7 @@ target_sources(SimpleImageIOCore
 
 set_target_properties(SimpleImageIOCore
     PROPERTIES
-        CXX_STANDARD 11
+        CXX_STANDARD 20
         CXX_STANDARD_REQUIRED ON
         CXX_EXTENSIONS OFF
 )
diff --git a/Core/External/tiny_dng_writer.h b/Core/External/tiny_dng_writer.h
index 8c416a1..6ce7b80 100644
--- a/Core/External/tiny_dng_writer.h
+++ b/Core/External/tiny_dng_writer.h
@@ -249,6 +249,11 @@ class DNGWriter {
   /// Returns true upon success.
   bool WriteToFile(const char *filename, std::string *err) const;
 
+  /// Write DNG to a file.
+  /// Return error string to `err` when Write() returns false.
+  /// Returns true upon success.
+  bool WriteToStream(std::ostream& ofs, std::string *err) const;
+
  private:
   bool swap_endian_;
   bool dng_big_endian_;  // Endianness of DNG file.
@@ -1306,17 +1311,7 @@ DNGWriter::DNGWriter(bool big_endian) : dng_big_endian_(big_endian) {
   swap_endian_ = (IsBigEndian() != dng_big_endian_);
 }
 
-bool DNGWriter::WriteToFile(const char *filename, std::string *err) const {
-  std::ofstream ofs(filename, std::ostream::binary);
-
-  if (!ofs) {
-    if (err) {
-      (*err) = "Failed to open file.\n";
-    }
-
-    return false;
-  }
-
+bool DNGWriter::WriteToStream(std::ostream& ofs, std::string *err) const {
   std::ostringstream header;
   bool ret = WriteTIFFVersionHeader(&header, dng_big_endian_);
   if (!ret) {
@@ -1395,6 +1390,20 @@ bool DNGWriter::WriteToFile(const char *filename, std::string *err) const {
   return true;
 }
 
+bool DNGWriter::WriteToFile(const char *filename, std::string *err) const {
+  std::ofstream ofs(filename, std::ostream::binary);
+
+  if (!ofs) {
+    if (err) {
+      (*err) = "Failed to open file.\n";
+    }
+
+    return false;
+  }
+
+  return WriteToStream(ofs, err);
+}
+
 #ifdef __clang__
 #pragma clang diagnostic pop
 #endif
diff --git a/Core/imageio.cpp b/Core/imageio.cpp
index c67ead6..069b5f2 100644
--- a/Core/imageio.cpp
+++ b/Core/imageio.cpp
@@ -8,6 +8,7 @@
 #include <vector>
 #include <mutex>
 #include <cassert>
+#include <filesystem>
 
 constexpr float testfloat = -0.0f;
 static const bool systemIsBigEndian = ((const char*)&testfloat)[0] != 0;
@@ -27,6 +28,7 @@ static const bool systemIsBigEndian = ((const char*)&testfloat)[0] != 0;
 #endif
 
 // #define STB_IMAGE_IMPLEMENTATION (included by tiny_dng_loader below)
+#define STBI_WINDOWS_UTF8
 #include "External/stb_image.h"
 
 #define STB_IMAGE_WRITE_IMPLEMENTATION
@@ -487,7 +489,12 @@ void WriteTiffImage(const float* data, int rowStride, int width, int height, int
     }
 
     std::string err;
-    writer.WriteToFile(filename, &err);
+    std::ofstream out(std::filesystem::path((const char8_t*) filename), std::ios_base::binary);
+    if (!out) {
+        std::cerr << "Could not open file for writing "
+                  << std::filesystem::path((const char8_t*) filename) << std::endl;
+    }
+    writer.WriteToStream(out, &err);
 
     if (!err.empty()) {
         std::cerr << err << std::endl;
@@ -554,10 +561,26 @@ bool WritePngWithFpng(const float* data, int rowStride, int width, int height, i
         return true;
     }
 
+    auto path = std::filesystem::path((const char8_t*) filename);
+
     std::vector<uint8_t> buffer(width * height * numChannels);
     ConvertToSrgbByteImage(data, rowStride, buffer.data(), width, height, numChannels);
 
-    return fpng::fpng_encode_image_to_file(filename, buffer.data(), width, height, numChannels);
+    std::vector<uint8_t> out_buf;
+    if (!fpng::fpng_encode_image_to_memory(buffer.data(), width, height, numChannels, out_buf)) {
+        std::cerr << "ERROR: png encoding failed when writing file: " << path << std::endl;
+        return false;
+    }
+
+    std::ofstream out(path, std::ios_base::binary);
+    if (!out) {
+        std::cerr << "ERROR: Could not open file for writing: " << path << std::endl;
+        return false;
+    }
+
+    out.write((const char*) out_buf.data(), out_buf.size());
+
+    return true;
 }
 
 bool WritePngWithFpngToMemory(std::vector<uint8_t>& data, int rowStride, int width, int height, int numChannels,
@@ -572,7 +595,7 @@ bool WritePngWithFpngToMemory(std::vector<uint8_t>& data, int rowStride, int wid
 }
 
 int CachePfmImage(int* width, int* height, int* numChannels, const char* filename) {
-    std::ifstream in(filename, std::ios_base::binary);
+    std::ifstream in(std::filesystem::path((const char8_t*) filename), std::ios_base::binary);
     if (!in) {
         std::cerr << "ERROR: Could not read file: " << filename << std::endl;
         return -1;
@@ -642,7 +665,7 @@ int CachePfmImage(int* width, int* height, int* numChannels, const char* filenam
 }
 
 void WritePfmImage(const float* data, int rowStride, int width, int height, int numChannels, const char* filename) {
-    std::ofstream out(filename, std::ios_base::binary);
+    std::ofstream out(std::filesystem::path((const char8_t*) filename), std::ios_base::binary);
 
     std::ostringstream str;
     if (numChannels == 1)
diff --git a/README.md b/README.md
index eab9a5b..97d50e6 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ Below, you can find instructions on how to accomplish that.
 ### Dependencies
 
 All dependencies *except OIDN* are header-only and included in the repository. Building requires
-- a C++11 (or newer) compiler
+- a C++20 compiler
 - CMake
 - [.NET 6.0](https://dotnet.microsoft.com/) (or newer)
 - Python &geq; 3.6
diff --git a/SimpleImageIO.Tests/Unicode.cs b/SimpleImageIO.Tests/Unicode.cs
new file mode 100644
index 0000000..e56d06c
--- /dev/null
+++ b/SimpleImageIO.Tests/Unicode.cs
@@ -0,0 +1,63 @@
+using System.Numerics;
+using Xunit;
+
+namespace SimpleImageIO.Tests;
+
+public class Unicode {
+    [Theory]
+    [InlineData("exr")]
+    [InlineData("pfm")]
+    [InlineData("hdr")]
+    [InlineData("png")]
+    [InlineData("tif")]
+    public void WriteUnicodeFilename(string ext)
+    {
+        string filename = $"こんにちは.{ext}";
+        if (System.IO.File.Exists(filename)) System.IO.File.Delete(filename);
+
+        RgbImage image = new(1, 1);
+        image.SetPixel(0, 0, Vector3.UnitY);
+        image.WriteToFile(filename);
+
+        Assert.True(System.IO.File.Exists(filename));
+    }
+
+    [Fact]
+    public void WriteUnicodeFilename_LayeredExr()
+    {
+        string filename = "こんにちは👋.exr";
+        if (System.IO.File.Exists(filename)) System.IO.File.Delete(filename);
+
+        RgbImage image = new(1, 1);
+        image.SetPixel(0, 0, Vector3.UnitY);
+        Layers.WriteToExr(filename, ("test", image));
+
+        Assert.True(System.IO.File.Exists(filename));
+    }
+
+    [Theory]
+    [InlineData("exr")]
+    [InlineData("pfm")]
+    [InlineData("hdr")]
+    [InlineData("tif")]
+    public void ReadUnicodeFilename(string ext)
+    {
+        string filename = $"こんにちは.{ext}";
+
+        RgbImage image = new(1, 1);
+        image.SetPixel(0, 0, Vector3.UnitY);
+        image.WriteToFile($"testunicode.{ext}");
+
+        if (System.IO.File.Exists(filename)) System.IO.File.Delete(filename);
+        System.IO.File.Copy($"testunicode.{ext}", filename);
+
+        RgbImage loaded = new(filename);
+        var pixel = loaded.GetPixel(0, 0);
+
+        Assert.Equal(1, loaded.Width);
+        Assert.Equal(1, loaded.Height);
+        Assert.Equal(0.0f, pixel.R, 4);
+        Assert.Equal(1.0f, pixel.G, 4);
+        Assert.Equal(0.0f, pixel.B, 4);
+    }
+}
\ No newline at end of file
diff --git a/SimpleImageIO/ImageBase.cs b/SimpleImageIO/ImageBase.cs
index 3160d2a..e1d924b 100644
--- a/SimpleImageIO/ImageBase.cs
+++ b/SimpleImageIO/ImageBase.cs
@@ -1,365 +1,362 @@
-using System;
-using System.Diagnostics;
 using System.IO;
 using System.Runtime.InteropServices;
-using System.Threading;
 using System.Runtime.CompilerServices;
 
-namespace SimpleImageIO {
+namespace SimpleImageIO;
+
+/// <summary>
+/// Wraps an image with arbitrarily many channels that is stored in native memory
+/// </summary>
+public unsafe class ImageBase : IDisposable {
     /// <summary>
-    /// Wraps an image with arbitrarily many channels that is stored in native memory
+    /// Width of the image in pixels
     /// </summary>
-    public unsafe class ImageBase : IDisposable {
-        /// <summary>
-        /// Width of the image in pixels
-        /// </summary>
-        public int Width { get; protected set; }
-
-        /// <summary>
-        /// Height of the image in pixels
-        /// </summary>
-        public int Height { get; protected set; }
-
-        /// <summary>
-        /// Number of channels per pixel (e.g., 3 for RGB, 4 for RGBA, ...)
-        /// </summary>
-        public int NumChannels { get; protected set; }
-
-        /// <summary>
-        /// Pointer to the native memory containing the image data
-        /// </summary>
-        internal IntPtr DataPointer;
-
-        private float* dataPtr {
-            [MethodImpl(MethodImplOptions.AggressiveInlining)]
-            get => (float*)DataPointer.ToPointer();
-        }
+    public int Width { get; protected set; }
 
-        /// <summary>
-        /// Creates a new empty image that is not yet managing any data
-        /// </summary>
-        protected ImageBase() {}
-
-        /// <summary>
-        /// Creates an image buffer initialized to zero
-        /// </summary>
-        public ImageBase(int w, int h, int numChannels) {
-            Width = w;
-            Height = h;
-            this.NumChannels = numChannels;
-            Alloc();
-
-            // Zero out the values to avoid undefined contents
-            Unsafe.InitBlock(dataPtr, 0, (uint)(Width * Height * NumChannels * sizeof(float)));
-        }
+    /// <summary>
+    /// Height of the image in pixels
+    /// </summary>
+    public int Height { get; protected set; }
 
-        /// <summary>
-        /// Moves the raw data from one image to another. The source image is empty afterwards and should
-        /// no longer be used. If dest is a derived class, like <see cref="RgbImage" /> the user should make
-        /// sure that the number of channels in src is correct.
-        ///
-        /// This is a potentially unsafe operation, use only if you know what you are doing!
-        /// </summary>
-        public static void Move(ImageBase src, ImageBase dest) {
-            dest.DataPointer = src.DataPointer;
-            src.DataPointer = IntPtr.Zero;
-            dest.Width = src.Width;
-            dest.Height = src.Height;
-            dest.NumChannels = src.NumChannels;
-        }
+    /// <summary>
+    /// Number of channels per pixel (e.g., 3 for RGB, 4 for RGBA, ...)
+    /// </summary>
+    public int NumChannels { get; protected set; }
 
+    /// <summary>
+    /// Pointer to the native memory containing the image data
+    /// </summary>
+    internal IntPtr DataPointer;
+
+    private float* dataPtr {
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        int GetIndex(int col, int row) => (row * Width + col) * NumChannels;
-
-        /// <summary>
-        /// Gets the value of a specific pixel's channel
-        /// </summary>
-        /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
-        /// <param name="row">Vertical pixel coordinate (0 is top)</param>
-        /// <param name="chan">Channel index</param>
-        /// <returns>Pixel channel value</returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public float GetPixelChannel(int col, int row, int chan) {
-            Debug.Assert(chan < NumChannels);
+        get => (float*)DataPointer.ToPointer();
+    }
 
-            int c = Math.Clamp(col, 0, Width - 1);
-            int r = Math.Clamp(row, 0, Height - 1);
+    /// <summary>
+    /// Creates a new empty image that is not yet managing any data
+    /// </summary>
+    protected ImageBase() { }
 
-            return dataPtr[GetIndex(c, r) + chan];
-        }
+    /// <summary>
+    /// Creates an image buffer initialized to zero
+    /// </summary>
+    public ImageBase(int w, int h, int numChannels) {
+        Width = w;
+        Height = h;
+        this.NumChannels = numChannels;
+        Alloc();
+
+        // Zero out the values to avoid undefined contents
+        Unsafe.InitBlock(dataPtr, 0, (uint)(Width * Height * NumChannels * sizeof(float)));
+    }
 
-        /// <summary>
-        /// Sets the value all channels in a pixel. Assumes that the number of parameters
-        /// matches the number of channels. Only asserted in debug mode.
-        ///
-        /// This function can be slow if called often, due to the allocation of the parameter
-        /// array on the heap.
-        /// </summary>
-        /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
-        /// <param name="row">Vertical pixel coordinate (0 is top)</param>
-        /// <param name="channels">
-        ///     Array with the values for each channel. Length needs to match
-        ///     the number of channels.
-        /// </param>
-        /// <returns>Pixel channel value</returns>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public void SetPixelChannels(int col, int row, params float[] channels) {
-            Debug.Assert(channels.Length == NumChannels);
+    /// <summary>
+    /// Moves the raw data from one image to another. The source image is empty afterwards and should
+    /// no longer be used. If dest is a derived class, like <see cref="RgbImage" /> the user should make
+    /// sure that the number of channels in src is correct.
+    ///
+    /// This is a potentially unsafe operation, use only if you know what you are doing!
+    /// </summary>
+    public static void Move(ImageBase src, ImageBase dest) {
+        dest.DataPointer = src.DataPointer;
+        src.DataPointer = IntPtr.Zero;
+        dest.Width = src.Width;
+        dest.Height = src.Height;
+        dest.NumChannels = src.NumChannels;
+    }
 
-            int c = Math.Clamp(col, 0, Width - 1);
-            int r = Math.Clamp(row, 0, Height - 1);
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    int GetIndex(int col, int row) => (row * Width + col) * NumChannels;
 
-            for (int chan = 0; chan < NumChannels; ++chan)
-                dataPtr[GetIndex(c, r) + chan] = channels[chan];
-        }
+    /// <summary>
+    /// Gets the value of a specific pixel's channel
+    /// </summary>
+    /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
+    /// <param name="row">Vertical pixel coordinate (0 is top)</param>
+    /// <param name="chan">Channel index</param>
+    /// <returns>Pixel channel value</returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public float GetPixelChannel(int col, int row, int chan) {
+        Debug.Assert(chan < NumChannels);
+
+        int c = Math.Clamp(col, 0, Width - 1);
+        int r = Math.Clamp(row, 0, Height - 1);
+
+        return dataPtr[GetIndex(c, r) + chan];
+    }
 
-        /// <summary>
-        /// Sets the value of an individual pixel's channel. Calling this multiple times can be faster
-        /// than using <see cref="SetPixelChannels(int, int, float[])"/>, but involves more code.
-        /// </summary>
-        /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
-        /// <param name="row">Vertical pixel coordinate (0 is top)</param>
-        /// <param name="chan">Channel index</param>
-        /// <param name="value">New value of the channel</param>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public void SetPixelChannel(int col, int row, int chan, float value) {
-            int c = Math.Clamp(col, 0, Width - 1);
-            int r = Math.Clamp(row, 0, Height - 1);
-            dataPtr[GetIndex(c, r) + chan] = value;
-        }
+    /// <summary>
+    /// Sets the value all channels in a pixel. Assumes that the number of parameters
+    /// matches the number of channels. Only asserted in debug mode.
+    ///
+    /// This function can be slow if called often, due to the allocation of the parameter
+    /// array on the heap.
+    /// </summary>
+    /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
+    /// <param name="row">Vertical pixel coordinate (0 is top)</param>
+    /// <param name="channels">
+    ///     Array with the values for each channel. Length needs to match
+    ///     the number of channels.
+    /// </param>
+    /// <returns>Pixel channel value</returns>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SetPixelChannels(int col, int row, params float[] channels) {
+        Debug.Assert(channels.Length == NumChannels);
+
+        int c = Math.Clamp(col, 0, Width - 1);
+        int r = Math.Clamp(row, 0, Height - 1);
+
+        for (int chan = 0; chan < NumChannels; ++chan)
+            dataPtr[GetIndex(c, r) + chan] = channels[chan];
+    }
 
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        static void AtomicAddFloat(ref float target, float value) {
-            float initialValue, computedValue;
-            do {
-                initialValue = target;
-                computedValue = initialValue + value;
-            } while (
-                initialValue != Interlocked.CompareExchange(ref target, computedValue, initialValue)
-                // If another thread changes target to NaN in the meantime, we will be stuck forever
-                // since NaN != NaN is always true, and NaN + value is also NaN
-                && !float.IsNaN(initialValue)
-            );
-        }
+    /// <summary>
+    /// Sets the value of an individual pixel's channel. Calling this multiple times can be faster
+    /// than using <see cref="SetPixelChannels(int, int, float[])"/>, but involves more code.
+    /// </summary>
+    /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
+    /// <param name="row">Vertical pixel coordinate (0 is top)</param>
+    /// <param name="chan">Channel index</param>
+    /// <param name="value">New value of the channel</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void SetPixelChannel(int col, int row, int chan, float value) {
+        int c = Math.Clamp(col, 0, Width - 1);
+        int r = Math.Clamp(row, 0, Height - 1);
+        dataPtr[GetIndex(c, r) + chan] = value;
+    }
 
-        /// <summary>
-        /// Atomically increases the value of a pixel's channel by the given value.
-        /// Can be used in a multi-threading context where multiple threads add values to a
-        /// pixel, like a Monte Carlo renderer.
-        /// </summary>
-        /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
-        /// <param name="row">Vertical pixel coordinate (0 is top)</param>
-        /// <param name="chan">Channel index</param>
-        /// <param name="value">Value to add to the channel</param>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public void AtomicAddChannel(int col, int row, int chan, float value) {
-            int c = Math.Clamp(col, 0, Width - 1);
-            int r = Math.Clamp(row, 0, Height - 1);
-            int idx = GetIndex(c, r);
-            AtomicAddFloat(ref *(dataPtr + idx + chan), value);
-        }
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    static void AtomicAddFloat(ref float target, float value) {
+        float initialValue, computedValue;
+        do {
+            initialValue = target;
+            computedValue = initialValue + value;
+        } while (
+            initialValue != Interlocked.CompareExchange(ref target, computedValue, initialValue)
+            // If another thread changes target to NaN in the meantime, we will be stuck forever
+            // since NaN != NaN is always true, and NaN + value is also NaN
+            && !float.IsNaN(initialValue)
+        );
+    }
 
-        /// <summary>
-        /// Same as <see cref="AtomicAddChannel(int, int, int, float)"/> but for multiple channels at
-        /// once. Only the individual channels are incremented atomically, not the whole pixel value.
-        /// Can be slower than incrementing each individual pixel, due to the allocation of the parameter
-        /// array on the heap.
-        /// </summary>
-        [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        public void AtomicAddChannels(int col, int row, params float[] channels) {
-            Debug.Assert(channels.Length == NumChannels);
-
-            int c = Math.Clamp(col, 0, Width - 1);
-            int r = Math.Clamp(row, 0, Height - 1);
-            int idx = GetIndex(c, r);
-            for (int chan = 0; chan < NumChannels; ++chan)
-                AtomicAddFloat(ref *(dataPtr + idx + chan), channels[chan]);
-        }
+    /// <summary>
+    /// Atomically increases the value of a pixel's channel by the given value.
+    /// Can be used in a multi-threading context where multiple threads add values to a
+    /// pixel, like a Monte Carlo renderer.
+    /// </summary>
+    /// <param name="col">Horizontal pixel coordinate (0 is left)</param>
+    /// <param name="row">Vertical pixel coordinate (0 is top)</param>
+    /// <param name="chan">Channel index</param>
+    /// <param name="value">Value to add to the channel</param>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AtomicAddChannel(int col, int row, int chan, float value) {
+        int c = Math.Clamp(col, 0, Width - 1);
+        int r = Math.Clamp(row, 0, Height - 1);
+        int idx = GetIndex(c, r);
+        AtomicAddFloat(ref *(dataPtr + idx + chan), value);
+    }
 
-        /// <summary>
-        /// Sets all pixels in the image equal to the given value(s)
-        /// </summary>
-        /// <param name="channels">The color channel values</param>
-        public void Fill(params float[] channels) {
-            for (int row = 0; row < Height; ++row) {
-                for (int col = 0; col < Width; ++col) {
-                    for (int chan = 0; chan < NumChannels; ++chan)
-                        dataPtr[GetIndex(col, row) + chan] = channels[chan];
-                }
+    /// <summary>
+    /// Same as <see cref="AtomicAddChannel(int, int, int, float)"/> but for multiple channels at
+    /// once. Only the individual channels are incremented atomically, not the whole pixel value.
+    /// Can be slower than incrementing each individual pixel, due to the allocation of the parameter
+    /// array on the heap.
+    /// </summary>
+    [MethodImpl(MethodImplOptions.AggressiveInlining)]
+    public void AtomicAddChannels(int col, int row, params float[] channels) {
+        Debug.Assert(channels.Length == NumChannels);
+
+        int c = Math.Clamp(col, 0, Width - 1);
+        int r = Math.Clamp(row, 0, Height - 1);
+        int idx = GetIndex(c, r);
+        for (int chan = 0; chan < NumChannels; ++chan)
+            AtomicAddFloat(ref *(dataPtr + idx + chan), channels[chan]);
+    }
+
+    /// <summary>
+    /// Sets all pixels in the image equal to the given value(s)
+    /// </summary>
+    /// <param name="channels">The color channel values</param>
+    public void Fill(params float[] channels) {
+        for (int row = 0; row < Height; ++row) {
+            for (int col = 0; col < Width; ++col) {
+                for (int chan = 0; chan < NumChannels; ++chan)
+                    dataPtr[GetIndex(col, row) + chan] = channels[chan];
             }
         }
+    }
 
-        /// <summary>
-        /// Scales all values of all channels in all pixels by multiplying them with a scalar
-        /// </summary>
-        /// <param name="s">Scalar to multiply on all values</param>
-        public void Scale(float s) {
-            for (int row = 0; row < Height; ++row) {
-                for (int col = 0; col < Width; ++col) {
-                    for (int chan = 0; chan < NumChannels; ++chan)
-                        dataPtr[GetIndex(col, row) + chan] *= s;
-                }
+    /// <summary>
+    /// Scales all values of all channels in all pixels by multiplying them with a scalar
+    /// </summary>
+    /// <param name="s">Scalar to multiply on all values</param>
+    public void Scale(float s) {
+        for (int row = 0; row < Height; ++row) {
+            for (int col = 0; col < Width; ++col) {
+                for (int chan = 0; chan < NumChannels; ++chan)
+                    dataPtr[GetIndex(col, row) + chan] *= s;
             }
         }
+    }
 
-        /// <summary>
-        /// Computes the sum of all pixel and channel values
-        /// </summary>
-        public float ComputeSum() {
-            float result = 0;
-            for (int row = 0; row < Height; ++row) {
-                for (int col = 0; col < Width; ++col) {
-                    for (int chan = 0; chan < NumChannels; ++chan)
-                        result += dataPtr[GetIndex(col, row) + chan];
-                }
+    /// <summary>
+    /// Computes the sum of all pixel and channel values
+    /// </summary>
+    public float ComputeSum() {
+        float result = 0;
+        for (int row = 0; row < Height; ++row) {
+            for (int col = 0; col < Width; ++col) {
+                for (int chan = 0; chan < NumChannels; ++chan)
+                    result += dataPtr[GetIndex(col, row) + chan];
             }
-            return result;
         }
+        return result;
+    }
 
-        internal static void EnsureDirectory(string filename) {
-            var dirname = System.IO.Path.GetDirectoryName(filename);
-            if (dirname != "")
-                System.IO.Directory.CreateDirectory(dirname);
-        }
+    internal static void EnsureDirectory(string filename) {
+        var dirname = System.IO.Path.GetDirectoryName(filename);
+        if (dirname != "")
+            System.IO.Directory.CreateDirectory(dirname);
+    }
 
-        /// <summary>
-        /// Writes the image into a file. Not all formats support all / arbitrary channel layouts.
-        /// </summary>
-        /// <param name="filename">Name of the file to write, extension must be one of the supported formats</param>
-        /// <param name="lossyQuality">
-        /// If the format is ".jpeg", this number between 0 and 100 determines the compression
-        /// quality. Otherwise, it is ignored.
-        /// </param>
-        public void WriteToFile(string filename, int lossyQuality = 80) {
-            EnsureDirectory(filename);
-            SimpleImageIOCore.WriteImage(DataPointer, NumChannels * Width, Width, Height, NumChannels,
-                filename, lossyQuality);
-        }
+    /// <summary>
+    /// Writes the image into a file. Not all formats support all / arbitrary channel layouts.
+    /// </summary>
+    /// <param name="filename">Name of the file to write, extension must be one of the supported formats</param>
+    /// <param name="lossyQuality">
+    /// If the format is ".jpeg", this number between 0 and 100 determines the compression
+    /// quality. Otherwise, it is ignored.
+    /// </param>
+    public void WriteToFile(string filename, int lossyQuality = 80) {
+        EnsureDirectory(filename);
+        SimpleImageIOCore.WriteImage(DataPointer, NumChannels * Width, Width, Height, NumChannels,
+            filename, lossyQuality);
+    }
 
-        /// <summary>
-        /// Encodes the file and writes its data to a memory buffer
-        /// </summary>
-        /// <param name="extension">
-        /// The file name extension of the desired format, e.g., ".exr" or ".png".
-        /// </param>
-        /// <param name="lossyQuality">
-        /// If the format is ".jpeg", this number between 0 and 100 determines the compression
-        /// quality. Otherwise, it is ignored.
-        /// </param>
-        /// <returns>The memory contents of the image file</returns>
-        public byte[] WriteToMemory(string extension, int lossyQuality = 80) {
-            IntPtr mem = SimpleImageIOCore.WriteToMemory(DataPointer, NumChannels * Width, Width, Height,
-                NumChannels, extension, lossyQuality, out int numBytes);
-
-            byte[] bytes = new byte[numBytes];
-            Marshal.Copy(mem, bytes, 0, numBytes);
-            SimpleImageIOCore.FreeMemory(mem);
-
-            return bytes;
-        }
+    /// <summary>
+    /// Encodes the file and writes its data to a memory buffer
+    /// </summary>
+    /// <param name="extension">
+    /// The file name extension of the desired format, e.g., ".exr" or ".png".
+    /// </param>
+    /// <param name="lossyQuality">
+    /// If the format is ".jpeg", this number between 0 and 100 determines the compression
+    /// quality. Otherwise, it is ignored.
+    /// </param>
+    /// <returns>The memory contents of the image file</returns>
+    public byte[] WriteToMemory(string extension, int lossyQuality = 80) {
+        IntPtr mem = SimpleImageIOCore.WriteToMemory(DataPointer, NumChannels * Width, Width, Height,
+            NumChannels, extension, lossyQuality, out int numBytes);
+
+        byte[] bytes = new byte[numBytes];
+        Marshal.Copy(mem, bytes, 0, numBytes);
+        SimpleImageIOCore.FreeMemory(mem);
+
+        return bytes;
+    }
 
-        /// <summary>
-        /// Converts the image data to a string containing the base64 encoded .png file.
-        /// Only supports 1, 3, or 4 channel images (monochrome, rgb, rgba)
-        /// </summary>
-        /// <returns>The base64 encoded .png as a string</returns>
-        public string AsBase64Png(string extension = ".png") {
-            var bytes = WriteToMemory(extension);
-            return Convert.ToBase64String(bytes);
-        }
+    /// <summary>
+    /// Converts the image data to a string containing the base64 encoded .png file.
+    /// Only supports 1, 3, or 4 channel images (monochrome, rgb, rgba)
+    /// </summary>
+    /// <returns>The base64 encoded .png as a string</returns>
+    public string AsBase64Png(string extension = ".png") {
+        var bytes = WriteToMemory(extension);
+        return Convert.ToBase64String(bytes);
+    }
 
-        /// <summary>
-        /// Loads an image from one of the supported formats into this object
-        /// </summary>
-        /// <param name="filename">Filename with supported extension</param>
-        protected void LoadFromFile(string filename) {
-            if (!File.Exists(filename))
-                throw new FileNotFoundException("Image file does not exist.", filename);
-
-            // Read the image from the file, it is cached in native memory
-            int id = SimpleImageIOCore.CacheImage(out int w, out int h, out int n, filename);
-            Width = w;
-            Height = h;
-            NumChannels = n;
-            if (id < 0 || Width <= 0 || Height <= 0)
-                throw new IOException($"ERROR: Could not load image file '{filename}'");
-
-            // Copy to managed memory array
-            Alloc();
-            SimpleImageIOCore.CopyCachedImage(id, DataPointer);
-        }
+    /// <summary>
+    /// Loads an image from one of the supported formats into this object
+    /// </summary>
+    /// <param name="filename">Filename with supported extension</param>
+    protected void LoadFromFile(string filename) {
+        if (!File.Exists(filename))
+            throw new FileNotFoundException("Image file does not exist.", filename);
+
+        // Read the image from the file, it is cached in native memory
+        int id = SimpleImageIOCore.CacheImage(out int w, out int h, out int n, filename);
+        Width = w;
+        Height = h;
+        NumChannels = n;
+        if (id < 0 || Width <= 0 || Height <= 0)
+            throw new IOException($"ERROR: Could not load image file '{filename}'");
+
+        // Copy to managed memory array
+        Alloc();
+        SimpleImageIOCore.CopyCachedImage(id, DataPointer);
+    }
 
-        /// <summary>
-        /// Flips the image horizontally, i.e., the first column becomes the last.
-        /// The image is modified in-place, no new image is allocated.
-        /// </summary>
-        public void FlipHorizontal() {
-            for (int row = 0; row < Height; ++row) {
-                for (int col = 0; col < Width / 2; ++col) {
-                    for (int chan = 0; chan < NumChannels; ++chan) {
-                        int idxLeft = GetIndex(col, row) + chan;
-                        int idxRight = GetIndex(Width - 1 - col, row) + chan;
-                        (dataPtr[idxLeft], dataPtr[idxRight]) = (dataPtr[idxRight], dataPtr[idxLeft]);
-                    }
+    /// <summary>
+    /// Flips the image horizontally, i.e., the first column becomes the last.
+    /// The image is modified in-place, no new image is allocated.
+    /// </summary>
+    public void FlipHorizontal() {
+        for (int row = 0; row < Height; ++row) {
+            for (int col = 0; col < Width / 2; ++col) {
+                for (int chan = 0; chan < NumChannels; ++chan) {
+                    int idxLeft = GetIndex(col, row) + chan;
+                    int idxRight = GetIndex(Width - 1 - col, row) + chan;
+                    (dataPtr[idxLeft], dataPtr[idxRight]) = (dataPtr[idxRight], dataPtr[idxLeft]);
                 }
             }
         }
+    }
 
-        /// <returns>A deep copy of the image object</returns>
-        public virtual ImageBase Copy() {
-            if (DataPointer == IntPtr.Zero) return null;
-            ImageBase other = new(Width, Height, NumChannels);
-            Unsafe.CopyBlock(other.dataPtr, dataPtr, (uint)(Width * Height * NumChannels * sizeof(float)));
-            return other;
-        }
+    /// <returns>A deep copy of the image object</returns>
+    public virtual ImageBase Copy() {
+        if (DataPointer == IntPtr.Zero) return null;
+        ImageBase other = new(Width, Height, NumChannels);
+        Unsafe.CopyBlock(other.dataPtr, dataPtr, (uint)(Width * Height * NumChannels * sizeof(float)));
+        return other;
+    }
 
-        /// <summary>
-        /// Allocates native memory for the image representation
-        /// </summary>
-        protected void Alloc()
-        => DataPointer = Marshal.AllocHGlobal(sizeof(float) * NumChannels * Width * Height);
-
-        /// <summary>
-        /// Frees the native memory
-        /// </summary>
-        protected void Free() {
-            if (DataPointer == IntPtr.Zero) return;
-            Marshal.FreeHGlobal(DataPointer);
-            DataPointer = IntPtr.Zero;
-        }
+    /// <summary>
+    /// Allocates native memory for the image representation
+    /// </summary>
+    protected void Alloc()
+    => DataPointer = Marshal.AllocHGlobal(sizeof(float) * NumChannels * Width * Height);
 
-        /// <summary>
-        /// Frees the native memory
-        /// </summary>
-        ~ImageBase() => Free();
-
-        /// <summary>
-        /// Frees the native memory
-        /// </summary>
-        public void Dispose() {
-            Free();
-            GC.SuppressFinalize(this);
-        }
+    /// <summary>
+    /// Frees the native memory
+    /// </summary>
+    protected void Free() {
+        if (DataPointer == IntPtr.Zero) return;
+        Marshal.FreeHGlobal(DataPointer);
+        DataPointer = IntPtr.Zero;
+    }
 
-        /// <summary>
-        /// Zooms into the image with nearest neighbor interpolation. Useful to ensure that individual pixels
-        /// remain visible for small images, independent of the viewer.
-        /// The current contents of this object are overwritten with the result of the operation
-        /// </summary>
-        /// <param name="other">The image to zoom, will not be modified</param>
-        /// <param name="scale">The scaling factor</param>
-        protected void Zoom(ImageBase other, int scale) {
-            Debug.Assert(scale > 0);
-
-            if (DataPointer != IntPtr.Zero) Free();
-
-            Width = other.Width * scale;
-            Height = other.Height * scale;
-            NumChannels = other.NumChannels;
-            Alloc();
-
-            SimpleImageIOCore.ZoomWithNearestInterp(other.DataPointer, NumChannels * other.Width, DataPointer,
-                NumChannels * Width, other.Width, other.Height, NumChannels, scale);
-        }
+    /// <summary>
+    /// Frees the native memory
+    /// </summary>
+    ~ImageBase() => Free();
+
+    /// <summary>
+    /// Frees the native memory
+    /// </summary>
+    public void Dispose() {
+        Free();
+        GC.SuppressFinalize(this);
+    }
+
+    /// <summary>
+    /// Zooms into the image with nearest neighbor interpolation. Useful to ensure that individual pixels
+    /// remain visible for small images, independent of the viewer.
+    /// The current contents of this object are overwritten with the result of the operation
+    /// </summary>
+    /// <param name="other">The image to zoom, will not be modified</param>
+    /// <param name="scale">The scaling factor</param>
+    protected void Zoom(ImageBase other, int scale) {
+        Debug.Assert(scale > 0);
+
+        if (DataPointer != IntPtr.Zero) Free();
+
+        Width = other.Width * scale;
+        Height = other.Height * scale;
+        NumChannels = other.NumChannels;
+        Alloc();
+
+        SimpleImageIOCore.ZoomWithNearestInterp(other.DataPointer, NumChannels * other.Width, DataPointer,
+            NumChannels * Width, other.Width, other.Height, NumChannels, scale);
     }
 }
\ No newline at end of file
diff --git a/SimpleImageIO/SimpleImageIOCore.cs b/SimpleImageIO/SimpleImageIOCore.cs
index fce05de..559a1bb 100644
--- a/SimpleImageIO/SimpleImageIOCore.cs
+++ b/SimpleImageIO/SimpleImageIOCore.cs
@@ -1,118 +1,119 @@
-using System;
 using System.Text;
 using System.Runtime.InteropServices;
 
-namespace SimpleImageIO {
-    static internal partial class SimpleImageIOCore {
-        #region ReadingImages
+namespace SimpleImageIO;
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern int CacheImage(out int width, out int height, out int numChannels, string filename);
+static internal partial class SimpleImageIOCore {
+    #region ReadingImages
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern int GetExrLayerCount(int id);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern int CacheImage(out int width, out int height, out int numChannels,
+                                        [MarshalAs(UnmanagedType.LPUTF8Str)] string filename);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern int GetExrLayerChannelCount(int id, string name);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern int GetExrLayerCount(int id);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern int GetExrLayerNameLen(int id, int layerIdx);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern int GetExrLayerChannelCount(int id, string name);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void GetExrLayerName(int id, int layerIdx, StringBuilder name);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern int GetExrLayerNameLen(int id, int layerIdx);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void CopyCachedLayer(int id, string name, IntPtr data);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void GetExrLayerName(int id, int layerIdx, StringBuilder name);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void DeleteCachedImage(int id);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void CopyCachedLayer(int id, string name, IntPtr data);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void CopyCachedImage(int id, IntPtr buffer);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void DeleteCachedImage(int id);
 
-        #endregion
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void CopyCachedImage(int id, IntPtr buffer);
 
-        #region WritingImages
+    #endregion
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void WriteImage(IntPtr data, int rowStride, int width, int height, int numChannels,
-                                             string filename, int lossyQuality);
+    #region WritingImages
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void WriteLayeredExr(IntPtr[] datas, int[] strides, int width, int height,
-                                                  int[] numChannels, int numLayers, string[] names, string filename);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void WriteImage(IntPtr data, int rowStride, int width, int height, int numChannels,
+                                         [MarshalAs(UnmanagedType.LPUTF8Str)] string filename, int lossyQuality);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern IntPtr WriteToMemory(IntPtr data, int rowStride, int width, int height,
-                                                  int numChannels, string extension, int lossyQuality,
-                                                  out int len);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void WriteLayeredExr(IntPtr[] datas, int[] strides, int width, int height,
+                                              int[] numChannels, int numLayers, string[] names,
+                                              [MarshalAs(UnmanagedType.LPUTF8Str)] string filename);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void FreeMemory(IntPtr mem);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern IntPtr WriteToMemory(IntPtr data, int rowStride, int width, int height,
+                                              int numChannels, string extension, int lossyQuality,
+                                              out int len);
 
-        #endregion
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void FreeMemory(IntPtr mem);
 
-        #region ErrorMetrics
+    #endregion
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern float ComputeMSE(IntPtr image, int imgRowStride, IntPtr reference, int refRowStride,
-                                              int width, int height, int numChannels);
+    #region ErrorMetrics
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern float ComputeRelMSE(IntPtr image, int imgRowStride, IntPtr reference, int refRowStride,
-                                                 int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern float ComputeMSE(IntPtr image, int imgRowStride, IntPtr reference, int refRowStride,
+                                          int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern float ComputeRelMSEOutlierReject(IntPtr image, int imgRowStride, IntPtr reference,
-                                                              int refRowStride, int width, int height,
-                                                              int numChannels, float percentage);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern float ComputeRelMSE(IntPtr image, int imgRowStride, IntPtr reference, int refRowStride,
+                                             int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern float ComputeMSEOutlierReject(IntPtr image, int imgRowStride, IntPtr reference,
-                                                           int refRowStride, int width, int height,
-                                                           int numChannels, float percentage);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern float ComputeRelMSEOutlierReject(IntPtr image, int imgRowStride, IntPtr reference,
+                                                          int refRowStride, int width, int height,
+                                                          int numChannels, float percentage);
 
-        #endregion
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern float ComputeMSEOutlierReject(IntPtr image, int imgRowStride, IntPtr reference,
+                                                       int refRowStride, int width, int height,
+                                                       int numChannels, float percentage);
 
-        #region ImageManipulation
+    #endregion
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void RgbToMonoAverage(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                   int width, int height, int numChannels);
+    #region ImageManipulation
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void RgbToMonoLuminance(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                     int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void RgbToMonoAverage(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                               int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void ZoomWithNearestInterp(IntPtr image, int imgRowStride, IntPtr result,
-                                                        int resRowStride, int origWidth, int origHeight,
-                                                        int numChannels, int scale);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void RgbToMonoLuminance(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                                 int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void BoxFilter(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                            int width, int height, int numChannels, int radius);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void ZoomWithNearestInterp(IntPtr image, int imgRowStride, IntPtr result,
+                                                    int resRowStride, int origWidth, int origHeight,
+                                                    int numChannels, int scale);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void BoxFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                               int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void BoxFilter(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                        int width, int height, int numChannels, int radius);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void DilationFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                    int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void BoxFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                           int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void ErosionFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                   int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void DilationFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                                int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void MedianFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                  int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void ErosionFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                               int width, int height, int numChannels);
 
-        [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
-        public static extern void GaussFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
-                                                 int width, int height, int numChannels);
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void MedianFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                              int width, int height, int numChannels);
+
+    [DllImport("SimpleImageIOCore", CallingConvention = CallingConvention.Cdecl)]
+    public static extern void GaussFilter3x3(IntPtr image, int imgRowStride, IntPtr result, int resRowStride,
+                                             int width, int height, int numChannels);
 
-        #endregion
-    }
+    #endregion
 }
\ No newline at end of file


Test patch:
diff --git a/PyTest/test_io.py b/PyTest/test_io.py
index df97021..a383faa 100644
--- a/PyTest/test_io.py
+++ b/PyTest/test_io.py
@@ -13,6 +13,14 @@ def test_single_pixel(self):
         self.assertEqual(px[0,0,2], 0.0)
         os.remove("redpixel.exr")
 
+    def test_unicode_exr(self):
+        sio.write("😎😎💩😎.exr", [[[1.0,0.0,0.0]]])
+        px = sio.read("😎😎💩😎.exr")
+        self.assertEqual(px[0,0,0], 1.0)
+        self.assertEqual(px[0,0,1], 0.0)
+        self.assertEqual(px[0,0,2], 0.0)
+        os.remove("😎😎💩😎.exr")
+
     def test_small_image(self):
         sio.write("image.exr", [
             [[1.0,0.0,0.0], [0.0,1.0,0.0], [0.0,0.0,1.0]],