Skip to content

Commit f921a0a

Browse files
authored
Metadata compression (#218)
* add metadata serialize/deserialize helpers * support uncompressed archives in edit * better minimal example
1 parent 1a8f64f commit f921a0a

File tree

9 files changed

+149
-103
lines changed

9 files changed

+149
-103
lines changed

examples/minimal.go

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,68 @@ import (
66
"os"
77
)
88

9+
// A program that creates the smallest meaningful PMTiles archive,
10+
// consisting of a purple square at tile 0,0,0 (the entire earth).
11+
// Uses only two library functions, SerializeHeader and SerializeEntries.
912
func main() {
10-
PINK := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/D/PwAHAwL/qGeMxAAAAABJRU5ErkJggg=="
11-
CYAN := "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+P//PwAGBAL/VJiKjgAAAABJRU5ErkJggg=="
13+
outfile, _ := os.Create("minimal.pmtiles")
14+
defer outfile.Close()
1215

13-
pink, _ := base64.StdEncoding.DecodeString(PINK)
14-
cyan, _ := base64.StdEncoding.DecodeString(CYAN)
16+
// A solid purple PNG with 50% opacity.
17+
png, _ := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mM0NLxTDwADmAG/Djok1gAAAABJRU5ErkJggg==")
1518

16-
outfile, _ := os.Create("output.pmtiles")
19+
// Create an entry with TileID=0 (z=0, x=0, y=0), Offset=0, Length=len(png), and RunLength=1.
20+
entries := []pmtiles.EntryV3{{0, 0, uint32(len(png)), 1}}
1721

18-
var h pmtiles.HeaderV3
19-
var entries []pmtiles.EntryV3
20-
entries = append(entries, pmtiles.EntryV3{1, uint64(len(cyan)), uint32(len(pink)), 1})
21-
entries = append(entries, pmtiles.EntryV3{2, 0, uint32(len(cyan)), 1})
22+
// Create the bytes of the root directory.
2223
dir := pmtiles.SerializeEntries(entries, pmtiles.NoCompression)
24+
25+
// the JSON metadata is the empty object.
26+
metadata := "{}"
27+
28+
// here we set the data of the header (the first 127 bytes)
29+
var h pmtiles.HeaderV3
2330
h.SpecVersion = 3
24-
h.RootOffset = 127
31+
32+
// the root directory follows the header.
33+
h.RootOffset = pmtiles.HeaderV3LenBytes
2534
h.RootLength = uint64(len(dir))
26-
h.MetadataOffset = uint64(127 + len(dir))
27-
h.MetadataLength = 2
35+
36+
// the JSON metadata follows the root directory.
37+
h.MetadataOffset = h.RootOffset + uint64(len(dir))
38+
h.MetadataLength = uint64(len(metadata))
39+
40+
// there are no leaves, but set the offset to the right place and length=0.
2841
h.LeafDirectoryOffset = h.MetadataOffset + h.MetadataLength
2942
h.LeafDirectoryLength = 0
43+
44+
// the tile data follows the JSON metadata.
3045
h.TileDataOffset = h.LeafDirectoryOffset
31-
h.TileDataLength = uint64(len(pink) + len(cyan))
32-
h.AddressedTilesCount = 2
33-
h.TileEntriesCount = 2
34-
h.TileContentsCount = 2
35-
h.InternalCompression = 1
36-
h.TileCompression = 1
37-
h.TileType = 2
38-
h.MinZoom = 1
39-
h.CenterZoom = 1
40-
h.MaxZoom = 1
46+
h.TileDataLength = uint64(len(png))
47+
48+
// set statistics
49+
h.AddressedTilesCount = 1
50+
h.TileEntriesCount = 1
51+
h.TileContentsCount = 1
52+
53+
// since we store a PNG, the tile data should not be interpreted as compressed.
54+
h.InternalCompression = pmtiles.NoCompression
55+
h.TileCompression = pmtiles.NoCompression
56+
h.TileType = pmtiles.Png
57+
58+
// set the zoom and geographic bounds.
59+
h.MinZoom = 0
60+
h.CenterZoom = 0
61+
h.MaxZoom = 0
4162
h.MinLatE7 = -85 * 10000000
4263
h.MaxLatE7 = 85 * 10000000
4364
h.MinLonE7 = -180 * 10000000
4465
h.MaxLonE7 = 180 * 10000000
4566

4667
outfile.Write(pmtiles.SerializeHeader(h))
68+
69+
// write the directory, JSON metadata and tile data.
4770
outfile.Write(dir)
48-
outfile.Write([]byte("{}"))
49-
outfile.Write(cyan)
50-
outfile.Write(pink)
71+
outfile.Write([]byte(metadata))
72+
outfile.Write(png)
5173
}

pmtiles/cluster.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package pmtiles
22

33
import (
44
"bytes"
5-
"compress/gzip"
6-
"encoding/json"
75
"fmt"
86
"github.com/schollz/progressbar/v3"
97
"io"
@@ -28,15 +26,8 @@ func Cluster(logger *log.Logger, InputPMTiles string, deduplicate bool) error {
2826
fmt.Println("total directory size", header.RootLength+header.LeafDirectoryLength)
2927

3028
metadataReader := io.NewSectionReader(file, int64(header.MetadataOffset), int64(header.MetadataLength))
31-
var metadataBytes []byte
32-
if header.InternalCompression == Gzip {
33-
r, _ := gzip.NewReader(metadataReader)
34-
metadataBytes, _ = io.ReadAll(r)
35-
} else {
36-
metadataBytes, _ = io.ReadAll(metadataReader)
37-
}
38-
var parsedMetadata map[string]interface{}
39-
_ = json.Unmarshal(metadataBytes, &parsedMetadata)
29+
30+
var metadata, err = DeserializeMetadata(metadataReader, header.InternalCompression)
4031

4132
var CollectEntries func(uint64, uint64, func(EntryV3))
4233

@@ -67,7 +58,7 @@ func Cluster(logger *log.Logger, InputPMTiles string, deduplicate bool) error {
6758
})
6859

6960
header.Clustered = true
70-
newHeader, err := finalize(logger, resolver, header, tmpfile, "output.pmtiles", parsedMetadata)
61+
newHeader, err := finalize(logger, resolver, header, tmpfile, "output.pmtiles", metadata)
7162
if err != nil {
7263
return err
7364
}

pmtiles/convert.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -375,17 +375,10 @@ func finalize(logger *log.Logger, resolve *resolver, header HeaderV3, tmpfile *o
375375
logger.Printf("Average bytes per addressed tile: %.2f\n", float64(len(rootBytes))/float64(resolve.AddressedTiles))
376376
}
377377

378-
var metadataBytes []byte
379-
{
380-
metadataBytesUncompressed, err := json.Marshal(jsonMetadata)
381-
if err != nil {
382-
return header, fmt.Errorf("Failed to marshal metadata, %w", err)
383-
}
384-
var b bytes.Buffer
385-
w, _ := gzip.NewWriterLevel(&b, gzip.BestCompression)
386-
w.Write(metadataBytesUncompressed)
387-
w.Close()
388-
metadataBytes = b.Bytes()
378+
metadataBytes, err := SerializeMetadata(jsonMetadata, Gzip)
379+
380+
if err != nil {
381+
return header, fmt.Errorf("Failed to marshal metadata, %w", err)
389382
}
390383

391384
setZoomCenterDefaults(&header, resolve.Entries)

pmtiles/directory.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"compress/gzip"
77
"encoding/binary"
88
"encoding/json"
9+
"errors"
910
"fmt"
1011
"io"
1112
)
@@ -198,6 +199,66 @@ type nopWriteCloser struct {
198199

199200
func (w *nopWriteCloser) Close() error { return nil }
200201

202+
func SerializeMetadata(metadata map[string]interface{}, compression Compression) ([]byte, error) {
203+
jsonBytes, err := json.Marshal(metadata)
204+
if err != nil {
205+
return nil, err
206+
}
207+
208+
if compression == NoCompression {
209+
return jsonBytes, nil
210+
} else if compression == Gzip {
211+
var b bytes.Buffer
212+
w, err := gzip.NewWriterLevel(&b, gzip.BestCompression)
213+
if err != nil {
214+
return nil, err
215+
}
216+
w.Write(jsonBytes)
217+
w.Close()
218+
return b.Bytes(), nil
219+
} else {
220+
return nil, errors.New("compression not supported")
221+
}
222+
}
223+
224+
func DeserializeMetadataBytes(reader io.Reader, compression Compression) ([]byte, error) {
225+
var jsonBytes []byte
226+
var err error
227+
228+
if compression == NoCompression {
229+
jsonBytes, err = io.ReadAll(reader)
230+
if err != nil {
231+
return nil, err
232+
}
233+
} else if compression == Gzip {
234+
gzipReader, err := gzip.NewReader(reader)
235+
if err != nil {
236+
return nil, err
237+
}
238+
jsonBytes, err = io.ReadAll(gzipReader)
239+
if err != nil {
240+
return nil, err
241+
}
242+
gzipReader.Close()
243+
} else {
244+
return nil, errors.New("compression not supported")
245+
}
246+
247+
return jsonBytes, nil
248+
}
249+
250+
func DeserializeMetadata(reader io.Reader, compression Compression) (map[string]interface{}, error) {
251+
jsonBytes, err := DeserializeMetadataBytes(reader, compression)
252+
var metadata map[string]interface{}
253+
err = json.Unmarshal(jsonBytes, &metadata)
254+
255+
if err != nil {
256+
return nil, err
257+
}
258+
259+
return metadata, nil
260+
}
261+
201262
func SerializeEntries(entries []EntryV3, compression Compression) []byte {
202263
var b bytes.Buffer
203264
var w io.WriteCloser

pmtiles/directory_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,20 @@ func TestStringToCompression(t *testing.T) {
252252
assert.False(t, has)
253253
assert.Equal(t, "unknown", s)
254254
}
255+
256+
func TestMetadataRoundtrip(t *testing.T) {
257+
data := map[string]interface{}{
258+
"foo": "bar",
259+
}
260+
b, err := SerializeMetadata(data, NoCompression)
261+
assert.Nil(t, err)
262+
newData, err := DeserializeMetadata(bytes.NewReader(b), NoCompression)
263+
assert.Nil(t, err)
264+
assert.Equal(t, "bar", newData["foo"])
265+
266+
b, err = SerializeMetadata(data, Gzip)
267+
assert.Nil(t, err)
268+
newData, err = DeserializeMetadata(bytes.NewReader(b), Gzip)
269+
assert.Nil(t, err)
270+
assert.Equal(t, "bar", newData["foo"])
271+
}

pmtiles/edit.go

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package pmtiles
22

33
import (
44
"bytes"
5-
"compress/gzip"
65
"encoding/json"
76
"fmt"
87
"github.com/schollz/progressbar/v3"
@@ -78,22 +77,18 @@ func Edit(_ *log.Logger, inputArchive string, newHeaderJSONFile string, newMetad
7877
return nil
7978
}
8079

81-
newMetadataUncompressed, err := ioutil.ReadFile(newMetadataFile)
82-
83-
var parsedMetadata map[string]interface{}
84-
if err := json.Unmarshal(newMetadataUncompressed, &parsedMetadata); err != nil {
80+
metadataReader, err := os.Open(newMetadataFile)
81+
if err != nil {
8582
return err
8683
}
84+
defer metadataReader.Close()
8785

88-
var metadataBytes bytes.Buffer
89-
if oldHeader.InternalCompression != Gzip {
90-
return fmt.Errorf("only gzip internal compression is currently supported")
86+
parsedMetadata, err := DeserializeMetadata(metadataReader, NoCompression)
87+
if err != nil {
88+
return err
9189
}
9290

93-
w, _ := gzip.NewWriterLevel(&metadataBytes, gzip.BestCompression)
94-
w.Write(newMetadataUncompressed)
95-
w.Close()
96-
91+
metadataBytes, err := SerializeMetadata(parsedMetadata, oldHeader.InternalCompression)
9792
if err != nil {
9893
return err
9994
}
@@ -111,12 +106,12 @@ func Edit(_ *log.Logger, inputArchive string, newHeaderJSONFile string, newMetad
111106
defer outfile.Close()
112107

113108
newHeader.MetadataOffset = newHeader.RootOffset + newHeader.RootLength
114-
newHeader.MetadataLength = uint64(len(metadataBytes.Bytes()))
109+
newHeader.MetadataLength = uint64(len(metadataBytes))
115110
newHeader.LeafDirectoryOffset = newHeader.MetadataOffset + newHeader.MetadataLength
116111
newHeader.TileDataOffset = newHeader.LeafDirectoryOffset + newHeader.LeafDirectoryLength
117112

118113
bar := progressbar.DefaultBytes(
119-
int64(HeaderV3LenBytes+newHeader.RootLength+uint64(len(metadataBytes.Bytes()))+newHeader.LeafDirectoryLength+newHeader.TileDataLength),
114+
int64(HeaderV3LenBytes+newHeader.RootLength+uint64(len(metadataBytes))+newHeader.LeafDirectoryLength+newHeader.TileDataLength),
120115
"writing file",
121116
)
122117

@@ -128,7 +123,7 @@ func Edit(_ *log.Logger, inputArchive string, newHeaderJSONFile string, newMetad
128123
return err
129124
}
130125

131-
if _, err := io.Copy(io.MultiWriter(outfile, bar), bytes.NewReader(metadataBytes.Bytes())); err != nil {
126+
if _, err := io.Copy(io.MultiWriter(outfile, bar), bytes.NewReader(metadataBytes)); err != nil {
132127
return err
133128
}
134129

pmtiles/server.go

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package pmtiles
22

33
import (
44
"bytes"
5-
"compress/gzip"
65
"container/list"
76
"context"
87
"encoding/json"
@@ -265,14 +264,9 @@ func (server *Server) getHeaderMetadataAttempt(ctx context.Context, name, purgeE
265264
}
266265
defer r.Close()
267266

268-
var metadataBytes []byte
269-
if header.InternalCompression == Gzip {
270-
metadataReader, _ := gzip.NewReader(r)
271-
defer metadataReader.Close()
272-
metadataBytes, err = io.ReadAll(metadataReader)
273-
} else if header.InternalCompression == NoCompression {
274-
metadataBytes, err = io.ReadAll(r)
275-
} else {
267+
metadataBytes, err := DeserializeMetadataBytes(r, header.InternalCompression)
268+
269+
if err != nil {
276270
status = "error"
277271
return true, HeaderV3{}, nil, "", errors.New("unknown compression")
278272
}

pmtiles/server_test.go

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package pmtiles
22

33
import (
4-
"bytes"
5-
"compress/gzip"
64
"context"
7-
"encoding/json"
85
"log"
96
"net/http"
107
"net/http/httptest"
@@ -71,20 +68,7 @@ func fakeArchive(t *testing.T, header HeaderV3, metadata map[string]interface{},
7168
tileDataBytes = append(tileDataBytes, tileBytes...)
7269
}
7370

74-
var metadataBytes []byte
75-
{
76-
metadataBytesUncompressed, err := json.Marshal(metadata)
77-
assert.Nil(t, err)
78-
var b bytes.Buffer
79-
if internalCompression == Gzip {
80-
w, _ := gzip.NewWriterLevel(&b, gzip.BestCompression)
81-
w.Write(metadataBytesUncompressed)
82-
w.Close()
83-
} else {
84-
b.Write(metadataBytesUncompressed)
85-
}
86-
metadataBytes = b.Bytes()
87-
}
71+
metadataBytes, _ := SerializeMetadata(metadata, internalCompression)
8872
var rootBytes []byte
8973
var leavesBytes []byte
9074
if leaves {

0 commit comments

Comments
 (0)