Skip to content

PDF Coordinate Systems

Marc Jones edited this page Dec 22, 2025 · 1 revision

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.

References

The Three Coordinate Systems

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)           │
└───────────────────────────────────────────────────────────────┘

Conversion Formulas

PDF to Avalonia (Same Units, Different Origin)

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
);

Image Pixels to PDF Points

PdfX = ImageX × (72 / DPI)
PdfY = PageHeight - (ImageY × (72 / DPI))

PDF Points to Image Pixels

ImageX = PdfX × (DPI / 72)
ImageY = (PageHeight - PdfY) × (DPI / 72)

CoordinateConverter Service

Our CoordinateConverter.cs centralizes all coordinate conversions:

Key Methods

/// <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
    );
}

Page Size Reference

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 Letter Coordinates

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 + Height

To 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
);

The Transformation Matrix Pipeline

PDF text position involves multiple transformations:

Glyph Space → Text Space → User Space → Device Space
     ↑             ↑            ↑
   Font         Text         CTM (cm)
   metrics      Matrix

Text Matrix (Tm)

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

Current Transformation Matrix (CTM)

Modified by cm operator, saved/restored by q/Q:

┌         ┐
│ a  b  0 │
│ c  d  0 │
│ e  f  1 │
└         ┘

Final Position

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));

Common Coordinate Bugs

Bug 1: Forgetting to Flip Y

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

Bug 2: DPI Scaling in Wrong Direction

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)

Bug 3: Rectangle Height/Y Confusion

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!

Bug 4: Using Different Conversion Methods

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.

Testing Coordinates

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);
}

Codebase Locations

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

Clone this wiki locally