Skip to content

Coordinate Conversion Challenges

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

Coordinate Conversion Challenges

This page documents the coordinate system challenges we faced and how we solved them.

The Problem

Symptom

Text selection found text correctly, but redaction failed to remove it:

User selects text at image position (479, 864)
Text extraction finds: "SECRET PASSWORD" ← Works!
Redaction removes: (nothing) ← Fails!

Investigation

Debug logging revealed:

Text operation bounding box: (100.07, 387.55, 313.02x10.02)
Redaction area: (230.16, 414.72, 78.24x16.80)

Y ranges:
  Text: 387.55 to 397.57
  Redaction: 414.72 to 431.52

No overlap!

The Y coordinates were about 27 points apart - no intersection.

Root Cause

Different Conversion Methods

Two services were using different coordinate conversion methods:

PdfTextExtractionService:

var (left, bottom, right, top) = CoordinateConverter.ImageSelectionToPdfCoords(
    imageSelection, pageHeight, renderDpi);

RedactionService:

var redactionArea = CoordinateConverter.ImageSelectionToPdfPointsTopLeft(
    imageSelection, renderDpi);

These methods had subtly different implementations!

The Subtle Bug

ImageSelectionToPdfPointsTopLeft didn't account for page height:

// Old (WRONG)
return new Rect(
    imageSelection.X * scale,
    imageSelection.Y * scale,      // ← Just scaled, no flip!
    imageSelection.Width * scale,
    imageSelection.Height * scale
);

ImageSelectionToPdfCoords correctly converted Y:

// Correct
double pdfTop = pageHeight - (imageSelection.Y * scale);
double pdfBottom = pageHeight - ((imageSelection.Y + imageSelection.Height) * scale);

The Three Coordinate Systems

Understanding why this is confusing:

IMAGE PIXELS (what user sees/clicks)
┌─────────────────┐
│ (0,0)           │  Origin: top-left
│   ↓             │  Y increases: DOWN
│   Selection     │  Units: pixels (DPI-dependent)
└─────────────────┘

PDF POINTS (internal format)
┌─────────────────┐
│                 │  Origin: bottom-left
│   Text          │  Y increases: UP
│ (0,0) ↑         │  Units: points (1/72 inch)
└─────────────────┘

AVALONIA (UI framework)
┌─────────────────┐
│ (0,0)           │  Origin: top-left
│   ↓             │  Y increases: DOWN
│   Rect          │  Units: points (same as PDF)
└─────────────────┘

The Solution

Step 1: Identify All Conversion Points

We mapped every place coordinates were converted:

Location Input Output Method Used
Mouse selection Image pixels Avalonia rect ImageSelectionToPdfPointsTopLeft
Text extraction Avalonia rect PDF coords ImageSelectionToPdfCoords
Letter positions PDF coords Avalonia rect PdfRectToAvaloniaRect
Redaction area Image pixels Avalonia rect ImageSelectionToPdfPointsTopLeft
Intersection test Avalonia rect Avalonia rect (both must be same system)

Step 2: Fix the Method

Updated ImageSelectionToPdfPointsTopLeft to be consistent:

/// <summary>
/// Convert image selection to PDF points with TOP-LEFT origin (Avalonia convention).
/// This is used when we need an Avalonia Rect for intersection testing.
/// </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
    );
}

The key insight: This method returns Avalonia coordinates (top-left origin), not PDF coordinates. The name was confusing.

Step 3: Ensure Consistent Usage

All intersection tests use Avalonia coordinates:

// Both in Avalonia coordinates (top-left origin)
bool intersects = redactionAreaAvalonia.Intersects(operationBBoxAvalonia);

Step 4: Add Verification Test

[Fact]
public void CompareCoordinateConversions_MustMatch()
{
    var imageSelection = new Rect(479.50, 864.00, 163.00, 35.00);
    var renderDpi = 150;
    var pageHeightPoints = 792.0;

    // Method 1: Used by RedactionService
    var redactionArea = CoordinateConverter.ImageSelectionToPdfPointsTopLeft(
        imageSelection, renderDpi);

    // Method 2: Used by PdfTextExtractionService
    var (left, bottom, right, top) = CoordinateConverter.ImageSelectionToPdfCoords(
        imageSelection, pageHeightPoints, renderDpi);

    // Convert method 2 to Avalonia for comparison
    var textExtractionAsAvalonia = CoordinateConverter.PdfRectToAvaloniaRect(
        left, bottom, right, top, pageHeightPoints);

    // They must produce the same Avalonia rectangle
    Assert.Equal(redactionArea.X, textExtractionAsAvalonia.X, precision: 2);
    Assert.Equal(redactionArea.Y, textExtractionAsAvalonia.Y, precision: 2);
}

Debugging Tip: Log All Coordinates

When debugging coordinate issues, log extensively:

_logger.LogDebug("Image selection: ({X},{Y},{W}x{H}) at {Dpi} DPI",
    selection.X, selection.Y, selection.Width, selection.Height, dpi);

_logger.LogDebug("Converted to Avalonia: ({X},{Y},{W}x{H})",
    avaloniaRect.X, avaloniaRect.Y, avaloniaRect.Width, avaloniaRect.Height);

_logger.LogDebug("Page height: {Height} points", pageHeight);

_logger.LogDebug("Operation bbox: ({X},{Y},{W}x{H}), Y range {YMin} to {YMax}",
    op.BoundingBox.X, op.BoundingBox.Y, op.BoundingBox.Width, op.BoundingBox.Height,
    op.BoundingBox.Y, op.BoundingBox.Bottom);

Common Mistakes

Mistake 1: Mixing Coordinate Systems

// WRONG: Comparing PDF coords to Avalonia coords
if (pdfLetterY >= avaloniaSelectionTop)  // These are different systems!

Mistake 2: Wrong Y Flip Direction

// WRONG: Subtracting from wrong value
var avaloniaY = letterY - pageHeight;  // Produces negative values!

// CORRECT: Subtract the PDF Y from page height
var avaloniaY = pageHeight - pdfY;

Mistake 3: Forgetting DPI Scaling

// WRONG: Using image pixels directly as points
var pdfX = imageSelection.X;  // Pixels ≠ points!

// CORRECT: Scale by 72/DPI
var pdfX = imageSelection.X * (72.0 / dpi);

Mistake 4: Height vs Bottom Confusion

// PDF uses (left, bottom, right, top)
// Avalonia uses (x, y, width, height) where y is TOP

// WRONG: Using PDF bottom as Avalonia y
var rect = new Rect(left, bottom, width, height);

// CORRECT: Convert top to Avalonia y
var rect = new Rect(left, pageHeight - top, width, top - bottom);

The Fix Checklist

When debugging coordinate issues:

  1. Identify the coordinate system at each step
  2. Log intermediate values with their system labeled
  3. Verify conversions produce expected ranges (Y should be within page height)
  4. Check signs (negative Y is almost always wrong)
  5. Test with known positions (corners, center of page)

References

  • PDF-Coordinate-Systems - Full coordinate system documentation
  • CoordinateConversionDebugTest.cs - Test that catches this issue

Issue discovered: December 2024 Resolution: Standardized on Avalonia coordinates for all intersection testing

Clone this wiki locally