comparison make-tile @ 0:c20b5314774f

Initial commit.
author David Barts <n5jrn@me.com>
date Tue, 24 Aug 2021 17:40:00 -0700
parents
children d268ae31f94b
comparison
equal deleted inserted replaced
-1:000000000000 0:c20b5314774f
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
3 # should honor GDALBIN, if set, else use PATH
4 # https://opengislab.com/blog/2016/4/2/usgs-geopdf-to-geotif-with-gdal
5 # gdalinfo -json -mdd LAYERS gunk.pdf
6 # build list of all non-Map.Frame.* layers
7 # to that add Map_Frame.Projection_and_Grids
8 # First:
9 # gdal_translate gunk.pdf /tmp/xxxxx.tif -of GTiff --config GDAL_PDF_LAYERS_OFF Map_Collar,Map_Frame.Projections_and_Grids,Map_Frame.Terrain.Shaded_Relief,Images.Orthoimage --config GDAL_PDF_DPI 300
10 # Then:
11 # gdal_translate /tmp/xxxxx.tif gunk.tif -of GTiff -srcwin xoff yoff xsize ysize
12 # Need to filter out all errors containing: insufficient arguments for Marked Content
13 # also fiter out: More than 1000 errors or warnings have been reported. No more will be reported from now.
14
15 # I m p o r t s
16
17 import os, sys
18 import argparse
19 import json
20 import subprocess, shutil
21 import locale
22 from abc import abstractmethod
23 from enum import IntEnum
24 from tempfile import TemporaryDirectory
25
26 # PIL is ...weird... and needs both of these.
27 import PIL
28 from PIL import Image
29
30 # V a r i a b l e s
31
32 MYNAME = os.path.basename(sys.argv[0])
33 estat = 0
34 ENCODING = "UTF-8"
35 ENVIRON = os.environ.copy()
36 for k in ENVIRON.keys():
37 if k.startswith("LC_"): del(ENVIRON[k])
38 ENVIRON["LANG"] = ENVIRON["LC_ALL"] = "C." + ENCODING
39
40 # C l a s s e s
41
42 # A horizontal or vertical strip of an image. Assumes RGB images with 8-bit
43 # color.
44 class Strip:
45 def __init__(self, based_on, index):
46 if not isinstance(based_on, PIL.Image.Image):
47 raise TypeError("Expecting RGB PIL.Image object for based_on.")
48 if based_on.mode != "RGB":
49 raise ValueError("Expecting RGB PIL.Image object for based_on.")
50 self._index = index
51 self._based_on = based_on
52
53 @property
54 def index(self):
55 return self._index
56
57 @abstractmethod
58 def __len__(self):
59 pass
60
61 @abstractmethod
62 def __getitem__(self, key):
63 pass
64
65 @abstractmethod
66 def __setitem__(self, key, value):
67 pass
68
69 def is_all_white(self):
70 for i in range(len(self)):
71 if self[i] != (255, 255, 255):
72 return False
73 return True
74
75 # A horizontal strip of an image.
76 class Row(Strip):
77 def __len__(self):
78 return self._based_on.width
79
80 def __getitem__(self, key):
81 return self._based_on.getpixel((key, self._index))
82
83 def __setitem__(self, key, value):
84 self._based_on.setpixel((key, self._index), value)
85
86 # A vertical strip of an image.
87 class Column(Strip):
88 def __len__(self):
89 return self._based_on.height
90
91 def __getitem__(self, key):
92 return self._based_on.getpixel((self._index, key))
93
94 def __setitem__(self, key, value):
95 self._based_on.setpixel((self._index, key), value)
96
97 # A direction in which to search.
98 class Direction(IntEnum):
99 DESCENDING = -1
100 ASCENDING = 1
101
102 # An orientation in which to search.
103 class Orientation(IntEnum):
104 VERTICAL = 0
105 HORIZONTAL = 1
106
107 class TopoTilerException(Exception):
108 pass
109
110 # F u n c t i o n s
111
112 def find_edge(image, orientation, direction):
113 """
114 Find an edge in an image with a white border.
115 """
116 if orientation == Orientation.VERTICAL:
117 what = "column"
118 strip = Column
119 maxn = image.width - 1
120 else:
121 what = "row"
122 strip = Row
123 maxn = image.height - 1
124 # Establish outer bound and sanity-check it.
125 outer = 0 if direction == Direction.ASCENDING else maxn
126 if not strip(image, outer).is_all_white():
127 raise TopoTilerException("Unexpected nonwhite edge ({0} = {1})!".format(what, outer))
128 # Establish inner bound and sanity-check it.
129 inner = outer + direction * maxn // 4
130 if strip(image, inner).is_all_white():
131 raise TopoTilerException("Unexpected white strip ({0} = {1})!".format(what, inner))
132 while abs(outer - inner) > 1:
133 new = (outer + inner) // 2
134 if strip(image, new).is_all_white():
135 outer = new
136 else:
137 inner = new
138 return inner
139
140 def positive_int(string):
141 """
142 Specify a positive integer argument.
143 """
144 try:
145 ret = int(string)
146 except ValueError as e:
147 raise argparse.ArgumentTypeError(e.message)
148 if ret <= 0:
149 raise argparse.ArgumentTypeError("%r is 0 or negative" % string)
150 return ret
151
152 def gdalcmd(command, *args):
153 """
154 Build gdal command vector for subprocess.Popen
155 """
156 # print('$', command, ' '.join(args)) # debug
157 gdalbin = os.environ.get("GDALBIN")
158 cmd = shutil.which(command) if gdalbin is None else os.path.join(gdalbin, command)
159 ret = list(args)
160 ret.insert(0, cmd)
161 return ret
162
163 _MUZZLE = [
164 "insufficient arguments for Marked Content",
165 "More than 1000 errors or warnings have been reported."
166 ]
167 def drainout(stream):
168 """
169 Drain an output/error stream, muzzling the meaningless babble.
170 """
171 for line in stream:
172 printit = True
173 for m in _MUZZLE:
174 if m in line:
175 printit = False
176 break
177 if printit:
178 sys.stderr.write(line)
179
180 def waitfor(proc):
181 global estat
182 command_name = os.path.basename(proc.args[0])
183 status = proc.wait()
184 if status < 0:
185 sys.stderr.write("{0}: {1} killed by signal {2}\n".format(MYNAME, command_name, -status))
186 estat = 1
187 elif status > 0:
188 sys.stderr.write("{0}: {1} exited with status {2}\n".format(MYNAME, command_name, status))
189 estat = 1
190
191 _BLACKLIST = set(["Map_Frame.Projection_and_Grids", "Map_Frame.Terrain.Shaded_Relief"])
192 def get_default_layers(name):
193 """
194 Given a file, analyze it, and get the list of layers to not include
195 when rendering it.
196 """
197 proc = subprocess.Popen(
198 gdalcmd("gdalinfo", "-json", "-mdd", "LAYERS", name),
199 stdout=subprocess.PIPE, stderr=sys.stderr, env=ENVIRON, encoding=ENCODING)
200 data = json.load(proc.stdout)
201 waitfor(proc)
202 layers = set(["Map_Frame"])
203 for layer in data["metadata"]["LAYERS"].values():
204 if layer.startswith("Map_Frame.") and layer not in _BLACKLIST:
205 layers.add(layer)
206 return ",".join(layers)
207
208 # M a i n P r o g r a m
209
210 # Parse command-line arguments.
211 parser = argparse.ArgumentParser(description='Render GeoPDF into GeoTIFF suitable for tiling.')
212 parser.add_argument('--layers', '-l', type=str,
213 help='List of layers to include (see documentation for default).')
214 parser.add_argument('--resolution', '-r', type=positive_int, default=300,
215 help='Resolution to render at in DPI.')
216 parser.add_argument('file', nargs=1,
217 help='File to read (must be a GeoPDF).')
218 try:
219 args = parser.parse_args()
220 except SystemExit:
221 sys.exit(2)
222
223 # File must end with '.pdf' (case insensitive), or else!
224 if not isinstance(args.file, str):
225 # How silly, it gave us a list or a tuple, despite only one of them!
226 args.file = args.file[0]
227 if not args.file.lower().endswith('.pdf'):
228 sys.steder.write("{0}: input file must end with .pdf (case insensitive)\n".format(MYNAME))
229
230 # Default the set of layers to delete, if necessary
231 if args.layers is None:
232 args.layers = get_default_layers(args.file)
233
234 with TemporaryDirectory() as td:
235 # Get scratch file name. This goes under TMPDIR; if the default temporary
236 # area is too small, set that environment variable accordingly!
237 tf = os.path.join(td, "temp.tiff")
238
239 # Render. The result will have undesired margins.
240 sys.stdout.write("Rendering (may take a while)...\n")
241 sys.stdout.flush()
242 proc = subprocess.Popen(
243 gdalcmd("gdal_translate", "-q", args.file, tf, "-of", "GTiff", "--config", "GDAL_PDF_LAYERS", args.layers, "--config", "GDAL_PDF_DPI", str(args.resolution)),
244 stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, env=ENVIRON, encoding=ENCODING)
245 drainout(proc.stderr)
246 waitfor(proc)
247 sys.stdout.write("Done!\n")
248
249 # Determine crop marquee.
250 with Image.open(tf) as im:
251 minx = find_edge(im, Orientation.VERTICAL, Direction.ASCENDING)
252 maxx = find_edge(im, Orientation.VERTICAL, Direction.DESCENDING)
253 miny = find_edge(im, Orientation.HORIZONTAL, Direction.ASCENDING)
254 maxy = find_edge(im, Orientation.HORIZONTAL, Direction.DESCENDING)
255
256 # Determine output file name.
257 outf = os.path.splitext(args.file)[0] + ".tiff"
258
259 # Crop.
260 proc = subprocess.Popen(
261 gdalcmd("gdal_translate", "-q", tf, outf, "-of", "GTiff", "-srcwin", str(minx), str(miny), str(maxx-minx+1), str(maxy-miny+1)),
262 stdout=subprocess.DEVNULL, stderr=sys.stderr)
263 waitfor(proc)
264
265 # AMF...
266 sys.exit(estat)