Repository navigation
Coordinate Conversion Challenges
This page documents the coordinate system challenges we faced and how we solved them.
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!
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.
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!
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);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)
└─────────────────┘
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) |
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.
All intersection tests use Avalonia coordinates:
// Both in Avalonia coordinates (top-left origin)
bool intersects = redactionAreaAvalonia.Intersects(operationBBoxAvalonia);[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);
}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);// WRONG: Comparing PDF coords to Avalonia coords
if (pdfLetterY >= avaloniaSelectionTop) // These are different systems!// WRONG: Subtracting from wrong value
var avaloniaY = letterY - pageHeight; // Produces negative values!
// CORRECT: Subtract the PDF Y from page height
var avaloniaY = pageHeight - pdfY;// 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);// 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);When debugging coordinate issues:
- Identify the coordinate system at each step
- Log intermediate values with their system labeled
- Verify conversions produce expected ranges (Y should be within page height)
- Check signs (negative Y is almost always wrong)
- Test with known positions (corners, center of page)
- 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