Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public IImage FromStream(Stream stream, ImageFormat format = ImageFormat.Png)
{
ArgumentNullException.ThrowIfNull(stream);

var image = CairoPlatformImage.FromStream(stream);
var image = CairoPlatformImage.FromStream(stream, format);
if (image == null)
throw new ArgumentException("Could not decode image from stream.");

Expand Down
62 changes: 14 additions & 48 deletions src/Platform.Maui.Linux.Gtk4/Graphics/CairoPlatformImage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public CairoPlatformImage(Cairo.ImageSurface surface)

internal Cairo.ImageSurface Surface { get; }

public float Width => cairo_image_surface_get_width(Surface.Handle.DangerousGetHandle());
public float Height => cairo_image_surface_get_height(Surface.Handle.DangerousGetHandle());
public float Width => Surface.Width;
public float Height => Surface.Height;

public IImage Downsize(float maxWidthOrHeight, bool disposeOriginal = false)
{
Expand Down Expand Up @@ -102,17 +102,8 @@ public IImage Resize(float width, float height, ResizeMode resizeMode = ResizeMo

public void Save(Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1)
{
var tmpPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"maui_img_{Guid.NewGuid():N}.png");
try
{
cairo_surface_write_to_png(Surface.Handle.DangerousGetHandle(), tmpPath);
using var fs = File.OpenRead(tmpPath);
fs.CopyTo(stream);
}
finally
{
try { File.Delete(tmpPath); } catch { }
}
using var pixbuf = Surface.CreatePixbuf();
pixbuf.SaveToStream(stream, format);
}

public Task SaveAsync(Stream stream, ImageFormat format = ImageFormat.Png, float quality = 1)
Expand All @@ -122,37 +113,29 @@ public Task SaveAsync(Stream stream, ImageFormat format = ImageFormat.Png, float
}

/// <summary>
/// Creates a CairoPlatformImage from a PNG stream.
/// Creates a CairoPlatformImage from a stream.
/// </summary>
public static CairoPlatformImage? FromStream(Stream stream)
public static CairoPlatformImage? FromStream(Stream stream, ImageFormat imageFormat)
{
var tmpPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), $"maui_img_{Guid.NewGuid():N}.png");
try
{
using (var fs = File.Create(tmpPath))
stream.CopyTo(fs);

var surfaceHandle = cairo_image_surface_create_from_png(tmpPath);
if (surfaceHandle == nint.Zero)
using var pixbuf = PixbufExtensions.LoadFromStream(stream);
if (pixbuf is null)
return null;

// Wrap the unmanaged surface handle. We use an ImageSurface that manages its own lifetime.
var surface = new Cairo.ImageSurface(Cairo.Format.Argb32,
cairo_image_surface_get_width(surfaceHandle),
cairo_image_surface_get_height(surfaceHandle));
var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, pixbuf.Width, pixbuf.Height);

// Copy the PNG data onto our managed surface
var cr = new Cairo.Context(surface);
cairo_set_source_surface(cr.Handle.DangerousGetHandle(), surfaceHandle, 0, 0);
Cairo.Internal.Context.Paint(cr.Handle);
cr.PaintPixbuf(pixbuf);

cr.Dispose();
cairo_surface_destroy(surfaceHandle);

return new CairoPlatformImage(surface);
}
finally
{
try { File.Delete(tmpPath); } catch { }
try { }
catch { }
}
}

Expand All @@ -161,21 +144,4 @@ public void Dispose()
Surface?.Dispose();
}

[DllImport("libcairo.so.2")]
private static extern int cairo_surface_write_to_png(nint surface, [MarshalAs(UnmanagedType.LPUTF8Str)] string filename);

[DllImport("libcairo.so.2")]
private static extern nint cairo_image_surface_create_from_png([MarshalAs(UnmanagedType.LPUTF8Str)] string filename);

[DllImport("libcairo.so.2")]
private static extern int cairo_image_surface_get_width(nint surface);

[DllImport("libcairo.so.2")]
private static extern int cairo_image_surface_get_height(nint surface);

[DllImport("libcairo.so.2")]
private static extern void cairo_set_source_surface(nint cr, nint surface, double x, double y);

[DllImport("libcairo.so.2")]
private static extern void cairo_surface_destroy(nint surface);
}
}
152 changes: 152 additions & 0 deletions src/Platform.Maui.Linux.Gtk4/Graphics/PixbufExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Cairo;
using GdkPixbuf;
using Gio;
using GLib;

namespace Platform.Maui.Linux.Gtk4.Graphics;

