Introduction
Sometimes default PDF to TIFF conversion doesn't provide the desired results, especially if it comes to black and white tiffs, containing so-called bitonal images. We've recently added a new mechanism into our Apitron PDF Rasterizer product which allows you to use custom bitonal image conversion implementation if you need it. See the sample below to see how it works.
The code
Provided code sample is a demo of custom bitonal conversion implementation and shows one of the possible ways to convert an RGB image to its black and white analog.
The idea is to scale the resulting image by the factory of 8 and put variably sized pixels consisting of black dots depending on the luminosity of the source pixel. Thus one source pixel would correspond to a square formed by 64 black and white 1-bit pixels.
static void Main(string[] args)
{
using (Stream inputStream = File.OpenRead("../../data/document.pdf"),
outputStream = File.Create("out.tiff"))
{
using (Document doc = new Document(inputStream))
{
doc.SaveToTiff(outputStream, new TiffRenderingSettings()
{
// set our conversion delegate
ConvertToBitonal = MyConvertToBitonal
});
}
}
Process.Start("out.tiff");
}
private static byte[] MyConvertToBitonal(int width, int height, byte[] imageData,
{
using (Stream inputStream = File.OpenRead("../../data/document.pdf"),
outputStream = File.Create("out.tiff"))
{
using (Document doc = new Document(inputStream))
{
doc.SaveToTiff(outputStream, new TiffRenderingSettings()
{
// set our conversion delegate
ConvertToBitonal = MyConvertToBitonal
});
}
}
Process.Start("out.tiff");
}
private static byte[] MyConvertToBitonal(int width, int height, byte[] imageData,
out int resultingWidth, out int resultingHeight)
{
// we will scale the resulting image,
// each source pixel will correspond to 64 black and white pixels
int scaleFactor = 8;
resultingWidth = width * scaleFactor;
resultingHeight = height * scaleFactor;
// create resulting data buffer
byte[] resultingImage = new byte[width * resultingHeight];
double luminance;
// black pixel goes first, lighter pixels follow
byte[] pixel0 = new byte[] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
byte[] pixel1 = new byte[] {0x18, 0x3c, 0x7e, 0xFF, 0xFF, 0x7e, 0x3c, 0x18 };
byte[] pixel2 = new byte[] {0, 0x18, 0x3c, 0x7e, 0x7e, 0x3c, 0x18, 0 };
byte[] pixel3 = new byte[] {0, 0, 0x18, 0x3c, 0x3c, 0x18, 0, 0 };
byte[] pixel4 = new byte[] {0, 0, 0, 0x18, 0x18, 0, 0, 0 };
// iterate over the source pixels and modify bitonal image
// according to own algorithm
for (int y = 0, offsetY=0, stride=width*4; y < height; ++y, offsetY+=stride)
{
// base offset for final pixel(s)
int resultingPixelOffsetBase = y*width*scaleFactor;
for (int x = 0, offsetX=0; x < width; ++x, offsetX+=4)
{
// read pixel data
byte b = imageData[offsetY+offsetX];
byte g = imageData[offsetY+offsetX+1];
byte r = imageData[offsetY+offsetX+2];
// calculate the luminance
luminance = (0.2126*r + 0.7152*g + 0.0722*b);
// based on the luminance we'll select which pixel to use
// from the darkest one to the lightest
if (luminance < 50)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel0, width);
}
else if (luminance<100)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel1, width);
}
else if(luminance <150)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel2, width);
}
else if (luminance<200)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel3, width);
}
else if (luminance < 250)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel4, width);
}
}
}
return resultingImage;
}
/// Copies pixel data describing resulting pixel shape to the
/// resulting image
private static void SetPixel(byte[] resultingImage, int resultingPixelOffset, byte[] pixelData,
{
// we will scale the resulting image,
// each source pixel will correspond to 64 black and white pixels
int scaleFactor = 8;
resultingWidth = width * scaleFactor;
resultingHeight = height * scaleFactor;
// create resulting data buffer
byte[] resultingImage = new byte[width * resultingHeight];
double luminance;
// black pixel goes first, lighter pixels follow
byte[] pixel0 = new byte[] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
byte[] pixel1 = new byte[] {0x18, 0x3c, 0x7e, 0xFF, 0xFF, 0x7e, 0x3c, 0x18 };
byte[] pixel2 = new byte[] {0, 0x18, 0x3c, 0x7e, 0x7e, 0x3c, 0x18, 0 };
byte[] pixel3 = new byte[] {0, 0, 0x18, 0x3c, 0x3c, 0x18, 0, 0 };
byte[] pixel4 = new byte[] {0, 0, 0, 0x18, 0x18, 0, 0, 0 };
// iterate over the source pixels and modify bitonal image
// according to own algorithm
for (int y = 0, offsetY=0, stride=width*4; y < height; ++y, offsetY+=stride)
{
// base offset for final pixel(s)
int resultingPixelOffsetBase = y*width*scaleFactor;
for (int x = 0, offsetX=0; x < width; ++x, offsetX+=4)
{
// read pixel data
byte b = imageData[offsetY+offsetX];
byte g = imageData[offsetY+offsetX+1];
byte r = imageData[offsetY+offsetX+2];
// calculate the luminance
luminance = (0.2126*r + 0.7152*g + 0.0722*b);
// based on the luminance we'll select which pixel to use
// from the darkest one to the lightest
if (luminance < 50)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel0, width);
}
else if (luminance<100)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel1, width);
}
else if(luminance <150)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel2, width);
}
else if (luminance<200)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel3, width);
}
else if (luminance < 250)
{
SetPixel(resultingImage, resultingPixelOffsetBase+x, pixel4, width);
}
}
}
return resultingImage;
}
/// Copies pixel data describing resulting pixel shape to the
/// resulting image
private static void SetPixel(byte[] resultingImage, int resultingPixelOffset, byte[] pixelData,
int strideInBytes)
{
for (int i = 0; i < pixelData.Length; ++i)
{
resultingImage[resultingPixelOffset + strideInBytes*i] = pixelData[i];
}
}
{
for (int i = 0; i < pixelData.Length; ++i)
{
resultingImage[resultingPixelOffset + strideInBytes*i] = pixelData[i];
}
}
The source file looks as follows:
Pic. 1 Source PDF document |
And the converted document is on the image below:
Pic. 2 Converted bitonal tiff |
If you zoom in you'll see that the image consists of variable black dots that create the smooth gradient fill:
Pic.3 Bitonal image |
The complete code sample can be found in our github repo.
Conclusion
The Apitron PDF Rasterizer is a powerful and extensible cross platform library that can be used to handle all PDF rendering tasks with incredible ease and quality. If you have any questions just send us an email and we'll be happy to help you.