#!/usr/bin/env python ############################################################################### # $Id$ # # Project: PROJ # Purpose: Cloud optimize a GeoTIFF file # Author: Even Rouault # ############################################################################### # Copyright (c) 2019, Even Rouault # # Permission is hereby granted, free of charge, to any person obtaining a # copy of this software and associated documentation files (the "Software"), # to deal in the Software without restriction, including without limitation # the rights to use, copy, modify, merge, publish, distribute, sublicense, # and/or sell copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included # in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER # DEALINGS IN THE SOFTWARE. ############################################################################### from osgeo import gdal import argparse import os import struct def get_args(): parser = argparse.ArgumentParser( description='Convert a GeoTIFF file into a cloud optimized one.') parser.add_argument('source', help='Source GeoTIFF file') parser.add_argument('dest', help='Destination GeoTIFF file') return parser.parse_args() def generate_optimized_file(srcfilename, destfilename): TIFF_BYTE = 1 # 8-bit unsigned integer TIFF_ASCII = 2 # 8-bit bytes w/ last byte null TIFF_SHORT = 3 # 16-bit unsigned integer TIFF_LONG = 4 # 32-bit unsigned integer TIFF_RATIONAL = 5 # 64-bit unsigned fraction TIFF_SBYTE = 6 # !8-bit signed integer TIFF_UNDEFINED = 7 # !8-bit untyped data TIFF_SSHORT = 8 # !16-bit signed integer TIFF_SLONG = 9 # !32-bit signed integer TIFF_SRATIONAL = 10 # !64-bit signed fraction TIFF_FLOAT = 11 # !32-bit IEEE floating point TIFF_DOUBLE = 12 # !64-bit IEEE floating point TIFF_IFD = 13 # %32-bit unsigned integer (offset) TIFF_LONG8 = 16 # BigTIFF 64-bit unsigned integer TIFF_SLONG8 = 17 # BigTIFF 64-bit signed integer TIFF_IFD8 = 18 # BigTIFF 64-bit unsigned integer (offset) TIFFTAG_STRIPOFFSETS = 273 TIFFTAG_SAMPLESPERPIXEL = 277 TIFFTAG_STRIPBYTECOUNTS = 279 TIFFTAG_PLANARCONFIG = 284 TIFFTAG_TILEOFFSETS = 324 TIFFTAG_TILEBYTECOUNTS = 325 PLANARCONFIG_CONTIG = 1 PLANARCONFIG_SEPARATE = 2 TIFFTAG_GDAL_METADATA = 42112 typesize = {} typesize[TIFF_BYTE] = 1 typesize[TIFF_ASCII] = 1 typesize[TIFF_SHORT] = 2 typesize[TIFF_LONG] = 4 typesize[TIFF_RATIONAL] = 8 typesize[TIFF_SBYTE] = 1 typesize[TIFF_UNDEFINED] = 1 typesize[TIFF_SSHORT] = 2 typesize[TIFF_SLONG] = 4 typesize[TIFF_SRATIONAL] = 8 typesize[TIFF_FLOAT] = 4 typesize[TIFF_DOUBLE] = 8 typesize[TIFF_IFD] = 4 typesize[TIFF_LONG8] = 8 typesize[TIFF_SLONG8] = 8 typesize[TIFF_IFD8] = 8 class OfflineTag: def __init__(self, tagtype, nvalues, data, fileoffset_in_out_ifd): self.tagtype = tagtype self.nvalues = nvalues self.data = data self.fileoffset_in_out_ifd = fileoffset_in_out_ifd def unpack_array(self): if type(self.data) == type((0,)): return self.data elif self.tagtype == TIFF_SHORT: return struct.unpack('<' + ('H' * self.nvalues), self.data) elif self.tagtype == TIFF_LONG: return struct.unpack('<' + ('I' * self.nvalues), self.data) else: assert False class IFD: def __init__(self, tagdict): self.tagdict = tagdict ds = gdal.Open(srcfilename) assert ds first_band_to_put_at_end = None if ds.GetMetadataItem('TYPE') == 'HORIZONTAL_OFFSET' and \ ds.RasterCount >= 4 and \ ds.GetRasterBand(1).GetDescription() == 'latitude_offset' and \ ds.GetRasterBand(2).GetDescription() == 'longitude_offset' and \ ds.GetRasterBand(3).GetDescription() == 'latitude_offset_accuracy' and \ ds.GetRasterBand(4).GetDescription() == 'longitude_offset_accuracy': first_band_to_put_at_end = 3 elif ds.GetMetadataItem('TYPE') == 'VELOCITY' and \ ds.RasterCount >= 6 and \ ds.GetRasterBand(1).GetDescription() == 'east_velocity' and \ ds.GetRasterBand(2).GetDescription() == 'north_velocity' and \ ds.GetRasterBand(3).GetDescription() == 'up_velocity' and \ ds.GetRasterBand(4).GetDescription() == 'east_velocity_accuracy' and \ ds.GetRasterBand(5).GetDescription() == 'north_velocity_accuracy' and \ ds.GetRasterBand(6).GetDescription() == 'up_velocity_accuracy': first_band_to_put_at_end = 4 del ds in_f = open(srcfilename, 'rb') signature = in_f.read(4) assert signature == b'\x49\x49\x2A\x00' next_ifd_offset = struct.unpack('= first_band_to_put_at_end: for ifd in ifds: if ifd.nbands < first_band_to_put_at_end: continue for i in range(ifd.num_striles_per_band): for iband in range(first_band_to_put_at_end-1, ifd.nbands): idx_strile = ifd.num_striles_per_band * iband + i in_f.seek(ifd.strile_offset_in[idx_strile]) data = in_f.read(ifd.strile_length_in[idx_strile]) ifd.strile_offset_out[idx_strile] = out_f.tell() out_f.write(data) # Write strile offset arrays for ifd in ifds: tagdict = ifd.tagdict if TIFFTAG_STRIPOFFSETS in tagdict: tagtype = tagdict[TIFFTAG_STRIPOFFSETS].tagtype else: tagtype = tagdict[TIFFTAG_TILEOFFSETS].tagtype if ifd.offset_out_offsets < 0: assert ifd.offset_out_offsets != -1 out_f.seek(-ifd.offset_out_offsets) else: out_f.seek(ifd.offset_out_offsets) if tagtype == TIFF_SHORT: for v in ifd.strile_offset_out: assert v < 65536 out_f.write(struct.pack('