Header einer FlatGeobuf-Datei - Reverse engineering erforderlich?

BAGZZlash

Moderator
Teammitglied
Ein relativ junger Stern am Himmel der Vektordatenformate im GIS-Bereich ist die FlatGeobuf-Datei. Sie basiert auf FlatBuffers und hält Vektor-Geodaten in einem Binärformat vor (Dateiendung .fgb). Enthalten sind Geometrien (genannt "Features", also z.B. Polygone, beschrieben durch eine Reihe von Punkten) und Attribute (Beschreibungen zu den Features, im Grunde eine Tabelle mit so vielen Zeilen, wie Features enthalten sind, und mehreren Spalten für die Variablen).

Dazu gibt es noch zwei Metadaten: Erstens, der Name des Datensatzes (ein einfacher String) und das Geokoordinatensystem ("CRS", coordinate reference system) im WKT-Format, also ebenfalls als String. Zum Handling von FlatGeobuf-Daten nutze ich in C# das Package "FlatGeobuf", das seinerseits auf "NetTopologySuite" aufsetzt. Lädt man damit eine .fgb-Datei und deserialisiert sie, erhält man ein .NET-Listobjekt (so eine Collection), mit der man ziemlich komfortabel auf die Geometrien und Attribute zugreifen kann.

Dabei scheint es allerdings so, als gingen die Metainformationen Name und CRS verloren. Nirgendwo in der Datenstruktur finde ich diese Informationen. Also dachte ich, ich kratze mir die Strings einfach selbst aus dem Header der Datei, wo sie einfach so drin stehen. Das klappt leidlich.

Mein Problem ist, dass ich keine gute Dokumentation zum Header finde. Hier steht bloß was zu den Magic Bytes sowie die Information, dass danach der Header kommt. Klickt man dort auf den Link zum Header, findet man das hier, was mir nicht hilft und meiner Ansicht nach sogar falsch ist, weil demnach der Datensatzname im Header vor der Angabe der Anzahl der in der Datei enthaltenen Features kommen soll, tatsächlich steht der Name aber danach in der Datei. Die Anzahl und Struktur der dazwischenliegenden Informationen stimmt auch nicht.

Ich würde gern den Header etwas besser verstehen. Leider gibt's offenbar nicht einfach Offsets, die mir die Startpositionen der nullterminierten Strings verraten. Wie komme ich hier weiter? Hat jemand von Euch ein besseres Reverse-Engineering-Auge als ich?

Hier mal der Header einer gültigen .fgb-Datei. Man sieht zuerst die acht Bytes für die Magic Number, dann kommt an Offset 0x98 der Name, bei Offset 0xCC beginnt dann der WKT-String für das CRS. Bei 0x0234 stehen die Spaltenüberschriften der Attributtabelle, danach kommen die eigentlichen Binärdaten. Die Datei enthält übrigens 3221 Features (= 0x0C95), das steht bei Offset 0x48, wie man sieht in Little Endian. Soweit ich weiß, sind alle Werte als 32-Bit-Typen zu verstehen (also immer vier Bytes).

Hier der Hexdump als Bild:
Hexdump.png


Und hier die rohen Daten:
Code:
Offset(h) 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F

