Skip to content

Commit

Permalink
Added capability to export to portable pixmap (PintaProject#549)
Browse files Browse the repository at this point in the history
* Added capability to export to portable pixmap

* Added file equality utility method and one test

* Added more tests and more files (two versions of the output ppm)

* Copying assets to output dir

* Added extra line break to sample output files

* Exporter refactored in order to be able to test. Also, finished tests attempt

* Refactored file path retrieval out of `DataInputStreamContext` constructor

* Initializing modules. Maybe that's the reason why it can't find GLib?

* Corrected file names

* Corrected remaining file name

* Deleted final line in test outputs

* Corrected bug in test

* Added commented-out console messages

* Added extra line

* Commented out test case that acts weird

---------

Co-authored-by: Lehonti Ramos <lehonti@ramos>
  • Loading branch information
Lehonti and Lehonti Ramos authored Oct 11, 2023
1 parent 30826aa commit 441becd
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 2 deletions.
42 changes: 42 additions & 0 deletions Pinta.Core/ImageFormats/NetpbmPortablePixmap.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,42 @@
using System;
using System.IO;
using System.Text;
using Cairo;
using Gtk;

namespace Pinta.Core;

public sealed class NetpbmPortablePixmap : IImageExporter
{
public void Export (ImageSurface flattenedImage, Stream outputStream)
{
using StreamWriter writer = new (outputStream, Encoding.ASCII);
Size imageSize = flattenedImage.GetSize ();
ReadOnlySpan<ColorBgra> pixelData = flattenedImage.GetReadOnlyPixelData ();
writer.WriteLine ("P3"); // Magic number for text-based portable pixmap format
writer.WriteLine ($"{imageSize.Width} {imageSize.Height}");
writer.WriteLine ("255");
for (int row = 0; row < imageSize.Height; row ) {
int rowStart = row * imageSize.Width;
int rowEnd = rowStart imageSize.Width;
for (int index = rowStart; index < rowEnd; index ) {
ColorBgra color = pixelData[index];
string r = color.R.ToString ().PadLeft (3, ' ');
string g = color.G.ToString ().PadLeft (3, ' ');
string b = color.B.ToString ().PadLeft (3, ' ');
writer.Write ($"{r} {g} {b}");
if (index != rowEnd - 1)
writer.Write (" ");
}
writer.WriteLine ();
}
writer.Close ();
}

public void Export (Document document, Gio.File file, Window parent)
{
ImageSurface flattenedImage = document.GetFlattenedImage ();
using GioStream outputStream = new (file.Replace ());
Export (flattenedImage, outputStream);
}
}
15 changes: 13 additions & 2 deletions Pinta.Core/Managers/ImageConverterManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 43,20 @@ private static IEnumerable<FormatDescriptor> GetInitialFormats ()
yield return CreateFormatDescriptor (format);

// Create all the formats we have our own importers/exporters for
OraFormat oraHandler = new OraFormat ();
var oraFormatDescriptor = new FormatDescriptor ("OpenRaster", new string[] { "ora", "ORA" }, new string[] { "image/openraster" }, oraHandler, oraHandler);

OraFormat oraHandler = new ();
FormatDescriptor oraFormatDescriptor = new ("OpenRaster", new[] { "ora", "ORA" }, new[] { "image/openraster" }, oraHandler, oraHandler);
yield return oraFormatDescriptor;

NetpbmPortablePixmap netpbmPortablePixmap = new ();
FormatDescriptor netpbmPortablePixmapDescriptor = new (
displayPrefix: "Netpbm Portable Pixmap",
extensions: new[] { "ppm", "PPM" },
mimes: new[] { "image/x-portable-pixmap" }, // Not official, but conventional
importer: null,
exporter: netpbmPortablePixmap
);
yield return netpbmPortablePixmapDescriptor;
}

private static FormatDescriptor CreateFormatDescriptor (PixbufFormat format)
Expand Down
Binary file added tests/Pinta.Core.Tests/Assets/sixcolorsinput.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions tests/Pinta.Core.Tests/Assets/sixcolorsoutput_crlf.ppm
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
P3
3 2
255
255 0 0 0 255 0 0 0 255
255 255 0 255 255 255 0 0 0
5 changes: 5 additions & 0 deletions tests/Pinta.Core.Tests/Assets/sixcolorsoutput_lf.ppm
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
P3
3 2
255
255 0 0 0 255 0 0 0 255
255 255 0 255 255 255 0 0 0
52 changes: 52 additions & 0 deletions tests/Pinta.Core.Tests/FileFormatTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,52 @@
using System.Collections.Generic;
using Cairo;
using NUnit.Framework;

namespace Pinta.Core.Tests;

