Skip to content

Commit dbaac79

Browse files
dsamaeypomadchin
andauthored
Add ZStd compression support for GTiff (#3580)
* Add ZStd compression support for GTiff * cleanup the code / build * ZStdDecompressor constructor cleanup --------- Co-authored-by: dsamaey <[email protected]> Co-authored-by: Grigory Pomadchin <[email protected]>
1 parent 267a4fd commit dbaac79

File tree

7 files changed

+125
-3
lines changed

7 files changed

+125
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66

77
## [Unreleased]
88

9+
### Added
10+
- Add ZStd compression support for GTiff
11+
912
## [3.8.0] - 2025-04-23
1013

1114
### Added

project/Dependencies.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ object Dependencies {
9292
val sl4jnop = "org.slf4j" % "slf4j-nop" % "1.7.25"
9393
val logbackClassic = "ch.qos.logback" % "logback-classic" % "1.2.3"
9494
val guava = "com.google.guava" % "guava" % "16.0.1"
95+
val zstdJni = "com.github.luben" % "zstd-jni" % "1.5.7-3"
9596

9697
val cassandraDriverCore = "com.datastax.oss" % "java-driver-core" % Version.cassandra
9798
val cassandraDriverQueryBuilder = "com.datastax.oss" % "java-driver-query-builder" % Version.cassandra

project/Settings.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,9 @@ object Settings {
4040
val ivy2Local = Resolver.file("local", file(Path.userHome.absolutePath + "/.ivy2/local"))(Resolver.ivyStylePatterns)
4141
val mavenLocal = Resolver.mavenLocal
4242
val maven = DefaultMavenRepository
43-
val sonatypeReleases = Resolver.sonatypeOssRepos("releases")
44-
val sonatypeSnapshots = Resolver.sonatypeOssRepos("snapshots")
43+
val sonatypeSnapshots = Resolver.sonatypeCentralSnapshots
4544
val local = Seq(ivy2Local, mavenLocal)
46-
val external = Seq(osgeoReleases, maven, eclipseReleases, geosolutions, jitpack, apacheSnapshots, osgeoSnapshots) ++ sonatypeReleases ++ sonatypeSnapshots
45+
val external = Seq(osgeoReleases, maven, eclipseReleases, geosolutions, jitpack, apacheSnapshots, osgeoSnapshots, sonatypeSnapshots)
4746
val all = external ++ local
4847
}
4948

@@ -419,6 +418,8 @@ object Settings {
419418
name := "geotrellis-raster",
420419
libraryDependencies ++= Seq(
421420
squants,
421+
zstdJni,
422+
apacheIO excludeAll(ExclusionRule("com.fasterxml.jackson.core"), ExclusionRule("com.google.j2objc")),
422423
monocle("core").value,
423424
monocle("macro").value,
424425
scalaXml,

raster/src/main/scala/geotrellis/raster/io/geotiff/compression/Decompressor.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ object Decompressor {
117117
checkEndian(PackBitsDecompressor(segmentSizes))
118118
case JpegCoded =>
119119
checkEndian(JpegDecompressor(tiffTags))
120+
case ZStdCoded =>
121+
checkPredictor(ZStdCompression.createDecompressor)
120122

121123
// Unsupported compression types
122124
case HuffmanCoded =>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2016 Azavea
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package geotrellis.raster.io.geotiff.compression
18+
19+
import geotrellis.raster.io.geotiff.tags.codes.CompressionType._
20+
21+
import com.github.luben.zstd.{ZstdInputStream, ZstdOutputStream}
22+
import org.apache.commons.io.IOUtils
23+
24+
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
25+
26+
case class ZStdCompression(level: Int = 3) extends Compression {
27+
def createCompressor(segmentCount: Int): Compressor =
28+
new ZStdCompressor(segmentCount, level)
29+
30+
def createDecompressor: Decompressor =
31+
new ZStdDecompressor()
32+
33+
}
34+
35+
object ZStdCompression extends ZStdCompression(3)
36+
37+
class ZStdCompressor(segmentCount: Int, level: Int) extends Compressor {
38+
private val segmentSizes = Array.ofDim[Int](segmentCount)
39+
40+
def compress(segment: Array[Byte], segmentIndex: Int): Array[Byte] = {
41+
segmentSizes(segmentIndex) = segment.size
42+
43+
val outputStream = new ByteArrayOutputStream()
44+
val compressorOutputStream = new ZstdOutputStream(outputStream, level)
45+
IOUtils.copyLarge(new ByteArrayInputStream(segment), compressorOutputStream)
46+
compressorOutputStream.close()
47+
outputStream.toByteArray
48+
}
49+
50+
def createDecompressor(): Decompressor =
51+
new ZStdDecompressor()
52+
}
53+
54+
class ZStdDecompressor extends Decompressor {
55+
def code = ZStdCoded
56+
57+
def decompress(segment: Array[Byte], segmentIndex: Int): Array[Byte] = {
58+
val outputStream = new ByteArrayOutputStream()
59+
val stream = new ByteArrayInputStream(segment)
60+
val compressorInputStream = new ZstdInputStream(stream)
61+
IOUtils.copyLarge(compressorInputStream, outputStream)
62+
compressorInputStream.close()
63+
outputStream.toByteArray
64+
}
65+
}
66+

raster/src/main/scala/geotrellis/raster/io/geotiff/tags/codes/CompressionType.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ object CompressionType {
2828
val ZLibCoded = 8
2929
val PackBitsCoded = 32773
3030
val PkZipCoded = 32946
31+
val ZStdCoded = 50000
3132

3233
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2016 Azavea
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package geotrellis.raster.io.geotiff.compression
18+
19+
import org.scalatest.matchers.should.Matchers
20+
import org.scalatest.funspec.AnyFunSpec
21+
22+
class ZStdCompressionSpec extends AnyFunSpec with Matchers {
23+
describe("ZStdCompression") {
24+
(-1 to 9).foreach { level =>
25+
it(s"should decompress and compress to the same things at the ${if(level == -1) "default compression level" else s"zoom level $level"}") {
26+
val segment =
27+
"""
28+
| the human machine only moves when the tide of every one of us pushes to the outside
29+
| and we realize that from the time you wake until the time you shake it up
30+
| you're nothing more than a ghost echo
31+
| it burns brighter when the pieces move the best way,
32+
| whatever best may mean,
33+
| it's so subjective. but when enough agree we can finally move it.
34+
| when the pieces understand what they're doing they can chose a direction
35+
| and run a little hotter.
36+
| against inertia,
37+
| it goes.
38+
| whole lives change
39+
| only the moment remains here""".getBytes("UTF-8")
40+
val compressor = new ZStdCompression(level).createCompressor(1)
41+
val compressed = compressor.compress(segment, 0)
42+
val decompressor = compressor.createDecompressor()
43+
val decompressed = decompressor.decompress(compressed, 0)
44+
decompressed should be(segment)
45+
}
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)