Search code examples
c#rgbtiffcmyk

Concatenate a bitmap (rgb) with a TIFF (cmyk) without converting cmyk to rgb


I'm developing an application to concatenate a bitmap image in RGB with a TIFF in CMYK. I've tried with System.Drawing and System.Windows.Media namespaces.

The problem is both the libraries try to convert my TIFF image into RGB before merging, which causes a loss in image quality.

As far as I understand, the reason they always convert images into RGB before processing because the two libraries do that with a rendering intent.

I don't need to render anything, just merge the two photos and save to disk, that's all.

What should I do to achieve my goal? Clearly, I don't want to lose the quality of the TIFF so I think it's best to not do any conversion, just keep it raw and merge. Anyway, that's just a guess, other option could be considered as well. Could anybody shed some light on my case please?

See a comparison of the tiff image before and after converted from cmyk to rgb below. comparison of a TIFF image in CMYK (left) and later converted to RGB (right)


Solution

  • I’m not aware of any capacity in the TIFF format to have two different color spaces at the same time. Since you are dealing in CMYK, I assume that is the one you want to preserve.

    If so, the steps to do so would be:

    1. Load CMYK image A (using BitmapDecoder)
    2. Load RGB image B (using BitmapDecoder)
    3. Convert image B to CMYK with the desired color profile (using FormatConvertedBitmap)
    4. If required, ensure the pixel format for image B matches A (using FormatConvertedBitmap)
    5. Composite the two in memory as a byte array (using CopyPixels, then memory manipulation, then new bitmap from the memory)
    6. Save the composite to a new CMYK TIFF file (using TiffBitmapEncoder)

    That should be possible with WIC (System.Media).

    An example doing so (github) could be written as:

    BitmapFrame LoadTiff(string filename)
    {
        using (var rs = File.OpenRead(filename))
        {
            return BitmapDecoder.Create(rs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad).Frames[0];
        }
    }
    
    // Load, validate A
    var imageA = LoadTiff("CMYK.tif");
    if (imageA.Format != PixelFormats.Cmyk32)
    {
        throw new InvalidOperationException("imageA is not CMYK");
    }
    
    // Load, validate, convert B
    var imageB = LoadTiff("RGB.tif");
    if (imageB.PixelHeight != imageA.PixelHeight)
    {
        throw new InvalidOperationException("Image B is not the same height as image A");
    }
    var imageBCmyk = new FormatConvertedBitmap(imageB, imageA.Format, null, 0d);
    
    // Merge
    int width = imageA.PixelWidth + imageB.PixelWidth,
        height = imageA.PixelHeight,
        bytesPerPixel = imageA.Format.BitsPerPixel / 8,
        stride = width * bytesPerPixel;
    var buffer = new byte[stride * height];
    imageA.CopyPixels(buffer, stride, 0);
    imageBCmyk.CopyPixels(buffer, stride, imageA.PixelWidth * bytesPerPixel);
    var result = BitmapSource.Create(width, height, imageA.DpiX, imageA.DpiY, imageA.Format, null, buffer, stride);
    
    // save to new file
    using (var ws = File.Create("out.tif"))
    {
        var tiffEncoder = new TiffBitmapEncoder();
        tiffEncoder.Frames.Add(BitmapFrame.Create(result));
        tiffEncoder.Save(ws);
    }
    

    Which maintains color accuracy of the CMYK image, and converts the RGB using the system color profile. This can be verified in Photoshop which shows that the each letter, and rich black, have maintained their original values. (note that imgur does convert to png with dubious color handling - check github for originals.)

    Image A (CMYK): Image 1 - CMYK, white, Rich Black

    Image B (RGB): Image B - RGB, white, black

    Result (CMYK): Image Result - RGB with CMYK maintained exactly, RGB altered.

    To have the two images overlayed, one image would have to have some notion of transparency. A mask would be one example thereof, where you pick a particular color value to mean "transparent". The downside of a mask is that masks do not play well with aliased source images. For that, you would want to do an alpha channel - but blending across color spaces would be challenging. (Github)

    // Load, validate A
    var imageA = LoadTiff("CMYK.tif");
    if (imageA.Format != PixelFormats.Cmyk32)
    {
        throw new InvalidOperationException("imageA is not CMYK");
    }
    
    // Load, validate, convert B
    var imageB = LoadTiff("RGBOverlay.tif");
    if (imageB.PixelHeight != imageA.PixelHeight
        || imageB.PixelWidth != imageA.PixelWidth)
    {
        throw new InvalidOperationException("Image B is not the same size as image A");
    }
    var imageBBGRA = new FormatConvertedBitmap(imageB, PixelFormats.Bgra32, null, 0d);
    var imageBCmyk = new FormatConvertedBitmap(imageB, imageA.Format, null, 0d);
    
    // Merge
    int width = imageA.PixelWidth, height = imageA.PixelHeight;
    var stride = width * (imageA.Format.BitsPerPixel / 8);
    var bufferA = new uint[width * height];
    var bufferB = new uint[width * height];
    var maskBuffer = new uint[width * height];
    imageA.CopyPixels(bufferA, stride, 0);
    imageBBGRA.CopyPixels(maskBuffer, stride, 0);
    imageBCmyk.CopyPixels(bufferB, stride, 0);
    
    for (int i = 0; i < bufferA.Length; i++)
    {
        // set pixel in bufferA to the value from bufferB if mask is not white
        if (maskBuffer[i] != 0xffffffff)
        {
            bufferA[i] = bufferB[i];
        }
    }
    
    var result = BitmapSource.Create(width, height, imageA.DpiX, imageA.DpiY, imageA.Format, null, bufferA, stride);
    
    // save to new file
    using (var ws = File.Create("out_overlay.tif"))
    {
        var tiffEncoder = new TiffBitmapEncoder();
        tiffEncoder.Frames.Add(BitmapFrame.Create(result));
        tiffEncoder.Save(ws);
    }
    

    Example image B: RGB Overlay image

    Example output: CMYK Overlay output