4

Currently, SkiaSharp doesn't support tiff images. (It supports jpg, gif, bmp, png, and a few others.)

How can you convert a tiff image into a SKBitmap object?

One idea: Perhaps there's an efficient way to convert a tiff stream > png stream > SKBitmap object? I'm not sure System.Drawing could handle the tiff>png stream efficiently. Another possible option is LibTiff.Net, though would need an example of how to convert a tiff stream to a png stream.

Another idea: Access the tiff pixels and draw it directly onto a SKCanvas?

Other ideas?

Doug S
  • 10,146
  • 3
  • 40
  • 45

2 Answers2

14

@DougS

Your implementation is mostly correct, but it is not very performant because of multiple memory allocations and copies.

I noticed that you are creating 3 memory chunks with a total size of (w*h*4 bytes) each:

// the int[]
raster = new int[width * height];

// the SKColor[]
pixels = new SKColor[width * height];

// the bitmap
bitmap = new SKBitmap(width, height)

You are also copying the pixels between the memory multiple times:

// decode the TIFF (first copy)
tifImg.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT)

// convert to SKColor (second copy)
pixels[arrayOffset] = new SKColor(...);

// set bitmap pixels (third copy)
bitmap.Pixels = pixels;

I think I managed to create a similar method that decodes the stream, with only a single copy and memory allocation:

public static SKBitmap OpenTiff(Stream tiffStream)
{
    // open a TIFF stored in the stream
    using (var tifImg = Tiff.ClientOpen("in-memory", "r", tiffStream, new TiffStream()))
    {
        // read the dimensions
        var width = tifImg.GetField(TiffTag.IMAGEWIDTH)[0].ToInt();
        var height = tifImg.GetField(TiffTag.IMAGELENGTH)[0].ToInt();

        // create the bitmap
        var bitmap = new SKBitmap();
        var info = new SKImageInfo(width, height);

        // create the buffer that will hold the pixels
        var raster = new int[width * height];

        // get a pointer to the buffer, and give it to the bitmap
        var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);
        bitmap.InstallPixels(info, ptr.AddrOfPinnedObject(), info.RowBytes, null, (addr, ctx) => ptr.Free(), null);

        // read the image into the memory buffer
        if (!tifImg.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT))
        {
            // not a valid TIF image.
            return null;
        }

        // swap the red and blue because SkiaSharp may differ from the tiff
        if (SKImageInfo.PlatformColorType == SKColorType.Bgra8888)
        {
            SKSwizzle.SwapRedBlue(ptr.AddrOfPinnedObject(), raster.Length);
        }

        return bitmap;
    }
}

A gist lives here: https://gist.github.com/mattleibow/0a09babdf0dc9d2bc3deedf85f9b57d6

Let me explain the code... I basically am creating the int[] as you are, but then passing that to the SKBitmap and letting it take over. I am pinning it as the SKBitmap lives in unmanaged memory and the GC may move it, but I am sure to unpin it when the bitmap is disposed.

Here is a more detailed step through:

// this does not actually allocate anything
//  - the size is 0x0 / 0 bytes of pixels
var bitmap = new SKBitmap();

// I create the only buffer for pixel data
var raster = new int[width * height];

// pin the managed array so it can be passed to unmanaged memory
var ptr = GCHandle.Alloc(raster, GCHandleType.Pinned);

// pass the pointer of the array to the bitmap
// making sure to free the pinned memory in the dispose delegate
//  - this is also not an allocation, as the memory already exists
bitmap.InstallPixels(info, ptr.AddrOfPinnedObject(), info.RowBytes, null, (addr, ctx) => ptr.Free(), null);

// the first and only copy from the TIFF stream into memory
tifImg.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT)

// an unfortunate extra memory operation for some platforms
//  - this is usually just for Windows as it uses a BGR color format
//  - Linux, macOS, iOS, Android all are RGB, so no swizzle is needed
SKSwizzle.SwapRedBlue(ptr.AddrOfPinnedObject(), raster.Length);

Just for some raw stats from a debug session, your code takes about 500ms for one of my images, but my code only takes 20ms.

I hope I don't sound too harsh/negative towards your code, I am not meaning that in any way.

Matthew
  • 4,832
  • 2
  • 29
  • 55
  • 1
    Hi @Matthew. Two things: 1) I think you should set the destination `ColorSpace` via `var info = new SKImageInfo(width, height, SKImageInfo.PlatformColorType, SKAlphaType.Premul, SKColorSpace.CreateSrgb());` so any further processing by Skia will allow Skia to know what the `ColorSpace` is. 2) CMYK Tif's processed by this code result in overly-saturated colors. How can we fix that? – Doug S May 19 '18 at 18:42
  • @Matthew What could be done in the case of multiple page TIF file? – MShah Dec 05 '19 at 14:46
  • I am not too familiar with the LibTiff.Net library, but I am assuming there is a way to read each page. I used ReadRGBAImageOriented as that what was in the original answer. I would have a look at the usages of "Directory" as I think that is what they use as pages. – Matthew Dec 28 '19 at 17:05
  • @DougS Have you found solution for the second point (CMYK Tif's processed by this code result in overly-saturated colors)? – maxlego Sep 07 '20 at 10:23
  • @maxlego I don't believe I ever found a solution to "CMYK Tif's result in overly-saturated colors". – Doug S Sep 08 '20 at 13:06
  • Perhaps @Matthew has additional feedback. Or better yet, a version of SkiaSharp that natively supports Tif's. – Doug S Sep 08 '20 at 13:08
  • @Matthew Great answer and example code! Based on your example I'm trying to make a function to do the opposite, save a SKBitmap to a tiff image in a high performance manner. I've ask a question about it [here](https://stackoverflow.com/questions/72377772/fast-lossless-encoding-of-skbitmap-images). Maybe you can shine you light on it? ;) – MrEighteen May 27 '22 at 08:06
2

I'm no expert, so I welcome any expert who can make this code more efficient (or has completely different ideas to get a tiff into a SKBitmap).

This uses LibTiff.Net

using BitMiracle.LibTiff.Classic;

. . . .

public static void ConvertTiffToSKBitmap(MemoryStream tifImage)
{
    SKColor[] pixels;
    int width, height;
    // open a Tiff stored in the memory stream, and grab its pixels
    using (Tiff tifImg = Tiff.ClientOpen("in-memory", "r", tifImage, new TiffStream()))
    {
        FieldValue[] value = tifImg.GetField(TiffTag.IMAGEWIDTH);
        width = value[0].ToInt();

        value = tifImg.GetField(TiffTag.IMAGELENGTH);
        height = value[0].ToInt();

        // Read the image into the memory buffer 
        int[] raster = new int[width * height];
        if (!tifImg.ReadRGBAImageOriented(width, height, raster, Orientation.TOPLEFT))
        {
            // Not a valid TIF image.
        }
        // store the pixels
        pixels = new SKColor[width * height];
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                int arrayOffset = y * width + x;
                int rgba = raster[arrayOffset];
                pixels[arrayOffset] = new SKColor((byte)Tiff.GetR(rgba), (byte)Tiff.GetG(rgba), (byte)Tiff.GetB(rgba), (byte)Tiff.GetA(rgba));
            }
        }
    }
    using (SKBitmap bitmap = new SKBitmap(width, height))
    {
        bitmap.Pixels = pixels;

        // do something with the SKBitmap
    }
}
Doug S
  • 10,146
  • 3
  • 40
  • 45