1

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)

Hoang Duc Nguyen
  • 389
  • 4
  • 13
  • What do you mean with "merge" here? Lay them out next to each other somehow? – AKX Dec 03 '20 at 18:08
  • Yes, lay them out next to each other somehow. For example: two images have same height, lay them 1 in left and one in right, save as one image; or they have same width, place them on top of each other to align the left and right edges, then save them as one image – Hoang Duc Nguyen Dec 03 '20 at 18:15
  • Please provide some code to explicit concatenate and how you're using Daring and Media. From the image you provided, I don't see a loss of quality, merely a shift in color space. Even if you process everything in 8 bits per channel there should not be any quality loss. – Soleil Dec 04 '20 at 09:40
  • @Soleil-MathieuPrévot can I chat with you about this? – Hoang Duc Nguyen Dec 04 '20 at 19:31
  • @HoangDucNguyen https://chat.stackoverflow.com/rooms/225549/rgb-cmjk – Soleil Dec 04 '20 at 21:29
  • I believe you'll get another piece of answer there: https://stackoverflow.com/questions/5237104/c-sharp-convert-rgb-value-to-cmyk-using-an-icc-profile and here https://stackoverflow.com/questions/2426432/convert-rgb-color-to-cmyk – Soleil Dec 04 '20 at 21:42

1 Answers1

1

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

Mitch
  • 21,223
  • 6
  • 63
  • 86
  • Could you explain the step 5 in more detail? I've tried with RenderingTargetBitmap without success, not aware of the Render there... – Hoang Duc Nguyen Dec 03 '20 at 18:41
  • @HoangDucNguyen, I added some sample code. `RenderTargetBitmap` is part of WPF - not WIC and would therefore only support RGB as far as I'm aware. I did misspeak. I meant `CopyPixels` - not `Render`. – Mitch Dec 03 '20 at 19:56
  • I'm trying to also place imageA on top of image B (now assume that image B has same width as image A), but I'm kind of lost. Is it the offset parameter that I should play with? I tried but the result looks strange. I'm trying to understand how the copypixels work with its parameter – Hoang Duc Nguyen Dec 04 '20 at 18:44
  • The only thing you can do with `CopyPixels` directly is to place them next to each other. If you want to composite them on top of each other, you would have to `bitblt` in some manner. Is the top image masked in some way? – Mitch Dec 04 '20 at 18:48
  • I don't understand what is masked. But I would just adjust imageB to have same width as image A, then would like to place A on top of B, or B on top of A. Can you help with a sample code? – Hoang Duc Nguyen Dec 04 '20 at 19:13
  • Bitblt is a general concept - not a specific code. See https://en.wikipedia.org/wiki/Bit_blit. Some examples of direct manipulation of images can be found at https://stackoverflow.com/questions/20181132/edit-raw-pixel-data-of-writeablebitmap/20182830#20182830. I also added an example of masking. – Mitch Dec 04 '20 at 19:27
  • I actually did, means that I'm able to place image A on top of B, and vice verse using CopyPixels. I'm able to confirm my understanding about the use of offset: the position **on the buffer array** where it (CopyPixels) **starts placing the pixels in bytes** of the BitmapSource into the buffer array – Hoang Duc Nguyen Dec 04 '20 at 19:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225544/discussion-between-hoang-duc-nguyen-and-mitch). – Hoang Duc Nguyen Dec 04 '20 at 19:29