Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for .tif.ovr external overviews #6

Merged
merged 4 commits into from Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.