Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
489e106
Added farbfeld exporter
Lehonti Mar 12, 2026
6821313
Added unit test
Lehonti Mar 12, 2026
dd531cb
Support for importing farbfeld
Lehonti Mar 13, 2026
d63d977
Added check
Lehonti Mar 13, 2026
1884ef0
Got rid of `BinaryReader` and several checks
Lehonti Mar 13, 2026
a50c41b
Organized reading operations
Lehonti Mar 13, 2026
3e8dfe4
Actually...delegates add a lot of overhead
Lehonti Mar 13, 2026
146281a
Removed clunky endianness checks, used specialized methods
Lehonti Mar 13, 2026
cccb0ed
Organized binary writing operations in nice `struct`
Lehonti Mar 13, 2026
ac8d286
Translation of pixels (the ugly parts) happen inside writer and reader
Lehonti Mar 13, 2026
8dc957e
Removed accidentally added namespace reference
Lehonti Mar 13, 2026
3fa653d
Correct alpha scaling
Lehonti Mar 14, 2026
2c32cdc
Fixed some subtle bugs: overflows, write performance
Lehonti Mar 14, 2026
07a8d5e
colors should survive round trips
Lehonti Mar 14, 2026
14b2a5a
buffering
Lehonti Mar 14, 2026
a1cb32b
downscaling
Lehonti Mar 14, 2026
f58b36b
Do not depend on order of static initialization
Lehonti Mar 14, 2026
bcba3e1
Shorten name
Lehonti Mar 14, 2026
66678c1
Minimized write operations
Lehonti Mar 14, 2026
ed75f6d
Minimized read operations
Lehonti Mar 14, 2026
fea3e1b
Minimized write operation
Lehonti Mar 14, 2026
9241087
Added MIT license
Lehonti Mar 21, 2026
2a86d99
Merge branch 'PintaProject:master' into feature/farbfeld
Lehonti Mar 21, 2026
e198325
Merge branch 'PintaProject:master' into feature/farbfeld
Lehonti Apr 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions Pinta.Core/ImageFormats/FarbfeldFormat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// Copyright (c) 2026 Lehonti Ramos
//
// 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.Buffers.Binary;
using System.IO;
using System.Text;
using Cairo;

namespace Pinta.Core;

