mirror of
https://github.com/cyanfish/naps2.git
synced 2024-08-16 18:50:45 +03:00
Gtk: Tiff saving
This commit is contained in:
parent
e628127547
commit
76a8778752
@ -1,5 +1,6 @@
|
||||
using System.Threading;
|
||||
using Gdk;
|
||||
using NAPS2.Images.Bitwise;
|
||||
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
@ -63,42 +64,138 @@ public class GtkImageContext : ImageContext
|
||||
return new[] { Load(path) };
|
||||
}
|
||||
|
||||
public override bool SaveTiff(IList<IMemoryImage> images, string path, TiffCompressionType compression = TiffCompressionType.Auto,
|
||||
public override bool SaveTiff(IList<IMemoryImage> images, string path,
|
||||
TiffCompressionType compression = TiffCompressionType.Auto,
|
||||
Action<int, int>? progressCallback = null, CancellationToken cancelToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var tiff = LibTiff.TIFFOpen(path, "w");
|
||||
return WriteTiff(tiff, null, images, compression, progressCallback, cancelToken);
|
||||
}
|
||||
|
||||
public override bool SaveTiff(IList<IMemoryImage> images, Stream stream, TiffCompressionType compression = TiffCompressionType.Auto,
|
||||
public override bool SaveTiff(IList<IMemoryImage> images, Stream stream,
|
||||
TiffCompressionType compression = TiffCompressionType.Auto,
|
||||
Action<int, int>? progressCallback = null, CancellationToken cancelToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
var client = new LibTiffStreamClient(stream);
|
||||
var tiff = client.TIFFClientOpen("w");
|
||||
return WriteTiff(tiff, client, images, compression, progressCallback, cancelToken);
|
||||
}
|
||||
|
||||
private bool WriteTiff(IntPtr tiff, LibTiffStreamClient client, IList<IMemoryImage> images,
|
||||
TiffCompressionType compression, Action<int, int>? progressCallback, CancellationToken cancelToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
int i = 0;
|
||||
progressCallback?.Invoke(0, images.Count);
|
||||
foreach (var image in images)
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested) return false;
|
||||
var pixelFormat =
|
||||
image.LogicalPixelFormat == ImagePixelFormat.BW1 || compression == TiffCompressionType.Ccitt4
|
||||
? ImagePixelFormat.BW1
|
||||
: image.LogicalPixelFormat;
|
||||
WriteTiffMetadata(tiff, pixelFormat, compression, image);
|
||||
WriteTiffImageData(tiff, pixelFormat, image);
|
||||
if (images.Count > 1)
|
||||
{
|
||||
LibTiff.TIFFWriteDirectory(tiff);
|
||||
}
|
||||
progressCallback?.Invoke(++i, images.Count);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LibTiff.TIFFClose(tiff);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void WriteTiffImageData(IntPtr tiff, ImagePixelFormat pixelFormat, IMemoryImage image)
|
||||
{
|
||||
var bufferInfo = new PixelInfo(image.Width, image.Height, pixelFormat switch
|
||||
{
|
||||
ImagePixelFormat.ARGB32 => SubPixelType.Rgba,
|
||||
ImagePixelFormat.RGB24 => SubPixelType.Rgb,
|
||||
ImagePixelFormat.Gray8 => SubPixelType.Gray,
|
||||
ImagePixelFormat.BW1 => SubPixelType.Bit
|
||||
});
|
||||
var buffer = new byte[bufferInfo.Length];
|
||||
new CopyBitwiseImageOp().Perform(image, buffer, bufferInfo);
|
||||
fixed (byte* buf = buffer)
|
||||
{
|
||||
for (int i = 0; i < image.Height; i++)
|
||||
{
|
||||
LibTiff.TIFFWriteScanline(tiff, (IntPtr) (buf + bufferInfo.Stride * i), i, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteTiffMetadata(IntPtr tiff, ImagePixelFormat pixelFormat,
|
||||
TiffCompressionType compression, IMemoryImage image)
|
||||
{
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.ImageWidth, image.Width);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.ImageHeight, image.Height);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.PlanarConfig, 1);
|
||||
// TODO: Test setting g4 compression when it's not a BW image
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.Compression, (int) (compression switch
|
||||
{
|
||||
TiffCompressionType.Auto => pixelFormat == ImagePixelFormat.BW1
|
||||
? TiffCompression.G4
|
||||
: TiffCompression.Lzw,
|
||||
TiffCompressionType.Ccitt4 => TiffCompression.G4,
|
||||
TiffCompressionType.Lzw => TiffCompression.Lzw,
|
||||
TiffCompressionType.None => TiffCompression.None
|
||||
}));
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.Orientation, 1);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.BitsPerSample, pixelFormat == ImagePixelFormat.BW1 ? 1 : 8);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.SamplesPerPixel, pixelFormat switch
|
||||
{
|
||||
ImagePixelFormat.RGB24 => 3,
|
||||
ImagePixelFormat.ARGB32 => 4,
|
||||
_ => 1
|
||||
});
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.Photometric, (int) (pixelFormat switch
|
||||
{
|
||||
ImagePixelFormat.RGB24 or ImagePixelFormat.ARGB32 => TiffPhotometric.Rgb,
|
||||
_ => TiffPhotometric.MinIsBlack
|
||||
}));
|
||||
if (pixelFormat == ImagePixelFormat.ARGB32)
|
||||
{
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.ExtraSamples, 1);
|
||||
}
|
||||
if (image.HorizontalResolution != 0 && image.VerticalResolution != 0)
|
||||
{
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.ResolutionUnit, 2);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.XResolution, (int) image.HorizontalResolution);
|
||||
LibTiff.TIFFSetField(tiff, TiffTag.YResolution, (int) image.VerticalResolution);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<IMemoryImage> LoadTiff(Stream stream, out int count)
|
||||
{
|
||||
var c = new LibTiffStreamClient(stream);
|
||||
var tiff = c.TIFFClientOpen("r");
|
||||
var client = new LibTiffStreamClient(stream);
|
||||
var tiff = client.TIFFClientOpen("r");
|
||||
count = LibTiff.TIFFNumberOfDirectories(tiff);
|
||||
return EnumerateTiffFrames(tiff, c);
|
||||
return EnumerateTiffFrames(tiff, client);
|
||||
}
|
||||
|
||||
private IEnumerable<IMemoryImage> LoadTiff(string path, out int count)
|
||||
{
|
||||
var tiff = LibTiff.TIFFOpen(path, "r");
|
||||
count = LibTiff.TIFFNumberOfDirectories(tiff);
|
||||
return EnumerateTiffFrames(tiff, null);
|
||||
return EnumerateTiffFrames(tiff);
|
||||
}
|
||||
|
||||
private IEnumerable<IMemoryImage> EnumerateTiffFrames(IntPtr tiff, LibTiffStreamClient? client)
|
||||
private IEnumerable<IMemoryImage> EnumerateTiffFrames(IntPtr tiff, LibTiffStreamClient? client = null)
|
||||
{
|
||||
// We keep a reference to the client to avoid garbage collection
|
||||
try
|
||||
{
|
||||
do
|
||||
{
|
||||
LibTiff.TIFFGetField(tiff, 256, out var w);
|
||||
LibTiff.TIFFGetField(tiff, 257, out var h);
|
||||
LibTiff.TIFFGetField(tiff, TiffTag.ImageWidth, out var w);
|
||||
LibTiff.TIFFGetField(tiff, TiffTag.ImageHeight, out var h);
|
||||
var img = Create(w, h, ImagePixelFormat.ARGB32);
|
||||
img.OriginalFileFormat = ImageFileFormat.Tiff;
|
||||
using var imageLock = img.Lock(LockMode.WriteOnly, out var data);
|
||||
|
@ -6,7 +6,7 @@ using tdata_t = System.IntPtr;
|
||||
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
public static class LibTiff
|
||||
internal static class LibTiff
|
||||
{
|
||||
// TODO: String marshalling?
|
||||
[DllImport("libtiff.so.5")]
|
||||
@ -54,7 +54,17 @@ public static class LibTiff
|
||||
public static extern int TIFFReadDirectory(IntPtr tiff);
|
||||
|
||||
[DllImport("libtiff.so.5")]
|
||||
public static extern int TIFFGetField(IntPtr tiff, int tag, out int field);
|
||||
public static extern int TIFFWriteDirectory(IntPtr tiff);
|
||||
|
||||
[DllImport("libtiff.so.5")]
|
||||
public static extern int TIFFGetField(IntPtr tiff, TiffTag tag, out int field);
|
||||
|
||||
[DllImport("libtiff.so.5")]
|
||||
public static extern int TIFFSetField(IntPtr tiff, TiffTag tag, int field);
|
||||
|
||||
[DllImport("libtiff.so.5")]
|
||||
public static extern int TIFFWriteScanline(
|
||||
IntPtr tiff, tdata_t buf, int row, short sample);
|
||||
|
||||
[DllImport("libtiff.so.5")]
|
||||
public static extern int TIFFReadRGBAImage(
|
||||
|
@ -6,7 +6,7 @@ using tdata_t = System.IntPtr;
|
||||
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
public class LibTiffStreamClient
|
||||
internal class LibTiffStreamClient
|
||||
{
|
||||
private readonly Stream _stream;
|
||||
private readonly LibTiff.TIFFErrorHandler _error;
|
||||
|
11
NAPS2.Images.Gtk/TiffCompression.cs
Normal file
11
NAPS2.Images.Gtk/TiffCompression.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
internal enum TiffCompression
|
||||
{
|
||||
None = 1,
|
||||
G4 = 4,
|
||||
Lzw = 5,
|
||||
Jpeg = 7,
|
||||
Deflate = 32946,
|
||||
Lzma = 34925
|
||||
}
|
7
NAPS2.Images.Gtk/TiffPhotometric.cs
Normal file
7
NAPS2.Images.Gtk/TiffPhotometric.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
internal enum TiffPhotometric
|
||||
{
|
||||
MinIsBlack = 1,
|
||||
Rgb = 2
|
||||
}
|
22
NAPS2.Images.Gtk/TiffTag.cs
Normal file
22
NAPS2.Images.Gtk/TiffTag.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace NAPS2.Images.Gtk;
|
||||
|
||||
internal enum TiffTag
|
||||
{
|
||||
SubFileType = 254,
|
||||
ImageWidth = 256,
|
||||
ImageHeight = 257,
|
||||
BitsPerSample = 258,
|
||||
Compression = 259,
|
||||
Photometric = 262,
|
||||
DocumentName = 269,
|
||||
ImageDescription = 270,
|
||||
Make = 271,
|
||||
Model = 272,
|
||||
Orientation = 274,
|
||||
SamplesPerPixel = 277,
|
||||
XResolution = 282,
|
||||
YResolution = 283,
|
||||
PlanarConfig = 284,
|
||||
ResolutionUnit = 296,
|
||||
ExtraSamples = 338
|
||||
}
|
@ -162,6 +162,7 @@ public abstract class ImageContext
|
||||
/// <returns></returns>
|
||||
public abstract IEnumerable<IMemoryImage> LoadFrames(string path, out int count);
|
||||
|
||||
// TODO: Instead of having these methods directly here, instead have a TiffWriter property
|
||||
public abstract bool SaveTiff(IList<IMemoryImage> images, string path,
|
||||
TiffCompressionType compression = TiffCompressionType.Auto, Action<int, int>? progressCallback = null,
|
||||
CancellationToken cancelToken = default);
|
||||
|
@ -6,7 +6,8 @@ namespace NAPS2.Sdk.Tests.Images;
|
||||
|
||||
public class LoadSaveTests : ContextualTests
|
||||
{
|
||||
// TODO: Add tests for error/edge cases (e.g. invalid files, mismatched extensions/format (?), tiff progress)
|
||||
// TODO: Add tests for error/edge cases (e.g. invalid files, mismatched extensions/format (?), tiff progress, saving 0/null images, unicode file names)
|
||||
// TODO: Verify rough expected file sizes to ensure compression is as expected (especially for tiff saving)
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(TestCases))]
|
||||
@ -89,12 +90,7 @@ public class LoadSaveTests : ContextualTests
|
||||
var original = LoadImage(ImageResources.color_image);
|
||||
|
||||
ImageContext.SaveTiff(new[] { original }, path);
|
||||
var actual = ImageContext.LoadFrames(path, out var count).ToArray();
|
||||
|
||||
Assert.Equal(1, count);
|
||||
Assert.Single(actual);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[0].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image, actual[0]);
|
||||
AssertTiff(path, ImageResources.color_image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -104,21 +100,12 @@ public class LoadSaveTests : ContextualTests
|
||||
var original = new[]
|
||||
{
|
||||
LoadImage(ImageResources.color_image),
|
||||
LoadImage(ImageResources.color_image_h_p300),
|
||||
LoadImage(ImageResources.color_image_bw).PerformTransform(new BlackWhiteTransform()),
|
||||
LoadImage(ImageResources.stock_cat)
|
||||
};
|
||||
|
||||
ImageContext.SaveTiff(original, path);
|
||||
var actual = ImageContext.LoadFrames(path, out var count).ToArray();
|
||||
|
||||
Assert.Equal(3, count);
|
||||
Assert.Equal(3, original.Length);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[0].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image, actual[0]);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[1].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image_h_p300, actual[1]);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[2].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.stock_cat, actual[2]);
|
||||
AssertTiff(path, ImageResources.color_image, ImageResources.color_image_bw, ImageResources.stock_cat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -128,13 +115,7 @@ public class LoadSaveTests : ContextualTests
|
||||
var original = LoadImage(ImageResources.color_image);
|
||||
|
||||
ImageContext.SaveTiff(new[] { original }, stream);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var actual = ImageContext.LoadFrames(stream, out var count).ToArray();
|
||||
|
||||
Assert.Equal(1, count);
|
||||
Assert.Single(actual);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[0].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image, actual[0]);
|
||||
AssertTiff(stream, ImageResources.color_image);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@ -144,22 +125,57 @@ public class LoadSaveTests : ContextualTests
|
||||
var original = new[]
|
||||
{
|
||||
LoadImage(ImageResources.color_image),
|
||||
LoadImage(ImageResources.color_image_h_p300),
|
||||
LoadImage(ImageResources.color_image_bw).PerformTransform(new BlackWhiteTransform()),
|
||||
LoadImage(ImageResources.stock_cat)
|
||||
};
|
||||
|
||||
ImageContext.SaveTiff(original, stream);
|
||||
AssertTiff(stream, ImageResources.color_image, ImageResources.color_image_bw, ImageResources.stock_cat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveBlackAndWhiteTiff()
|
||||
{
|
||||
var path = Path.Combine(FolderPath, "image.tiff");
|
||||
var original = LoadImage(ImageResources.color_image_bw);
|
||||
original = original.PerformTransform(new BlackWhiteTransform());
|
||||
|
||||
ImageContext.SaveTiff(new[] { original }, path);
|
||||
AssertTiff(path, ImageResources.color_image_bw);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveColorTiffWithG4()
|
||||
{
|
||||
var path = Path.Combine(FolderPath, "image.tiff");
|
||||
var original = LoadImage(ImageResources.color_image);
|
||||
|
||||
ImageContext.SaveTiff(new[] { original }, path, TiffCompressionType.Ccitt4);
|
||||
AssertTiff(path, ImageResources.color_image_bw);
|
||||
}
|
||||
|
||||
private void AssertTiff(string path, params byte[][] expectedImages)
|
||||
{
|
||||
var actual = ImageContext.LoadFrames(path, out var count).ToArray();
|
||||
DoAssertTiff(actual, count, expectedImages);
|
||||
}
|
||||
|
||||
private void AssertTiff(Stream stream, params byte[][] expectedImages)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
var actual = ImageContext.LoadFrames(stream, out var count).ToArray();
|
||||
DoAssertTiff(actual, count, expectedImages);
|
||||
}
|
||||
|
||||
Assert.Equal(3, count);
|
||||
Assert.Equal(3, original.Length);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[0].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image, actual[0]);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[1].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.color_image_h_p300, actual[1]);
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[2].OriginalFileFormat);
|
||||
ImageAsserts.Similar(ImageResources.stock_cat, actual[2]);
|
||||
private static void DoAssertTiff(IMemoryImage[] actual, int count, byte[][] expectedImages)
|
||||
{
|
||||
Assert.Equal(expectedImages.Length, count);
|
||||
Assert.Equal(expectedImages.Length, actual.Length);
|
||||
for (int i = 0; i < expectedImages.Length; i++)
|
||||
{
|
||||
Assert.Equal(ImageFileFormat.Tiff, actual[i].OriginalFileFormat);
|
||||
ImageAsserts.Similar(expectedImages[i], actual[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] GetResource(string resource) =>
|
||||
|
Loading…
Reference in New Issue
Block a user