Repository navigation
PDF Content Streams
Content streams are the heart of PDF rendering. This page documents their structure and how we parse/modify them.
- PDF 32000-1:2008, Section 7.8 "Content Streams and Resources"
- PDF Association Operators Cheat Sheet
- Our
ContentStreamParser.csandContentStreamBuilder.cs
A content stream is a sequence of operators and their operands that describe page content:
q % Save graphics state
1 0 0 1 72 720 cm % Translate to (72, 720)
BT % Begin text
/F1 12 Tf % Set font
(Hello World) Tj % Show text
ET % End text
Q % Restore graphics state
Key characteristics:
- Postfix notation: operands come before operators
- Whitespace separates tokens
- Comments start with
% - Operators are keywords (no leading
/) - Names start with
/(e.g.,/F1)
/Page <<
/Type /Page
/Contents stream % Single stream object
/Resources <<...>>
>>
/Page <<
/Type /Page
/Contents [stream1 stream2 stream3] % Array of streams
/Resources <<...>>
>>
When multiple streams exist, they are concatenated for rendering (with implicit space between).
// Access page content
var page = document.Pages[0];
var contents = page.Contents;
// Contents may have multiple elements
foreach (var content in contents.Elements)
{
// Each is a PdfReference to a stream
}| Operator | Purpose | Our Handling |
|---|---|---|
q |
Save graphics state | Push to state stack |
Q |
Restore graphics state | Pop from state stack |
cm |
Modify CTM | Multiply with current matrix |
w |
Set line width | Track for paths |
J |
Set line cap | Ignore |
j |
Set line join | Ignore |
M |
Set miter limit | Ignore |
d |
Set dash pattern | Ignore |
ri |
Set rendering intent | Ignore |
i |
Set flatness | Ignore |
gs |
Set graphics state dict | Partial support |
| Operator | Purpose |
|---|---|
CS / cs
|
Set color space (stroking/nonstroking) |
SC / sc
|
Set color |
SCN / scn
|
Set color (extended) |
G / g
|
Set gray level |
RG / rg
|
Set RGB color |
K / k
|
Set CMYK color |
See PDF-Text-Operators for detailed documentation.
| Operator | Purpose |
|---|---|
BT / ET
|
Begin/end text object |
Tf |
Set font |
Td, TD, Tm, T*
|
Position text |
Tj, TJ, ', "
|
Show text |
Tc, Tw, Tz, TL, Tr, Ts
|
Text state |
| Operator | Purpose |
|---|---|
m |
Move to |
l |
Line to |
c |
Curve to (cubic Bézier) |
v, y
|
Curve shortcuts |
h |
Close subpath |
re |
Rectangle |
S |
Stroke |
s |
Close and stroke |
f, F
|
Fill (nonzero winding) |
f* |
Fill (even-odd) |
B, B*
|
Fill and stroke |
b, b*
|
Close, fill, and stroke |
n |
End path (no-op) |
| Operator | Purpose |
|---|---|
Do |
Invoke XObject (image or form) |
| Operator | Purpose |
|---|---|
BMC |
Begin marked content |
BDC |
Begin marked content with properties |
EMC |
End marked content |
ContentStreamParser.cs parses content streams into structured PdfOperation objects:
public List<PdfOperation> Parse(byte[] contentBytes, double pageHeight)public abstract class PdfOperation
{
public Rect BoundingBox { get; }
public bool IntersectsWith(Rect area);
}
public class TextOperation : PdfOperation
{
public string Text { get; }
public string OriginalOperator { get; } // "Tj", "TJ", etc.
public byte[] OriginalBytes { get; }
}
public class PathOperation : PdfOperation
{
public List<PathSegment> Segments { get; }
public bool IsFilled { get; }
public bool IsStroked { get; }
}
public class ImageOperation : PdfOperation
{
public string XObjectName { get; }
}
public class StateOperation : PdfOperation
{
// q, Q, cm, etc.
}The parser maintains:
private PdfGraphicsState _currentState; // CTM, colors, line width
private Stack<PdfGraphicsState> _stateStack; // For q/Q
private PdfTextState _textState; // Font, size, matrixFor text, we use TextBoundsCalculator:
var bbox = _textBoundsCalculator.CalculateBounds(
text,
_textState,
_currentState,
pageHeight
);Rebuilds content streams after filtering operations:
public byte[] Build(List<PdfOperation> operations)private void SerializeOperation(PdfOperation op, StringBuilder sb)
{
switch (op)
{
case TextOperation textOp:
SerializeTextOperation(textOp, sb);
break;
case PathOperation pathOp:
SerializePathOperation(pathOp, sb);
break;
// ...
}
}private void SerializeTextOperation(TextOperation op, StringBuilder sb)
{
switch (op.OriginalOperator)
{
case "Tj":
sb.Append('(');
sb.Append(EscapeString(op.Text));
sb.Append(") Tj\n");
break;
case "TJ":
// Rebuild TJ array
SerializeTJArray(op, sb);
break;
// ...
}
}Content streams are typically compressed:
| Filter | Description |
|---|---|
/FlateDecode |
zlib compression (most common) |
/LZWDecode |
LZW compression (legacy) |
/ASCII85Decode |
ASCII encoding |
/ASCIIHexDecode |
Hex encoding |
// Get raw bytes (decompressed)
var stream = contentObject as PdfDictionary;
byte[] bytes = stream.Stream.Value; // Already decompressed// When replacing content
var newContent = new PdfDictionary();
newContent.CreateStream(newBytes);
// PdfSharpCore handles compressionContent stream order ≠ Reading order:
% Content stream order:
(W) Tj % 'W' at x=200
(H) Tj % 'H' at x=100
(o) Tj % 'o' at x=150
Visual result: "HoW" (sorted by x position)
- Optimization: PDF generators optimize for output efficiency
- Font changes: Text may be grouped by font
- Layer order: Background first, foreground last
PdfPig sorts letters by position to produce reading order:
var letters = page.Letters.OrderBy(l => l.GlyphRectangle.Top)
.ThenBy(l => l.GlyphRectangle.Left);Our CharacterMatcher does similar sorting:
.OrderBy(l => pageHeight - l.GlyphRectangle.Top) // Top to bottom
.ThenBy(l => l.GlyphRectangle.Left) // Left to rightReusable content streams, like a "subroutine":
% Page content
q
1 0 0 1 100 500 cm % Position the form
/Form1 Do % "Call" the form
Q
% Form XObject (separate stream)
/XObject <<
/Type /XObject
/Subtype /Form
/BBox [0 0 200 100]
/Resources <<...>>
stream
BT /F1 12 Tf (Form Text) Tj ET
endstream
>>
- Coordinate transformation: Form has own coordinate space
- Resource inheritance: May use page resources
- Multiple invocations: Same form used multiple times
- Nested forms: Forms can invoke other forms
Our parser doesn't currently handle Form XObjects. This is tracked in the implementation plan.
Images embedded directly in content stream:
BI % Begin inline image
/W 100
/H 100
/CS /RGB
/BPC 8
ID % Begin image data
[raw image bytes]
EI % End inline image
The image data can contain any bytes, including sequences that look like operators. You must:
- Read the image dictionary to find dimensions and color space
- Calculate exact byte length
- Skip that many bytes
We don't fully parse inline images. They pass through unchanged.
Input content stream:
q
BT
/F1 12 Tf
100 700 Td
(Hello) Tj
50 0 Td
(World) Tj
ET
Q
Parsed operations:
[0] StateOperation { Operator = "q" }
[1] StateOperation { Operator = "BT" }
[2] TextStateOperation { Font = "F1", Size = 12 }
[3] TextOperation { Text = "Hello", BBox = (100, 92, 45, 12) }
[4] TextOperation { Text = "World", BBox = (150, 92, 42, 12) }
[5] StateOperation { Operator = "ET" }
[6] StateOperation { Operator = "Q" }Note: Text state operators (Tf, Td) are absorbed into TextOperation context; only text-showing operators produce TextOperation.
| File | Purpose |
|---|---|
Services/Redaction/ContentStreamParser.cs |
Parse content streams |
Services/Redaction/ContentStreamBuilder.cs |
Rebuild content streams |
Services/Redaction/PdfOperation.cs |
Operation data models |
Services/Redaction/PdfGraphicsState.cs |
Graphics state tracking |
Services/Redaction/PdfTextState.cs |
Text state tracking |
Last updated: 2024-12-22