public sealed class FarbfeldFormat : IImageExporter, IImageImporter
{
public void Export (Document document, Gio.File file, Gtk.Window parent)
{
using ImageSurface flattenedImage = document.GetFlattenedImage ();
using GioStream outputStream = new (file.Replace ());
using BufferedStream bufferedStream = new (outputStream);
Export (flattenedImage, bufferedStream);
}

private const string FARBFELD_SIGNATURE = "farbfeld";
private static readonly byte[] farbfeld_signature_bytes = Encoding.ASCII.GetBytes (FARBFELD_SIGNATURE);

public static void Export (ImageSurface flattenedImage, Stream outputStream)
{
FarbfeldWriter writer = new (outputStream);
writer.WriteSignature ();
writer.WriteSize (flattenedImage.GetSize ());
ReadOnlySpan<ColorBgra> pixels = flattenedImage.GetReadOnlyPixelData ();
foreach (var pixel in pixels)
writer.WritePixel (in pixel);
}

public Document Import (Gio.File file)
{
using GioStream inputStream = new (file.Read (cancellable: null));
using BufferedStream bufferedStream = new (inputStream);
FarbfeldReader reader = new (bufferedStream);
reader.ReadSignature ();
Size imageSize = reader.ReadSize ();
Document newDocument = new (
PintaCore.Actions,
PintaCore.Tools,
PintaCore.Workspace,
imageSize,
file,
"ff");
Layer layer = newDocument.Layers.AddNewLayer (file.GetDisplayName ());
layer.Surface.Flush ();
Span<ColorBgra> pixels = layer.Surface.GetPixelData ();
int pixelCount = checked(imageSize.Width * imageSize.Height);
for (int i = 0; i < pixelCount; i++) pixels[i] = reader.ReadPixel ();
layer.Surface.MarkDirty ();
return newDocument;
}

private static FarbfeldPixel ToFarbfeldPixel (in ColorBgra pixel)
{
if (pixel.A == 0) return new (0, 0, 0, 0);

// Un-premultiply
ushort r = (ushort) Math.Min ((pixel.R * 65535 + pixel.A / 2) / pixel.A, 65535);
ushort g = (ushort) Math.Min ((pixel.G * 65535 + pixel.A / 2) / pixel.A, 65535);
ushort b = (ushort) Math.Min ((pixel.B * 65535 + pixel.A / 2) / pixel.A, 65535);
ushort a = (ushort) (pixel.A * 257u); // 255 * 257 = 65535, so this maps 0-255 to 0-65535

return new (
r: r,
g: g,
b: b,
a: a);
}

private static ColorBgra ToColorBgra (in FarbfeldPixel pixel)
{
byte a8 = (byte) ((pixel.A + 128) / 257);
if (a8 == 0) return ColorBgra.Transparent;
byte b8 = (byte) ((pixel.B + 128) / 257);
byte g8 = (byte) ((pixel.G + 128) / 257);
byte r8 = (byte) ((pixel.R + 128) / 257);
return ColorBgra.FromBgra (
b: (byte) ((b8 * a8 + 127) / 255),
g: (byte) ((g8 * a8 + 127) / 255),
r: (byte) ((r8 * a8 + 127) / 255),
a: a8);
}

private readonly ref struct FarbfeldWriter (Stream outputStream)
{
public void WriteSignature ()
{
outputStream.Write (farbfeld_signature_bytes);
}

public void WriteSize (Size size)
{
uint width = Convert.ToUInt32 (size.Width);
uint height = Convert.ToUInt32 (size.Height);
Span<byte> sizeBytes = stackalloc byte[8];
Span<byte> widthBytes = sizeBytes.Slice (0, 4);
Span<byte> heightBytes = sizeBytes.Slice (4, 4);
BinaryPrimitives.WriteUInt32BigEndian (widthBytes, width);
BinaryPrimitives.WriteUInt32BigEndian (heightBytes, height);
outputStream.Write (sizeBytes);
}

public void WritePixel (in ColorBgra pixel)
{
FarbfeldPixel farbfeldPixel = ToFarbfeldPixel (in pixel);
WriteFarbfeldPixel (in farbfeldPixel);
}

private void WriteFarbfeldPixel (in FarbfeldPixel pixel)
{
Span<byte> colorBytes = stackalloc byte[8];
Span<byte> rBytes = colorBytes.Slice (0, 2);
Span<byte> gBytes = colorBytes.Slice (2, 2);
Span<byte> bBytes = colorBytes.Slice (4, 2);
Span<byte> aBytes = colorBytes.Slice (6, 2);
BinaryPrimitives.WriteUInt16BigEndian (rBytes, pixel.R);
BinaryPrimitives.WriteUInt16BigEndian (gBytes, pixel.G);
BinaryPrimitives.WriteUInt16BigEndian (bBytes, pixel.B);
BinaryPrimitives.WriteUInt16BigEndian (aBytes, pixel.A);
outputStream.Write (colorBytes);
}
}

private readonly ref struct FarbfeldReader (Stream stream)
{
public void ReadSignature ()
{
Span<byte> signatureBytes = stackalloc byte[8];
stream.ReadExactly (signatureBytes);
string signatureString = Encoding.ASCII.GetString (signatureBytes);
if (signatureString != FARBFELD_SIGNATURE)
throw new FormatException ($"Signature is not correct. It should be '{FARBFELD_SIGNATURE}'");
}

public Size ReadSize ()
{
Span<byte> sizeBytes = stackalloc byte[8];
stream.ReadExactly (sizeBytes);
Span<byte> widthBytes = sizeBytes.Slice (0, 4);
Span<byte> heightBytes = sizeBytes.Slice (4, 4);
int width = checked((int) BinaryPrimitives.ReadUInt32BigEndian (widthBytes));
int height = checked((int) BinaryPrimitives.ReadUInt32BigEndian (heightBytes));
return new (width, height);
}

public ColorBgra ReadPixel ()
{
FarbfeldPixel farbfeldPixel = ReadFarbfeldPixel ();
return ToColorBgra (in farbfeldPixel);
}

private FarbfeldPixel ReadFarbfeldPixel ()
{
Span<byte> colorBytes = stackalloc byte[8];
stream.ReadExactly (colorBytes);
Span<byte> rBytes = colorBytes.Slice (0, 2);
Span<byte> gBytes = colorBytes.Slice (2, 2);
Span<byte> bBytes = colorBytes.Slice (4, 2);
Span<byte> aBytes = colorBytes.Slice (6, 2);
return new (
r: BinaryPrimitives.ReadUInt16BigEndian (rBytes),
g: BinaryPrimitives.ReadUInt16BigEndian (gBytes),
b: BinaryPrimitives.ReadUInt16BigEndian (bBytes),
a: BinaryPrimitives.ReadUInt16BigEndian (aBytes));
}
}

private readonly ref struct FarbfeldPixel (
ushort r,
ushort g,
ushort b,
ushort a)
{
public readonly ushort R { get; } = r;
public readonly ushort G { get; } = g;
public readonly ushort B { get; } = b;
public readonly ushort A { get; } = a;
}
}
10 changes: 10 additions & 0 deletions Pinta.Core/Managers/ImageConverterManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ private static IEnumerable<FormatDescriptor> GetInitialFormats ()
supportsLayers: false
);
yield return netpbmPortablePixmapDescriptor;

