Technical Article

PDF Inline Images: Why a Gray Ramp Can Flip from Black to White

A grayscale inline image looks like a photographic negative even though its sample bytes are correct. Its /Decode array may deliberately reverse the gray mapping. HotPDF reads the inline /D entry and applies that mapping after the image filter produces the samples

An inline image puts its dictionary and data directly in the page content stream. Its short dictionary keys still describe the properties needed to interpret those bytes: /D abbreviates /Decode, and /CS abbreviates /ColorSpace. Width, height and bits per component belong to the same description. When a small gray image looks inverted, those entries are worth reading before assuming that its compressed data is wrong

Where the image dictionary lives

PDF can store an image as an Image XObject or inline it in a content stream. The inline form begins with BI, followed by dictionary entries; ID starts the image data, and EI ends it. The fragment below uses the abbreviated names and places a two-pixel image on the page with a transformation. A real document still needs valid stream delimiters, whitespace and a surrounding page structure

q
100 0 0 50 72 500 cm
BI
/W 2 /H 1 /BPC 8 /CS /G
/D [1 0] /F /AHx
ID
00FF>
EI
Q

The fragment describes a one-row, two-pixel DeviceGray image with eight bits per component. /AHx abbreviates ASCIIHexDecode, so 00FF> yields the sample bytes 0 and 255. With a normal gray decode range of [0 1], those endpoints map to black and white. Reverse the range to [1 0] and the first becomes white while the second becomes black. The bytes are unchanged. The dictionary tells the renderer how to interpret them after the filter runs. The transformation gives the tiny image a visible area on the page; a proper PDF writer should construct the surrounding content stream and its delimiters

How does a Decode array map gray samples?

For an eight-bit gray sample s, divide by 255 and calculate dmin + s / 255 × (dmax − dmin). With [1 0], byte 0 maps to 1 and byte 255 maps to 0. A midpoint such as 128 should lie near the middle of the gray range, so include intermediate values when checking more than the endpoints. The Delphi routine below renders page 1 and reads the red channel at two bitmap coordinates. When you read pixel values, choose points near the centers of the image regions, away from antialiased edges. In our 144 DPI run, the reversed range produced red-channel values of 255 on the left and 0 on the right; changing only the range to [0 1] reversed those readings. For a gray image, that channel distinguishes the light and dark endpoints

uses
  SysUtils, Vcl.Graphics, HPDFDoc;

procedure PrintGrayPixels(const PdfFile: string; LeftX, RightX, Y: Integer);
var
  Pdf: THotPDF;
  Bitmap: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(PdfFile) <= 0 then
      raise Exception.Create('Cannot open the PDF');
    Bitmap := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bitmap = nil then
      raise Exception.Create('Cannot render page 1');
    try
      if (LeftX < 0) or (RightX >= Bitmap.Width) or (LeftX > RightX) or
         (Y < 0) or (Y >= Bitmap.Height) then
        raise Exception.Create('Pixel coordinates are outside the bitmap');
      Writeln('Left red channel: ', Integer(ColorToRGB(Bitmap.Canvas.Pixels[LeftX, Y])) and $FF);
      Writeln('Right red channel: ', Integer(ColorToRGB(Bitmap.Canvas.Pixels[RightX, Y])) and $FF);
    finally
      Bitmap.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

The points passed to the routine are bitmap coordinates. At 144 DPI a page point corresponds to two output pixels before the page box, rotation and image transform are applied, so locate the image in the rendered bitmap before choosing the points. The decode array stays with the image even if the page places it more than once. /DecodeParms, abbreviated /DP, serves a different purpose: it supplies parameters to a filter. JPEG marker interpretation and DCT color transformation concern the filter, while the image's decode array maps the resulting samples into its color space

Can an inline ColorSpace be an array?

Inline /CS can carry more than a simple name such as /G or /RGB. An Indexed color space is an array containing a base space, a maximum index and a lookup table. Its samples select entries in that table, and the selected entries are interpreted in the base space. Turning the array's text into a single PDF name would discard the definition. HotPDF accepts an array-valued inline color space and applies the grayscale decode range when processing DeviceGray images. We would test a reversed gray ramp and an Indexed palette in separate small pages first: the former reveals the decode endpoints, while the latter shows whether an index selects the intended palette entry

Render a controlled page in Delphi

The second Delphi routine loads a PDF and saves a bitmap of its first page. The page index for RenderLoadedPageToBitmap is zero-based. Use the same DPI when you render the two decode ranges so the pixel positions can be read under the same conditions

procedure RenderPage(const PdfFile, BitmapFile: string);
var
  Pdf: THotPDF;
  Bitmap: TBitmap;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(PdfFile) <= 0 then
      raise Exception.Create('Cannot open the PDF');
    Bitmap := Pdf.RenderLoadedPageToBitmap(0, 144);
    if Bitmap = nil then
      raise Exception.Create('Cannot render page 1');
    try
      Bitmap.SaveToFile(BitmapFile);
    finally
      Bitmap.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

You own the returned bitmap. The routine saves it and frees it afterward; for the gray-ramp check, choose points inside each rendered image region rather than on the edge between pixels. A pixel on the boundary can be affected by rendering and scaling, making it a poor representative of either endpoint. Read the page at the same resolution when comparing the normal and reversed decode ranges, and choose corresponding interior positions in both images. For the two-pixel stream above, the left and right regions carry the two endpoint values; a longer ramp provides additional positions between them. Those positions show whether values between the endpoints follow the expected mapping. Keep the two renderings tied to their respective image dictionaries. The sample bytes can remain identical while the /Decode endpoints change their interpretation, so the dictionary is needed to explain why the left and right regions exchange their gray values

Trace the image data

Before you change the decode array, establish what the filter produced. A two-pixel, one-row image with one component and eight bits per component needs two decoded sample bytes; the five ASCII characters 00FF> yield 0 and 255 after ASCIIHex decoding. If the byte count is wrong, follow the filter and its input. If the bytes are right but the gray endpoints are reversed, read /Decode and the color-space definition. Wider images require the declared bit depth and row padding to be accounted for when calculating the expected decoded byte count. The same image data can also be placed in an Image XObject to examine whether a result depends on the inline dictionary. Preserve the actual PDF object types when doing so: an array-valued color space carries a definition that a name alone cannot replace

A two-pixel image stretched across a large rectangle will naturally look blocky; that appearance does not by itself establish a decoding problem. A longer gray ramp makes intermediate mappings and scaling easier to see. For a stream with multiple filters, keep their declared order and the parameters associated with each filter: the array is a sequence to be processed in order. Record the decoded byte count alongside width, height and bits per component. The count helps locate a discrepancy before the sample values are mapped to colors, whereas the decode endpoints explain what those values mean after filtering

A page can contain multiple inline images with different decode arrays even when both use /G. Make sure you identify the image by its position, dimensions and dictionary before examining its pixels. For an Indexed image, follow the lookup table and base-space definition; its index selects a palette entry, whereas a gray sample maps directly into the gray range. The DCT ColorTransform and DecodeParms article covers JPEG filter details, and the Indexed palette guide explains base-space lookup. The rendering routines use the HotPDF Delphi Component