Repository navigation
PDF Coordinate Systems
Understanding coordinate systems is one of the most challenging aspects of PDF development. This page documents the coordinate systems we work with and how to convert between them.
- PDF 32000-1:2008, Section 8.3 "Coordinate Systems"
- PDF Association - Understanding PDF Coordinates
- Our
CoordinateConverter.csservice
Our application deals with three distinct coordinate systems:
┌───────────────────────────────────────────────────────────────┐
│ PDF COORDINATE SYSTEM │
│ │
│ Origin: Bottom-Left │
│ Y-axis: Points UP │
│ Units: Points (1/72 inch) │
│ │
│ (0, 792) ──────────────────────────── (612, 792) │
│ │ │ │
│ │ Text at (100, 700) appears │ │
│ │ near the TOP of the page │ │
│ │ │ │
│ (0, 0) ───────────────────────────── (612, 0) │
│ ↑ Origin │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ AVALONIA COORDINATE SYSTEM │
│ │
│ Origin: Top-Left │
│ Y-axis: Points DOWN │
│ Units: Points (1/72 inch) - same as PDF │
│ │
│ (0, 0) ───────────────────────────── (612, 0) │
│ ↓ Origin │ │
│ │ Text at (100, 92) appears │ │
│ │ near the TOP of the page │ │
│ │ │ │
│ (0, 792) ──────────────────────────── (612, 792) │
└───────────────────────────────────────────────────────────────┘
┌───────────────────────────────────────────────────────────────┐
│ IMAGE COORDINATE SYSTEM │
│ │
│ Origin: Top-Left │
│ Y-axis: Points DOWN │
│ Units: Pixels (depends on DPI) │
│ │
│ At 150 DPI, a letter-size page (612x792 points) becomes: │
│ 612 × (150/72) = 1275 pixels wide │
│ 792 × (150/72) = 1650 pixels tall │
│ │
│ (0, 0) ───────────────────────────── (1275, 0) │
│ ↓ Origin │ │
│ │ │ │
│ (0, 1650) ─────────────────────────── (1275, 1650) │
└───────────────────────────────────────────────────────────────┘
AvaloniaY = PageHeight - PdfY
AvaloniaX = PdfX (unchanged)
For a rectangle with (left, bottom, right, top) in PDF:
var avaloniaRect = new Rect(
left, // X unchanged
pageHeight - top, // Y flipped (top becomes Y position)
right - left, // Width unchanged
top - bottom // Height unchanged
);PdfX = ImageX × (72 / DPI)
PdfY = PageHeight - (ImageY × (72 / DPI))
ImageX = PdfX × (DPI / 72)
ImageY = (PageHeight - PdfY) × (DPI / 72)
Our CoordinateConverter.cs centralizes all coordinate conversions:
/// <summary>
/// Convert image selection (pixels) to PDF points with top-left origin.
/// Used by RedactionService.
/// </summary>
public static Rect ImageSelectionToPdfPointsTopLeft(Rect imageSelection, int dpi)
{
double scale = 72.0 / dpi;
return new Rect(
imageSelection.X * scale,
imageSelection.Y * scale,
imageSelection.Width * scale,
imageSelection.Height * scale
);
}
/// <summary>
/// Convert image selection to PDF coordinates (bottom-left origin).
/// Returns (left, bottom, right, top) tuple.
/// Used by PdfTextExtractionService.
/// </summary>
public static (double left, double bottom, double right, double top)
ImageSelectionToPdfCoords(Rect imageSelection, double pageHeight, int dpi)
{
double scale = 72.0 / dpi;
double left = imageSelection.X * scale;
double right = (imageSelection.X + imageSelection.Width) * scale;
double imageTop = imageSelection.Y * scale;
double imageBottom = (imageSelection.Y + imageSelection.Height) * scale;
// Convert from top-left origin to bottom-left origin
double pdfTop = pageHeight - imageTop;
double pdfBottom = pageHeight - imageBottom;
return (left, pdfBottom, right, pdfTop);
}
/// <summary>
/// Convert PDF rectangle (bottom-left origin) to Avalonia rect (top-left origin).
/// </summary>
public static Rect PdfRectToAvaloniaRect(
double left, double bottom, double right, double top, double pageHeight)
{
return new Rect(
left,
pageHeight - top,
right - left,
top - bottom
);
}Standard page sizes in PDF points (72 points = 1 inch):
| Size | Width (pts) | Height (pts) | Width (in) | Height (in) |
|---|---|---|---|---|
| Letter | 612 | 792 | 8.5 | 11 |
| Legal | 612 | 1008 | 8.5 | 14 |
| A4 | 595 | 842 | 8.27 | 11.69 |
| A3 | 842 | 1191 | 11.69 | 16.54 |
PdfPig returns letter positions in PDF coordinates (bottom-left origin):
var letter = page.Letters.First();
// GlyphRectangle is in PDF coordinates
letter.GlyphRectangle.Left // X position
letter.GlyphRectangle.Bottom // PDF Y (larger = higher on page)
letter.GlyphRectangle.Right // X + Width
letter.GlyphRectangle.Top // PDF Y + HeightTo convert to Avalonia for display:
var avaloniaY = pageHeight - letter.GlyphRectangle.Top;
var avaloniaRect = new Rect(
letter.GlyphRectangle.Left,
avaloniaY,
letter.GlyphRectangle.Width,
letter.GlyphRectangle.Height
);PDF text position involves multiple transformations:
Glyph Space → Text Space → User Space → Device Space
↑ ↑ ↑
Font Text CTM (cm)
metrics Matrix
Set by Tm operator or modified by Td, TD, T*:
┌ ┐
│ a b 0 │
│ c d 0 │ Position = (e, f)
│ e f 1 │ Scaling = (a, d)
└ ┘ Rotation = combined a, b, c, d
Modified by cm operator, saved/restored by q/Q:
┌ ┐
│ a b 0 │
│ c d 0 │
│ e f 1 │
└ ┘
FinalPosition = TextMatrix × CTM × point(0, 0)
In our code (TextBoundsCalculator.cs):
var transformedMatrix = textState.TextMatrix.Multiply(graphicsState.TransformationMatrix);
var position = transformedMatrix.Transform(new Point(0, 0));Symptom: Selections appear inverted - selecting top of page affects bottom.
Cause: Using PDF Y directly in Avalonia without conversion.
Fix: Always convert: avaloniaY = pageHeight - pdfY
Symptom: Selections at 150 DPI are offset from 72 DPI.
Cause: Multiplying instead of dividing (or vice versa).
Fix:
- Points to Pixels:
× (DPI / 72) - Pixels to Points:
× (72 / DPI)
Symptom: Rectangles have wrong height or position.
Cause: PDF uses (left, bottom, right, top) but Avalonia uses (x, y, width, height).
Fix:
// PDF to Avalonia
var y = pageHeight - top; // Not pageHeight - bottom!
var height = top - bottom; // Not bottom - top!Symptom: Text extraction finds text but redaction doesn't work.
Cause: PdfTextExtractionService and RedactionService using different conversion formulas.
Fix: Use CoordinateConverter consistently everywhere. See CoordinateConversionDebugTest.cs.
Our test suite includes specific coordinate tests:
// CoordinateConversionDebugTest.cs
[Fact]
public void CompareCoordinateConversions_FromUserReport()
{
var imageSelection = new Rect(479.50, 864.00, 163.00, 35.00);
var renderDpi = 150;
var pageHeightPoints = 792.0;
var redactionArea = CoordinateConverter.ImageSelectionToPdfPointsTopLeft(imageSelection, renderDpi);
var (left, bottom, right, top) = CoordinateConverter.ImageSelectionToPdfCoords(
imageSelection, pageHeightPoints, renderDpi);
// Both should produce the same result when normalized
Assert.Equal(redactionArea.X, left, precision: 2);
}| File | Purpose |
|---|---|
Services/CoordinateConverter.cs |
Central conversion methods |
Services/Redaction/TextBoundsCalculator.cs |
Text position calculations |
Tests/Integration/CoordinateConversionDebugTest.cs |
Coordinate testing |
Tests/Unit/CoordinateConverterTests.cs |
Unit tests for converter |
Last updated: 2024-12-22