FarbfeldFormat farbfeldFormat = new ();
FormatDescriptor farbfeldFormatDescriptor = new (
displayPrefix: "Farbfeld",
extensions: ["ff", "FF"],
mimes: ["image/farbfeld", "image/x-farbfeld"],
importer: farbfeldFormat,
exporter: farbfeldFormat,
supportsLayers: false);
yield return farbfeldFormatDescriptor;
}

private static FormatDescriptor CreateFormatDescriptor (PixbufFormat format)
Expand Down
Binary file added tests/Pinta.Core.Tests/Assets/sixcolors_farbfeld.ff
Binary file not shown.
20 changes: 20 additions & 0 deletions tests/Pinta.Core.Tests/FileFormatTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,24 @@ public void Export_NetpbmPixmap_TextBased (string inputFile, IEnumerable<string>
new[] { "sixcolors_standard_lf.ppm" }
),
];

[TestCase ("sixcolorsinput.gif", "sixcolors_farbfeld.ff")]
public void Export_Farbfeld (string input, string output)
{
string inputFilePath = Utilities.GetAssetPath (input);
ImageSurface loaded = Utilities.LoadImage (inputFilePath);
FarbfeldFormat exporter = new ();
Gio.MemoryOutputStream memoryOutput = Gio.MemoryOutputStream.NewResizable ();
using GioStream outputStream = new (memoryOutput);
FarbfeldFormat.Export (loaded, outputStream);
outputStream.Close ();
memoryOutput.Close (null);
var exportedBytes = memoryOutput.StealAsBytes ();
var bytesStream = Gio.MemoryInputStream.NewFromBytes (exportedBytes);
var bytesReader = Gio.DataInputStream.New (bytesStream);
string filePath = Utilities.GetAssetPath (output);
using var context = Utilities.OpenFile (filePath);
bool filesAreEqual = Utilities.AreFilesEqual (bytesReader, context.DataStream);
Assert.That (filesAreEqual);
}
}
Loading