internal static class PixbufExtensions
{
public static void PaintPixbuf(this Context context, Pixbuf pixbuf, double x = 0, double y = 0, double? width = default, double? height = default)
{
var translate = x != 0 || y != 0;
var scale = width.HasValue && height.HasValue && (width != pixbuf.Width || height != pixbuf.Height);
if (translate || scale)
context.Save();
if (translate)
context.Translate(x, y);
if (scale)
context.Scale(width.Value / pixbuf.Width, height.Value / pixbuf.Height);
Gdk.Functions.CairoSetSourcePixbuf(context, pixbuf, 0, 0);

using var p = context.GetSource();
if (p is SurfacePattern pattern)
{
// Fixes blur issue when rendering on an image surface
pattern.Filter =
width > pixbuf.Width || height > pixbuf.Height ? Filter.Fast : Filter.Good;
}

context.Paint();
if (translate || scale)
context.Restore();
}

public static Pixbuf? CreatePixbuf(this ImageSurface? surface)
{
if (surface == null)
return null;

var surfaceData = surface.GetData();
var nbytes = surface.Format == Format.Argb32 ? 4 : 3;
var pixData = new byte[surfaceData.Length / 4 * nbytes];

var i = 0;
var n = 0;
var stride = surface.Stride;
var ncols = surface.Width;

if (BitConverter.IsLittleEndian)
{
var row = surface.Height;

while (row-- > 0)
{
var prevPos = n;
var col = ncols;

while (col-- > 0)
{
var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1;
pixData[i] = (byte)(surfaceData[n + 2] * alphaFactor + 0.5);
pixData[i + 1] = (byte)(surfaceData[n + 1] * alphaFactor + 0.5);
pixData[i + 2] = (byte)(surfaceData[n + 0] * alphaFactor + 0.5);

if (nbytes == 4)
pixData[i + 3] = surfaceData[n + 3];

n += 4;
i += nbytes;
}

n = prevPos + stride;
}
}
else
{
var row = surface.Height;

while (row-- > 0)
{
var prevPos = n;
var col = ncols;

while (col-- > 0)
{
var alphaFactor = nbytes == 4 ? 255d / surfaceData[n + 3] : 1;
pixData[i] = (byte)(surfaceData[n + 1] * alphaFactor + 0.5);
pixData[i + 1] = (byte)(surfaceData[n + 2] * alphaFactor + 0.5);
pixData[i + 2] = (byte)(surfaceData[n + 3] * alphaFactor + 0.5);

if (nbytes == 4)
pixData[i + 3] = surfaceData[n + 0];

n += 4;
i += nbytes;
}

n = prevPos + stride;
}
}

return Pixbuf.NewFromBytes(Bytes.New(pixData),
Colorspace.Rgb,
nbytes == 4,
8, surface.Width, surface.Height, surface.Width * nbytes);
}

public static string? ToImageExtension(this ImageFormat imageFormat) =>
imageFormat switch
{
ImageFormat.Bmp => "bmp",
ImageFormat.Png => "png",
ImageFormat.Jpeg => "jpeg",
ImageFormat.Gif => "gif",
ImageFormat.Tiff => "tiff",
_ => default
};

public static void SaveToStream(this Pixbuf? pixbuf, Stream stream, ImageFormat imageFormat = ImageFormat.Png)
{
if (pixbuf == null)
return;

using var outputStream = MemoryOutputStream.NewResizable();
var success = pixbuf.SaveToStreamv(outputStream, imageFormat.ToImageExtension(), default, default, default);

if (!success)
throw new Exception("Failed to save pixbuf to stream");

var bytes = outputStream.StealAsBytes();
stream.Write(bytes.GetRegionSpan<byte>(0, bytes.GetSize()));
}

public static Pixbuf? LoadFromStream(Stream stream)
{
var loader = PixbufLoader.New();
loader.LoadFromStream(stream);
return loader.GetPixbuf();
}

private static void LoadFromStream(this PixbufLoader loader, Stream input)
{
const int bufferSize = 8192;
var buffer = new byte[bufferSize];
int bytesRead;

while ((bytesRead = input.Read(buffer, 0, bufferSize)) != 0)
{
loader.Write(buffer.AsSpan(0, bytesRead));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<ItemGroup>
<PackageReference Include="GirCore.Gtk-4.0" Version="0.7.0" />
<PackageReference Include="GirCore.GdkPixbuf-2.0" Version="0.7.0" />
<PackageReference Include="GirCore.WebKit-6.0" Version="0.7.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
</ItemGroup>
Expand Down