Skip to content

Commit

Permalink
add support for .tif.ovr external overviews (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbonfort committed Jun 28, 2021
1 parent 9d4f443 commit 957b8b3
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 68 deletions.
12 changes: 11 additions & 1 deletion README.md
Expand Up @@ -35,12 +35,22 @@ go install github.com/airbusgeo/cogger/cmd/cogger@latest

### Binary

#### With internal overviews

```bash
gdal_translate -of GTIFF -co BIGTIFF=YES -co TILED=YES -co COMPRESS=ZSTD -co NUM_THREADS=4 input.file geotif.tif
gdaladdo --config GDAL_NUM_THREADS 4 geotif.tif 2 4 8 16 32
gdaladdo --config GDAL_NUM_THREADS 4 --config COMPRESS_OVERVIEW ZSTD geotif.tif 2 4 8 16 32
cogger -output mycog.tif geotif.tif
```

#### With external overviews

```bash
gdal_translate -of GTIFF -co BIGTIFF=YES -co TILED=YES -co COMPRESS=ZSTD -co NUM_THREADS=4 input.file geotif.tif
gdaladdo -ro --config GDAL_NUM_THREADS 4 --config COMPRESS_OVERVIEW ZSTD geotif.tif 2 4 8 16 32 #creates geotif.tif.ovr
cogger -output mycog.tif geotif.tif geotif.tif.ovr
```

### Library

The cogger API consists of a single function:
Expand Down
32 changes: 21 additions & 11 deletions cogger_test.go
Expand Up @@ -6,36 +6,41 @@ import (
"io"
"os"
"testing"

"github.com/google/tiff"
)

func testCase(t *testing.T, filename string) {
func testCase(t *testing.T, expected_filename string, filenames ...string) {
t.Helper()
f, err := os.Open("testdata/cog_" + filename)
f, err := os.Open("testdata/" + expected_filename)
if err != nil {
t.Fatal(err)
}
hasher := md5.New()
_, _ = io.Copy(hasher, f)
srchash := hasher.Sum(nil)
f.Close()
f, err = os.Open("testdata/" + filename)
if err != nil {
t.Fatal(err)
}
defer f.Close()

_, _ = f.Seek(0, io.SeekStart)
files := make([]tiff.ReadAtReadSeeker, len(filenames))
for i := range filenames {
f, err = os.Open("testdata/" + filenames[i])
if err != nil {
t.Fatal(err)
}
defer f.Close()
files[i] = f
}

buf := bytes.Buffer{}

hasher.Reset()
_ = Rewrite(&buf, f)
_ = Rewrite(&buf, files...)
_, _ = io.Copy(hasher, &buf)

coghash := hasher.Sum(nil)

if !bytes.Equal(coghash, srchash) {
t.Errorf("mismatch on %s: %x / %x", filename, srchash, coghash)
t.Errorf("mismatch on %v: %x / %x", filenames, srchash, coghash)
}
}

Expand All @@ -49,6 +54,11 @@ func TestCases(t *testing.T) {
"rgb.tif",
}
for i := range cases {
testCase(t, cases[i])
testCase(t, "cog_"+cases[i], cases[i])
}
}

func TestMultiFiles(t *testing.T) {
testCase(t, "cog_ext_ovr.tif", "exttest.tif", "exttest.tif.ovr")
testCase(t, "cog_ext_multi.tif", "exttest.tif", "exttest.tif.2", "exttest.tif.4")
}
97 changes: 41 additions & 56 deletions loader.go
Expand Up @@ -8,8 +8,7 @@ import (
"github.com/google/tiff"
)

func loadMultipleTIFFs(tifs []tiff.TIFF) (*cog, error) {
cog := new()
func loadMultipleTIFFs(tifs []tiff.TIFF) ([]*ifd, error) {
ifds := make([]*ifd, 0)
for it, tif := range tifs {
tifds := tif.IFDs()
Expand All @@ -18,41 +17,22 @@ func loadMultipleTIFFs(tifs []tiff.TIFF) (*cog, error) {
if err != nil {
return nil, err
}
if ifd.SubfileType&subfileTypeReducedImage == subfileTypeReducedImage {
return nil, fmt.Errorf("cannot load multiple tifs if they contain overviews")
}
if it != 0 {
//check that the additional files are smaller than the first, i.e. that they represent an overview
if ifd.ImageLength >= ifds[0].ImageLength || ifd.ImageWidth >= ifds[0].ImageWidth {
return nil, fmt.Errorf("provided tiff %d size %dx%d is larger than first tiff size %dx%d. when using multiple files, the subsequent ones must be overviews of the first one",
it, ifd.ImageWidth, ifd.ImageLength, ifds[0].ImageWidth, ifds[0].ImageLength)
}
//force to overview
ifd.SubfileType |= subfileTypeReducedImage
}
ifds = append(ifds, ifd)
}
}
sort.Slice(ifds, func(i, j int) bool {
//return in order: fullres, fullresmasks, ovr1, ovr1masks, ovr2, ....
if ifds[i].ImageLength != ifds[j].ImageLength {
return ifds[i].ImageLength > ifds[j].ImageLength
}
return ifds[i].SubfileType < ifds[j].SubfileType
})
if ifds[0].SubfileType != 0 {
return nil, fmt.Errorf("failed sort: first px=%d type=%d", ifds[0].ImageLength, ifds[0].SubfileType)
}
cog.ifd = ifds[0]
curOvr := cog.ifd
l := curOvr.ImageLength
for _, ci := range ifds[1:] {
if ci.ImageLength == l {
curOvr.AddMask(ci)
} else {
curOvr.AddOverview(ci)
curOvr = ci
l = curOvr.ImageLength
}
}
return cog, nil
return ifds, nil
}
func loadSingleTIFF(tif tiff.TIFF) (*cog, error) {
cog := new()

func loadSingleTIFF(tif tiff.TIFF) ([]*ifd, error) {
tifds := tif.IFDs()
ifds := make([]*ifd, len(tifds))
var err error
Expand All @@ -62,29 +42,7 @@ func loadSingleTIFF(tif tiff.TIFF) (*cog, error) {
return nil, err
}
}
sort.Slice(ifds, func(i, j int) bool {
//return in order: fullres, fullresmasks, ovr1, ovr1masks, ovr2, ....
if ifds[i].ImageLength != ifds[j].ImageLength {
return ifds[i].ImageLength > ifds[j].ImageLength
}
return ifds[i].SubfileType < ifds[j].SubfileType
})
if ifds[0].SubfileType != 0 {
return nil, fmt.Errorf("failed sort: first px=%d type=%d", ifds[0].ImageLength, ifds[0].SubfileType)
}
cog.ifd = ifds[0]
curOvr := cog.ifd
l := curOvr.ImageLength
for _, ci := range ifds[1:] {
if ci.ImageLength == l {
curOvr.AddMask(ci)
} else {
curOvr.AddOverview(ci)
curOvr = ci
l = curOvr.ImageLength
}
}
return cog, nil
return ifds, nil
}

func loadIFD(r tiff.BReader, tifd tiff.IFD) (*ifd, error) {
Expand Down Expand Up @@ -121,18 +79,45 @@ func Rewrite(out io.Writer, readers ...tiff.ReadAtReadSeeker) error {
if err != nil {
return fmt.Errorf("consistency check: %w", err)
}
var cog *cog
var ifds []*ifd
if len(tiffs) > 1 {
cog, err = loadMultipleTIFFs(tiffs)
ifds, err = loadMultipleTIFFs(tiffs)
if err != nil {
return fmt.Errorf("load: %w", err)
}
} else {
cog, err = loadSingleTIFF(tiffs[0])
ifds, err = loadSingleTIFF(tiffs[0])
if err != nil {
return fmt.Errorf("load: %w", err)
}
}
sort.Slice(ifds, func(i, j int) bool {
//return in order: fullres, fullresmasks, ovr1, ovr1masks, ovr2, ....
if ifds[i].ImageLength != ifds[j].ImageLength {
return ifds[i].ImageLength > ifds[j].ImageLength
}
return ifds[i].SubfileType < ifds[j].SubfileType
})
if ifds[0].SubfileType != 0 {
return fmt.Errorf("failed sort: first px=%d type=%d", ifds[0].ImageLength, ifds[0].SubfileType)
}
cog := new()
cog.ifd = ifds[0]
curOvr := cog.ifd
l := curOvr.ImageLength
for _, ci := range ifds[1:] {
if ci.ImageLength == l {
err = curOvr.AddMask(ci)
if err != nil {
return err
}
} else {
curOvr.AddOverview(ci)
curOvr = ci
l = curOvr.ImageLength
}
}

err = cog.write(out)
if err != nil {
return fmt.Errorf("mucog write: %w", err)
Expand Down
Binary file added testdata/cog_ext_multi.tif
Binary file not shown.
Binary file added testdata/cog_ext_ovr.tif
Binary file not shown.
Binary file added testdata/exttest.tif
Binary file not shown.
Binary file added testdata/exttest.tif.2
Binary file not shown.
Binary file added testdata/exttest.tif.4
Binary file not shown.
Binary file added testdata/exttest.tif.ovr
Binary file not shown.

0 comments on commit 957b8b3

Please sign in to comment.