using System.Globalization; using Gdk; using NAPS2.Images.Bitwise; using NAPS2.Util; namespace NAPS2.Images.Gtk; public class GtkImage : IMemoryImage { public GtkImage(Pixbuf pixbuf) { LeakTracer.StartTracking(this); Pixbuf = pixbuf; HorizontalResolution = float.TryParse(pixbuf.GetOption("x-dpi"), out var xDpi) ? xDpi : 0; VerticalResolution = float.TryParse(pixbuf.GetOption("y-dpi"), out var yDpi) ? yDpi : 0; } public ImageContext ImageContext { get; } = new GtkImageContext(); public Pixbuf Pixbuf { get; } public int Width => Pixbuf.Width; public int Height => Pixbuf.Height; public float HorizontalResolution { get; private set; } public float VerticalResolution { get; private set; } public void SetResolution(float xDpi, float yDpi) { HorizontalResolution = xDpi; VerticalResolution = yDpi; } public ImagePixelFormat PixelFormat => (Pixbuf.NChannels, Pixbuf.BitsPerSample) switch { (3, 8) => ImagePixelFormat.RGB24, (4, 8) => ImagePixelFormat.ARGB32, _ => throw new InvalidOperationException("Unsupported pixel format") }; public ImageLockState Lock(LockMode lockMode, out BitwiseImageData imageData) { if (lockMode != LockMode.ReadOnly) { LogicalPixelFormat = ImagePixelFormat.Unknown; } var ptr = Pixbuf.Pixels; var stride = Pixbuf.Rowstride; var subPixelType = PixelFormat switch { ImagePixelFormat.RGB24 => SubPixelType.Rgb, ImagePixelFormat.ARGB32 => SubPixelType.Rgba, _ => throw new InvalidOperationException("Unsupported pixel format") }; imageData = new BitwiseImageData(ptr, new PixelInfo(Width, Height, subPixelType, stride)); return new GtkImageLockState(); } // TODO: Should we implement some kind of actual locking? internal class GtkImageLockState : ImageLockState { public override void Dispose() { } } public ImageFileFormat OriginalFileFormat { get; set; } public ImagePixelFormat LogicalPixelFormat { get; set; } public void Save(string path, ImageFileFormat imageFormat = ImageFileFormat.Unknown, ImageSaveOptions? options = null) { if (imageFormat == ImageFileFormat.Unknown) { imageFormat = ImageContext.GetFileFormatFromExtension(path); } if (imageFormat == ImageFileFormat.Tiff) { ((GtkImageContext) ImageContext).TiffIo.SaveTiff([this], path); return; } ImageContext.CheckSupportsFormat(imageFormat); options ??= new ImageSaveOptions(); var type = GetType(imageFormat); var (keys, values) = GetSaveOptions(imageFormat, options.Quality); using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.RGB24); helper.Image.Pixbuf.Savev(path, type, keys, values); } public void Save(Stream stream, ImageFileFormat imageFormat, ImageSaveOptions? options = null) { if (imageFormat == ImageFileFormat.Unknown) { throw new ArgumentException("Format required to save to a stream", nameof(imageFormat)); } if (imageFormat == ImageFileFormat.Tiff) { ((GtkImageContext) ImageContext).TiffIo.SaveTiff([this], stream); return; } ImageContext.CheckSupportsFormat(imageFormat); options ??= new ImageSaveOptions(); var type = GetType(imageFormat); var (keys, values) = GetSaveOptions(imageFormat, options.Quality); // TODO: GDK doesn't support optimizing bit depth (e.g. 1bit/8bit instead of 24bit/32bit) for BMP/PNG/JPEG. // We'd probably need to use libpng/libjpeg etc. directly to fix that. using var helper = PixelFormatHelper.Create(this, options.PixelFormatHint, minFormat: ImagePixelFormat.RGB24); // TODO: Map to OutputStream directly? stream.Write(helper.Image.Pixbuf.SaveToBuffer(type, keys, values)); } private string GetType(ImageFileFormat fileFormat) => fileFormat switch { ImageFileFormat.Jpeg => "jpeg", ImageFileFormat.Png => "png", ImageFileFormat.Bmp => "bmp", ImageFileFormat.Tiff => "tiff", _ => throw new ArgumentException("Unsupported file format") }; private (string[] keys, string[] values) GetSaveOptions(ImageFileFormat imageFormat, int quality) { var keys = new List(); var values = new List(); if (HorizontalResolution > 0 && VerticalResolution > 0) { keys.Add("x-dpi"); keys.Add("y-dpi"); values.Add(HorizontalResolution.ToString(CultureInfo.InvariantCulture)); values.Add(VerticalResolution.ToString(CultureInfo.InvariantCulture)); } if (imageFormat == ImageFileFormat.Jpeg && quality != -1) { keys.Add("quality"); values.Add(quality.ToString()); } return (keys.ToArray(), values.ToArray()); } public IMemoryImage Clone() => new GtkImage((Pixbuf) Pixbuf.Clone()) { OriginalFileFormat = OriginalFileFormat, LogicalPixelFormat = LogicalPixelFormat, HorizontalResolution = HorizontalResolution, VerticalResolution = VerticalResolution }; public void Dispose() { Pixbuf.Dispose(); LeakTracer.StopTracking(this); } }