00000000  66 67 62 03 66 67 62 00 DC 02 00 00 24 00 00 00  fgb.fgb.Ü...$...
00000010  00 00 00 00 00 00 1A 00 20 00 08 00 0C 00 07 00  ........ .......
00000020  00 00 00 00 00 00 00 00 10 00 18 00 00 00 14 00  ................
00000030  1A 00 00 00 00 00 00 06 5C 00 00 00 30 00 00 00  ........\...0...
00000040  10 00 00 00 70 00 00 00 95 0C 00 00 00 00 00 00  ....p...•.......
00000050  06 00 00 00 78 02 00 00 50 02 00 00 34 02 00 00  ....x...P...4...
00000060  18 02 00 00 FC 01 00 00 E0 01 00 00 04 00 00 00  ....ü...à.......
00000070  90 2C 60 02 B7 64 66 C0 5D 13 D2 1A 83 E2 31 40  .,`.·dfÀ].Ò.ƒâ1@
00000080  59 DD EA 39 E9 78 66 40 7E F5 84 5A 90 D6 51 40  YÝê9éxf@~õ„Z.ÖQ@
00000090  00 00 00 00 0A 00 00 00 55 53 63 6F 75 6E 74 69  ........UScounti
000000A0  65 73 00 00 00 00 0E 00 14 00 04 00 08 00 0C 00  es..............
000000B0  00 00 10 00 0E 00 00 00 84 01 00 00 AD 10 00 00  ........„.......
000000C0  70 01 00 00 04 00 00 00 62 01 00 00 47 45 4F 47  p.......b...GEOG
000000D0  43 52 53 5B 22 4E 41 44 38 33 22 2C 44 41 54 55  CRS["NAD83",DATU
000000E0  4D 5B 22 4E 6F 72 74 68 20 41 6D 65 72 69 63 61  M["North America
000000F0  6E 20 44 61 74 75 6D 20 31 39 38 33 22 2C 45 4C  n Datum 1983",EL
00000100  4C 49 50 53 4F 49 44 5B 22 47 52 53 20 31 39 38  LIPSOID["GRS 198
00000110  30 22 2C 36 33 37 38 31 33 37 2C 32 39 38 2E 32  0",6378137,298.2
00000120  35 37 32 32 32 31 30 31 2C 4C 45 4E 47 54 48 55  57222101,LENGTHU
00000130  4E 49 54 5B 22 6D 65 74 72 65 22 2C 31 5D 5D 5D  NIT["metre",1]]]
00000140  2C 50 52 49 4D 45 4D 5B 22 47 72 65 65 6E 77 69  ,PRIMEM["Greenwi
00000150  63 68 22 2C 30 2C 41 4E 47 4C 45 55 4E 49 54 5B  ch",0,ANGLEUNIT[
00000160  22 64 65 67 72 65 65 22 2C 30 2E 30 31 37 34 35  "degree",0.01745
00000170  33 32 39 32 35 31 39 39 34 33 33 5D 5D 2C 43 53  32925199433]],CS
00000180  5B 65 6C 6C 69 70 73 6F 69 64 61 6C 2C 32 5D 2C  [ellipsoidal,2],
00000190  41 58 49 53 5B 22 6C 61 74 69 74 75 64 65 22 2C  AXIS["latitude",
000001A0  6E 6F 72 74 68 2C 4F 52 44 45 52 5B 31 5D 2C 41  north,ORDER[1],A
000001B0  4E 47 4C 45 55 4E 49 54 5B 22 64 65 67 72 65 65  NGLEUNIT["degree
000001C0  22 2C 30 2E 30 31 37 34 35 33 32 39 32 35 31 39  ",0.017453292519
000001D0  39 34 33 33 5D 5D 2C 41 58 49 53 5B 22 6C 6F 6E  9433]],AXIS["lon
000001E0  67 69 74 75 64 65 22 2C 65 61 73 74 2C 4F 52 44  gitude",east,ORD
000001F0  45 52 5B 32 5D 2C 41 4E 47 4C 45 55 4E 49 54 5B  ER[2],ANGLEUNIT[
00000200  22 64 65 67 72 65 65 22 2C 30 2E 30 31 37 34 35  "degree",0.01745
00000210  33 32 39 32 35 31 39 39 34 33 33 5D 5D 2C 49 44  32925199433]],ID
00000220  5B 22 45 50 53 47 22 2C 34 32 36 39 5D 5D 00 00  ["EPSG",4269]]..
00000230  05 00 00 00 4E 41 44 38 33 00 00 00 04 00 00 00  ....NAD83.......
00000240  45 50 53 47 00 00 00 00 84 FF FF FF 00 00 00 0B  EPSG....„ÿÿÿ....
00000250  04 00 00 00 04 00 00 00 4C 53 41 44 00 00 00 00  ........LSAD....
00000260  9C FF FF FF 00 00 00 0B 04 00 00 00 04 00 00 00  œÿÿÿ............
00000270  4E 41 4D 45 00 00 00 00 B4 FF FF FF 00 00 00 0B  NAME....´ÿÿÿ....
00000280  04 00 00 00 05 00 00 00 53 54 41 54 45 00 00 00  ........STATE...
00000290  CC FF FF FF 00 00 00 0B 04 00 00 00 04 00 00 00  Ìÿÿÿ............
000002A0  46 49 50 53 00 00 00 00 E4 FF FF FF 00 00 00 0B  FIPS....äÿÿÿ....
000002B0  04 00 00 00 0A 00 00 00 43 4F 55 4E 54 59 5F 46  ........COUNTY_F
000002C0  49 50 00 00 08 00 0C 00 08 00 07 00 08 00 00 00  IP..............
000002D0  00 00 00 0B 04 00 00 00 0A 00 00 00 53 54 41 54  ............STAT
000002E0  45 5F 46 49 50 53 00 00 90 2C 60 02 B7 64 66 C0  E_FIPS...,`.·dfÀ
000002F0  5D 13 D2 1A 83 E2 31 40 59 DD EA 39 E9 78 66 40  ].Ò.ƒâ1@YÝê9éxf@
00000300  7E F5 84 5A 90 D6 51 40 01 00 00 00 00 00 00 00  ~õ„Z.ÖQ@........
00000310  90 2C 60 02 B7 64 66 C0 F0 16 48 50 FC 0A 46 40  .,`.·dfÀð.HPü.F@
00000320  59 DD EA 39 E9 78 66 40 7E F5 84 5A 90 D6 51 40  YÝê9éxf@~õ„Z.ÖQ@
00000330  0E 00 00 00 00 00 00 00 73 FA 1D BE 16 27 5F C0  ........sú.¾.'_À
 

Mat

Aktives Mitglied
Vielleicht habe ich das Problem falsch verstanden, aber warum benutzt du nicht den mitgelieferten Deserialisierer aus dem Paket?

Hab da jetzt auf die Schnelle nicht die beste Vorgehensweise gefunden, aber den Header kannst du zB mit dem mitgelieferten Helper auslesen:

C#:
using(FileStream stream = new FileStream("../../UScounties.fgb", FileMode.Open)) { FlatGeobuf.Header dings = FlatGeobuf.Helpers.ReadHeader(stream); }


NameWertTyp
dings.Name"UScounties"string


NameWertTyp
dings.Crs{FlatGeobuf.Crs}FlatGeobuf.Crs?


NameWertTyp
dings.Crs.Wkt"GEOGCRS[\"NAD83\",DATUM[\"North American Datum 1983\",ELLIPSOID[\"GRS 1980\",6378137,298.257222101,LENGTHUNIT[\"metre\",1]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433]],CS[ellipsoidal,2],AXIS[\"latitude\",north,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433]],AXIS[\"longitude\",east,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433]],ID[\"EPSG\",4269]]"string
 

BAGZZlash

Moderator
Teammitglied
Du hast das Problem genau richtig verstanden, vielen Dank! 😘😀

Warum ich das nicht längst so mache? Weil ich das nicht wusste. Darf ich ernsthaft fragen, wie Du das gefunden hast? Ich hab' mir stundenlang die Finger blutig gegooglet.
 

Mat

Aktives Mitglied
Darf ich ernsthaft fragen, wie Du das gefunden hast? Ich hab' mir stundenlang die Finger blutig gegooglet.
Ich hatte erst versucht, eine Doku zu finden, aber schnell gemerkt, dass das nix wird. Deswegen hab ich einfach ganz blöd in der Repo rumgeguckt (also im NET-Teil). Erst hatte ich mir die Tests und den Headergenerator angesehen, aber dann dachte ich mir, dass die da ja etwas eigenes zum Parsen haben müssen. Helpers sah vielversprechend aus.

Also nur Glück und bestimmte Grundannahmen. :)
 

BAGZZlash

Moderator
Teammitglied
Hammer. Hast Du vielleicht auch eine Idee, wie ich die Metainformationen via Header wieder beim Serialisieren mit einer FeatureCollection verheiratet bekomme, sodass wieder ein Byte-Array entsteht, das die Headerinformationen in sich trägt? Das scheinen die Serialisierungsmethoden nämlich irgendwie nicht vorzusehen...
 

Mat

Aktives Mitglied
Hmm. Was man bräuchte ist ja ein geparstes Objekt mit einer FeatureCollection, die man dann bearbeiten kann. Im Moment setzt du das Objekt noch händisch zusammen, oder? Also deswegen auch der Header einzeln usw.

Wenn ich das richtig gesehen hab, kann man so ein Objekt dann wieder in einen Byte-Array umwandeln und in eine Datei schreiben

Edit: https://github.com/flatgeobuf/flatg...obuf/NTS/FeatureCollectionConversions.cs#L116
Vielleicht kann man das resultierende Objekt bearbeiten. In der Funktion werden ja die Features ausgelesen. Anschließend könnte man die Einzelteile wieder an Serialize übergeben.
 
Zuletzt bearbeitet:

BAGZZlash

Moderator
Teammitglied
Ja, genau. Das hab' ich schon. Die Tatsache, dass man die Datei einfach als Binary in den Speicher schaufeln kann und anschließend mit einer Zeile Code deserialisiert (oder sogar direkt beim Einlesen mittels Envelope nur einen Teil der Features ausliest mittels der zweiten Überladung von .Deserialize(), das ist der Code, den Du verlinkt hast), ist einer der Hauptgründe, warum wir FlatGeobuf verwenden:

C#:
IntVecData = System.IO.File.ReadAllBytes(FlatGeobufFileName);
NetTopologySuite.Features.FeatureCollection MyFC = FlatGeobuf.NTS.FeatureCollectionConversions.Deserialize(IntVecData);

Deserialize erzeugt ein List-Objekt ("FeatureCollection"). Wie man aber in dem von Dir verlinkten Code sieht, wird bei der dortigen .Deserialize()-Methode der Header zwar ausgelesen, aber die resultierenden Ergebnisse nur intern verwendet und sind nach Verlassen der Methode verloren. Deswegen muss der Header nachher nochmal neu ausgelesen werden, was dank Deiner Hilfe jetzt auch klappt.
Die Metainformationen finden also keinen Platz in der List, wo auch? Der umgekehrte Weg, nämlich aus der FeatureCollection wieder ein Byte-Array zu machen mittels .Serialize() (siehe hier), packt auch, wie man sieht, den Header nicht wieder rein. Speichert man so eine Datei, hat man zwar eine gültige .fgb-Datei, die alle Geometrien und Attribute enthält, aber nicht die Metainformationen. Es wird lediglich ein generischer Header erzeugt, der zwar die Magic Bytes usw. enthält, aber insb. kein CRS.

Hm, ob ich mir die Methode klaue und für meine Zwecke modifiziere?
 
Zuletzt bearbeitet:

Mat

Aktives Mitglied
Jo, erben und die Implementierung ändern wäre dann auch mein Gedanke.

Alternativ in ein anderes Format zwischenspeichern und dann erst in FGB schreiben? Wäre unschön, aber vielleicht leichter mit den JSON-Objekten.

Vielleicht lenke ich dich hier grad in eine völlig falsche Richtung und das geht eigentlich direkt mit der Library. An deiner Stelle würde ich einfach mal eine Diskussion in der Repo aufmachen:
 
Oben Unten