[TestFixture]
internal sealed class FileFormatTests
{
[TestCase ("sixcolorsinput.gif", "sixcolorsoutput_lf.ppm")]
[TestCase ("sixcolorsinput.gif", "sixcolorsoutput_crlf.ppm")]
//[TestCase ("sixcolorsoutput_lf.ppm", "sixcolorsoutput_crlf.ppm")] // TODO: Test reads them as equal, but they're not
public void Files_NotEqual (string file1, string file2)
{
var path1 = Utilities.GetAssetPath (file1);
var path2 = Utilities.GetAssetPath (file2);
Assert.IsFalse (Utilities.AreFilesEqual (path1, path2));
}

[TestCaseSource (nameof (netpbm_pixmap_text_cases))]
public void Export_NetpbmPixmap_TextBased (string inputFile, IEnumerable<string> acceptableOutputs)
{
var inputFilePath = Utilities.GetAssetPath (inputFile);
ImageSurface loaded = Utilities.LoadImage (inputFilePath);
NetpbmPortablePixmap exporter = new ();
Gio.MemoryOutputStream memoryOutput = Gio.MemoryOutputStream.NewResizable ();
using GioStream outputStream = new (memoryOutput);
exporter.Export (loaded, outputStream);
outputStream.Close ();
memoryOutput.Close (null);
var exportedBytes = memoryOutput.StealAsBytes ();
bool matched = false;
foreach (string fileName in acceptableOutputs) {
var bytesStream = Gio.MemoryInputStream.NewFromBytes (exportedBytes);
var bytesReader = Gio.DataInputStream.New (bytesStream);
var filePath = Utilities.GetAssetPath (fileName);
using var context = Utilities.OpenFile (filePath);
if (Utilities.AreFilesEqual (bytesReader, context.DataStream)) {
matched = true;
break;
}
}
Assert.IsTrue (matched);
}

static readonly IReadOnlyList<TestCaseData> netpbm_pixmap_text_cases = new TestCaseData[] {
new (
"sixcolorsinput.gif",
new [] { "sixcolorsoutput_lf.ppm", "sixcolorsoutput_crlf.ppm" }
),
};
}
6 changes: 6 additions & 0 deletions tests/Pinta.Core.Tests/Pinta.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 12,11 @@
<ItemGroup>
<ProjectReference Include="..\..\Pinta.Core\Pinta.Core.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="Assets/*">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
110 changes: 110 additions & 0 deletions tests/Pinta.Core.Tests/Utilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 1,110 @@
using System;
using Cairo;

namespace Pinta.Core.Tests;

internal static class Utilities
{
static Utilities ()
{
Gio.Module.Initialize ();
GdkPixbuf.Module.Initialize ();
Cairo.Module.Initialize ();
Gdk.Module.Initialize ();
}

/// <returns>
/// <see langword="true"/> if the files with these file names
/// are byte-for-byte the same, <see langword="false"/> if not
/// </returns>
internal static bool AreFilesEqual (string fileName1, string fileName2)
{
var context1 = OpenFile (fileName1);
var context2 = OpenFile (fileName2);
return AreFilesEqual (context1.DataStream, context2.DataStream); ;
}

internal static DataInputStreamContext OpenFile (string filePath)
{
return new (filePath);
}

internal static string GetAssetPath (string fileName)
{
const string ASSETS_FOLDER = "Assets";
string assemblyPath = System.IO.Path.GetDirectoryName (typeof (Utilities).Assembly.Location)!;
return System.IO.Path.Combine (assemblyPath, ASSETS_FOLDER, fileName);
}

internal sealed class DataInputStreamContext : IDisposable
{
private Gio.FileInputStream FileStream { get; }
public Gio.DataInputStream DataStream { get; }

internal DataInputStreamContext (string filePath)
{
Gio.File file = Gio.FileHelper.NewForPath (filePath);
Gio.FileInputStream fs = file.Read (null);
FileStream = fs;
DataStream = Gio.DataInputStream.New (fs);
}

public void Dispose ()
{
DataStream.Dispose ();
FileStream.Dispose ();
}
}

internal static bool AreFilesEqual (Gio.DataInputStream dataStream1, Gio.DataInputStream dataStream2)
{
dataStream1.Seek (0, GLib.SeekType.Set, null);
dataStream2.Seek (0, GLib.SeekType.Set, null);
const int BUFFER_SIZE = 4096;
Span<byte> buffer1 = stackalloc byte[BUFFER_SIZE];
Span<byte> buffer2 = stackalloc byte[BUFFER_SIZE];
while (true) {

long bytesRead1 = dataStream1.Read (buffer1, null);
long bytesRead2 = dataStream2.Read (buffer2, null);

//Console.WriteLine ($"1: {bytesRead1} bytes read");
//Console.WriteLine ($"2: {bytesRead2} bytes read");

if (bytesRead1 != bytesRead2) // Different file sizes
{
//Console.WriteLine ("Different file sizes");
return false;
}

if (bytesRead1 == 0) // End of file
{
//Console.WriteLine ("End of file");
return true;
}

for (int i = 0; i < bytesRead1; i ) {
if (buffer1[i] != buffer2[i]) // Differing byte
{
//Console.WriteLine ($"Differing byte at position {i} of buffer");
return false;
}
}
}
}

public static ImageSurface LoadImage (string imageFilePath)
{
var file = Gio.FileHelper.NewForPath (imageFilePath);
using var fs = file.Read (null);
try {
var bg = GdkPixbuf.Pixbuf.NewFromStream (fs, cancellable: null);
var surf = CairoExtensions.CreateImageSurface (Format.Argb32, bg.Width, bg.Height);
var context = new Cairo.Context (surf);
context.DrawPixbuf (bg, 0, 0);
return surf;
} finally {
fs.Close (null);
}
}
}

0 comments on commit 441becd

Please sign in to comment.