Dienstag, 5. Januar 2016

Umgang mit verschiedenen Rastern und Maßstäben mit Hilfe von Promille Koordinaten

Heute habe ich Quellcode in meinem java-basierten DMS verbessert. Über die betroffenen Funktionen wird dem Nutzer eine Miniaturansicht von PDF-Seiten angezeigt. Mit der Maus kann eine Position bzw. ein Bereich auf einer Dokumentseite festgelegt werden.
Die grafische Darstellung ist mit Hilfe von SWT und Apache PDFBox umgesetzt. Grundsätzlich ist die PDF-Rendering Library austauschbar. Zwischendurch hatte ich JPod im Einsatz. Zur Darstellung wird ein bestimmter Maßstab (Zoomlevel) verwendet.

Die Auswahl des Bereichs ist erforderlich, um eine der folgenden Operationen durchzuführen:

  • Einfärben des Bereichs (schwärzen)
  • Aufkleben einer digitalen Haftnotiz
  • Digital stempeln

Bei der Umsetzung entsteht schnell ein Durcheinander, wenn man mit verschiedenen Koordinatensystemen, Maßstäben usw. arbeitet. Fest kodierte Umrechnungsfaktoren erschweren das Verständnis und die Wartung.
Im Beispiel kommt hinzu, dass die Y-Achse innerhalb des PDF invers ist, d.h. 0 ist unten und nicht oben.

Relative Koordinaten


Um die verschiedenen Koordinatensysteme voneinander zu entkoppeln bin ich dazu übergegangen, alle Angaben (x,y,width,height) relativ in Promille zu verarbeiten:

x(relativ) = x(absolut) / Seitenbreite * 1000
y(relativ) = y(absolut) / Seitenhöhe * 1000

width(relativ) = width(absolut) / Seitenbreite * 1000
height(relativ) = height(absolut) / Seitenhöhe * 1000

Promille bringt gegenüber Prozent einen entscheidenden Vorteil: Für die berechneten Werte reichen in der Regel Ganzzahlen (Integer) aus.

Die relativen Promille-Koordinaten werden im Zielkoordinatensystem wieder in absolute Koordinaten umgerechnet.

Das bringt folgende Vorteile:

  1. Keine Kodierung von Umrechnungsfaktoren zwischen den Koordinatensystemen
  2. Entkoppelung der Koordinatensysteme
  3. In der Darstellung verwendete Maßstäbe (Zoom) sind nicht relevant, da die relativen Verhältnisse immer gleich bleiben.
  4. Zur Transformation in relative bzw. absolute Koordinaten kann ein einfaches API verwendet werden (dazu später mehr)

Transform API


Zur Übergabe von ganzzahligen Promille Koordinaten habe ich zunächst eine ganze einfache Klasse erstellt:

/**
 * Ganzzahlige Koordinaten
 * 
 * @author Peter Pinnau (peter@pinnau.biz)
 *
 */
public class Position {
 
  public int x;
 
  public int y;
 
  public int width;
 
  public int height;

}

Für die Transformation und Berechnung der absoluten Koordinaten in den einzelnen Koordinatensystemen ist bei Apache PDFBox eine höhere Genauigkeit erforderlich. Dafür habe ich die einfache Klasse FloatPosition erstellt. Diese wird aber nur innerhalb eines Koordinatensystems verwendet.

/**
 * Float Koordinaten
 * 
 * @author Peter Pinnau (peter@pinnau.biz)
 *
 */
public class FloatPosition {

 public float x;
 
 public float y;
 
 public float width;
 
 public float height;
  
}

Zur Transformation von ganzzahligen Koordinaten (Position) in Fließkommakoordinaten (FloatPosition) und zur Berechnung von relativen, absoluten und inversen Koordinaten habe ich eine statische Klasse Transform erstellt:


1:  /**  
2:   * Statische Klasse zum Druchführen von Koordinaten- und Größentransformationen  
3:   *   
4:   * @author Peter Pinnau (peter@pinnau.biz)  
5:   *  
6:   */  
7:  public class Transform {  
8:    
9:     /**  
10:      * Erzeugt eine FloatPosition aus einer Position  
11:      * @param p  
12:      * @return  
13:      */  
14:     public static FloatPosition fromPosition(Position p) {  
15:        FloatPosition pos = new FloatPosition();  
16:          
17:        pos.x = p.x;  
18:        pos.y = p.y;  
19:          
20:        pos.height = p.height;  
21:        pos.width = p.width;  
22:          
23:        return pos;  
24:     }  
25:       
26:     /**  
27:      * Erzeugt eine FloatPosition aus x,y,width und height  
28:      *   
29:      * @param x  
30:      * @param y  
31:      * @param width  
32:      * @param height  
33:      * @return  
34:      */  
35:     public static FloatPosition create(float x, float y, float width, float height) {  
36:        FloatPosition pos = new FloatPosition();  
37:          
38:        pos.x = x;  
39:        pos.y = y;  
40:        pos.width = width;  
41:        pos.height = height;  
42:          
43:        return pos;  
44:     }  
45:       
46:     /**  
47:      * Berechnet eine absolute FloatPosition aus Promillewerten und einem Bezugsbereich  
48:      *   
49:      * @param promille  
50:      * @param area  
51:      * @return  
52:      */  
53:     public static FloatPosition fromPromille(FloatPosition promille, FloatPosition area) {  
54:        FloatPosition p = new FloatPosition();  
55:          
56:        float widthFactor = area.width / 1000;  
57:        float heightFactor = area.height / 1000;  
58:          
59:        p.x = promille.x * widthFactor;  
60:        p.y = promille.y * heightFactor;  
61:          
62:        p.width = promille.width * widthFactor;  
63:        p.height = promille.height * heightFactor;  
64:          
65:        return p;  
66:     }  
67:       
68:     /**  
69:      * Berechnet Promillewerte aus absoluten Werten und einem Bezugsbereich  
70:      *   
71:      * @param pos  
72:      * @param area  
73:      * @return  
74:      */  
75:     public static FloatPosition toPromille(FloatPosition pos, FloatPosition area) {  
76:        FloatPosition p = new FloatPosition();  
77:                
78:        p.x = pos.x * 1000 / area.width;        
79:        p.y = pos.y * 1000 / area.height;  
80:          
81:        p.width = pos.width * 1000 / area.width;  
82:        p.height = pos.height * 1000 / area.height;  
83:          
84:        return p;              
85:     }  
86:       
87:     /**  
88:      * Invertiert die Y-Achse  
89:      *   
90:      * y(neu) = area.height - y  
91:      *   
92:      * @param pos  
93:      * @param area  
94:      */  
95:     public static void invertY(FloatPosition pos, FloatPosition area) {        
96:        pos.y = area.height - pos.y;  
97:     }  
98:       
99:  }  
100:    


Alle Methoden erhalten als Übergabeparameter eine Position-Instanz (ganzzahlige, relative Promille-Koordinaten).
Die absoluten Koordinaten im benötigten Koordinatensystem werden dann mit Hilfe der statischen Methoden aus Transform berechnet:



// Als Parameter übergebene relative Koordinaten
Position position ...

// Seite des PDF
PDPage page = pdDocument.getPage(0);
    
// CROPbox (kompletter Seitenbereich)  
PDRectangle pageSize = page.getCropBox();

// FloatPosition für komplette Seite erstellen (Bezugsbereich)
FloatPosition area = Transform.create(0, 0,
    pageSize.width, pageSize.height);   

// Transform Promille Position to absolute FloatPosition
FloatPosition transformed = Transform.fromPromille(
    Transform.fromPosition(position), area);

// Y Achse invertieren (PDF Koordinaten)
Transform.invertY(transformed, area);

// => transformed enthält jetzt die absoluten Koordinaten im Zielsystem


Nachteil


Ich möchte nicht verschweigen, dass die Performance theoretisch schlechter wird. Der Grund dafür ist, dass keine direkte Umrechnung zwischen den Systemen erfolgt (fester Faktor).
Es müssen zunächst relative Koordinaten aus dem Quellsystem ermittelt werden und diese dann wieder in absolute Koordinaten des Zielsystems umgerechnet werden.

Insgesamt ergibt sich logischerweise wieder der feste Faktor, jedoch wird dieser nicht kodiert und ergibt sich immer automatisch anhand der verwendeten Koordinatensysteme.

Diesen geringen Performancenachteil kann ich in meinem System in Kauf nehmen, da keine Massenoperationen durchgeführt werden.
Die Lesbarkeit des Quelltextes hat sich erheblich verbessert und vor Allem sind die unterschiedlichen Koordinatensystem entkoppelt.

Keine Kommentare:

Kommentar veröffentlichen