|
47 | 47 | from openslide import OpenSlide, OpenSlideCache, OpenSlideError, OpenSlideVersionError
|
48 | 48 | from openslide.deepzoom import DeepZoomGenerator
|
49 | 49 |
|
50 |
| -SLIDE_DIR = '.' |
51 |
| -SLIDE_CACHE_SIZE = 10 |
52 |
| -SLIDE_TILE_CACHE_MB = 128 |
53 |
| -DEEPZOOM_FORMAT = 'jpeg' |
54 |
| -DEEPZOOM_TILE_SIZE = 254 |
55 |
| -DEEPZOOM_OVERLAP = 1 |
56 |
| -DEEPZOOM_LIMIT_BOUNDS = True |
57 |
| -DEEPZOOM_TILE_QUALITY = 75 |
58 | 50 |
|
59 |
| -app = Flask(__name__) |
60 |
| -app.config.from_object(__name__) |
61 |
| -app.config.from_envvar('DEEPZOOM_MULTISERVER_SETTINGS', silent=True) |
| 51 | +def create_app(config=None, config_file=None): |
| 52 | + # Create and configure app |
| 53 | + app = Flask(__name__) |
| 54 | + app.config.from_mapping( |
| 55 | + SLIDE_DIR='.', |
| 56 | + SLIDE_CACHE_SIZE=10, |
| 57 | + SLIDE_TILE_CACHE_MB=128, |
| 58 | + DEEPZOOM_FORMAT='jpeg', |
| 59 | + DEEPZOOM_TILE_SIZE=254, |
| 60 | + DEEPZOOM_OVERLAP=1, |
| 61 | + DEEPZOOM_LIMIT_BOUNDS=True, |
| 62 | + DEEPZOOM_TILE_QUALITY=75, |
| 63 | + ) |
| 64 | + app.config.from_envvar('DEEPZOOM_MULTISERVER_SETTINGS', silent=True) |
| 65 | + if config_file is not None: |
| 66 | + app.config.from_pyfile(config_file) |
| 67 | + if config is not None: |
| 68 | + app.config.from_mapping(config) |
| 69 | + |
| 70 | + # Set up cache |
| 71 | + app.basedir = os.path.abspath(app.config['SLIDE_DIR']) |
| 72 | + config_map = { |
| 73 | + 'DEEPZOOM_TILE_SIZE': 'tile_size', |
| 74 | + 'DEEPZOOM_OVERLAP': 'overlap', |
| 75 | + 'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds', |
| 76 | + } |
| 77 | + opts = {v: app.config[k] for k, v in config_map.items()} |
| 78 | + app.cache = _SlideCache( |
| 79 | + app.config['SLIDE_CACHE_SIZE'], app.config['SLIDE_TILE_CACHE_MB'], opts |
| 80 | + ) |
| 81 | + |
| 82 | + # Helper functions |
| 83 | + def get_slide(path): |
| 84 | + path = os.path.abspath(os.path.join(app.basedir, path)) |
| 85 | + if not path.startswith(app.basedir + os.path.sep): |
| 86 | + # Directory traversal |
| 87 | + abort(404) |
| 88 | + if not os.path.exists(path): |
| 89 | + abort(404) |
| 90 | + try: |
| 91 | + slide = app.cache.get(path) |
| 92 | + slide.filename = os.path.basename(path) |
| 93 | + return slide |
| 94 | + except OpenSlideError: |
| 95 | + abort(404) |
| 96 | + |
| 97 | + # Set up routes |
| 98 | + @app.route('/') |
| 99 | + def index(): |
| 100 | + return render_template('files.html', root_dir=_Directory(app.basedir)) |
| 101 | + |
| 102 | + @app.route('/<path:path>') |
| 103 | + def slide(path): |
| 104 | + slide = get_slide(path) |
| 105 | + slide_url = url_for('dzi', path=path) |
| 106 | + return render_template( |
| 107 | + 'slide-fullpage.html', |
| 108 | + slide_url=slide_url, |
| 109 | + slide_filename=slide.filename, |
| 110 | + slide_mpp=slide.mpp, |
| 111 | + ) |
| 112 | + |
| 113 | + @app.route('/<path:path>.dzi') |
| 114 | + def dzi(path): |
| 115 | + slide = get_slide(path) |
| 116 | + format = app.config['DEEPZOOM_FORMAT'] |
| 117 | + resp = make_response(slide.get_dzi(format)) |
| 118 | + resp.mimetype = 'application/xml' |
| 119 | + return resp |
| 120 | + |
| 121 | + @app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>') |
| 122 | + def tile(path, level, col, row, format): |
| 123 | + slide = get_slide(path) |
| 124 | + format = format.lower() |
| 125 | + if format != 'jpeg' and format != 'png': |
| 126 | + # Not supported by Deep Zoom |
| 127 | + abort(404) |
| 128 | + try: |
| 129 | + tile = slide.get_tile(level, (col, row)) |
| 130 | + except ValueError: |
| 131 | + # Invalid level or coordinates |
| 132 | + abort(404) |
| 133 | + buf = BytesIO() |
| 134 | + tile.save(buf, format, quality=app.config['DEEPZOOM_TILE_QUALITY']) |
| 135 | + resp = make_response(buf.getvalue()) |
| 136 | + resp.mimetype = 'image/%s' % format |
| 137 | + return resp |
| 138 | + |
| 139 | + return app |
62 | 140 |
|
63 | 141 |
|
64 | 142 | class _SlideCache:
|
@@ -121,80 +199,6 @@ def __init__(self, relpath):
|
121 | 199 | self.url_path = relpath
|
122 | 200 |
|
123 | 201 |
|
124 |
| -@app.before_first_request |
125 |
| -def _setup(): |
126 |
| - app.basedir = os.path.abspath(app.config['SLIDE_DIR']) |
127 |
| - config_map = { |
128 |
| - 'DEEPZOOM_TILE_SIZE': 'tile_size', |
129 |
| - 'DEEPZOOM_OVERLAP': 'overlap', |
130 |
| - 'DEEPZOOM_LIMIT_BOUNDS': 'limit_bounds', |
131 |
| - } |
132 |
| - opts = {v: app.config[k] for k, v in config_map.items()} |
133 |
| - app.cache = _SlideCache( |
134 |
| - app.config['SLIDE_CACHE_SIZE'], app.config['SLIDE_TILE_CACHE_MB'], opts |
135 |
| - ) |
136 |
| - |
137 |
| - |
138 |
| -def _get_slide(path): |
139 |
| - path = os.path.abspath(os.path.join(app.basedir, path)) |
140 |
| - if not path.startswith(app.basedir + os.path.sep): |
141 |
| - # Directory traversal |
142 |
| - abort(404) |
143 |
| - if not os.path.exists(path): |
144 |
| - abort(404) |
145 |
| - try: |
146 |
| - slide = app.cache.get(path) |
147 |
| - slide.filename = os.path.basename(path) |
148 |
| - return slide |
149 |
| - except OpenSlideError: |
150 |
| - abort(404) |
151 |
| - |
152 |
| - |
153 |
| -@app.route('/') |
154 |
| -def index(): |
155 |
| - return render_template('files.html', root_dir=_Directory(app.basedir)) |
156 |
| - |
157 |
| - |
158 |
| -@app.route('/<path:path>') |
159 |
| -def slide(path): |
160 |
| - slide = _get_slide(path) |
161 |
| - slide_url = url_for('dzi', path=path) |
162 |
| - return render_template( |
163 |
| - 'slide-fullpage.html', |
164 |
| - slide_url=slide_url, |
165 |
| - slide_filename=slide.filename, |
166 |
| - slide_mpp=slide.mpp, |
167 |
| - ) |
168 |
| - |
169 |
| - |
170 |
| -@app.route('/<path:path>.dzi') |
171 |
| -def dzi(path): |
172 |
| - slide = _get_slide(path) |
173 |
| - format = app.config['DEEPZOOM_FORMAT'] |
174 |
| - resp = make_response(slide.get_dzi(format)) |
175 |
| - resp.mimetype = 'application/xml' |
176 |
| - return resp |
177 |
| - |
178 |
| - |
179 |
| -@app.route('/<path:path>_files/<int:level>/<int:col>_<int:row>.<format>') |
180 |
| -def tile(path, level, col, row, format): |
181 |
| - slide = _get_slide(path) |
182 |
| - format = format.lower() |
183 |
| - if format != 'jpeg' and format != 'png': |
184 |
| - # Not supported by Deep Zoom |
185 |
| - abort(404) |
186 |
| - try: |
187 |
| - tile = slide.get_tile(level, (col, row)) |
188 |
| - except ValueError: |
189 |
| - # Invalid level or coordinates |
190 |
| - abort(404) |
191 |
| - buf = BytesIO() |
192 |
| - tile.save(buf, format, quality=app.config['DEEPZOOM_TILE_QUALITY']) |
193 |
| - resp = make_response(buf.getvalue()) |
194 |
| - resp.mimetype = 'image/%s' % format |
195 |
| - return resp |
196 |
| - |
197 |
| - |
198 | 202 | if __name__ == '__main__':
|
199 | 203 | parser = OptionParser(usage='Usage: %prog [options] [slide-directory]')
|
200 | 204 | parser.add_option(
|
@@ -265,18 +269,18 @@ def tile(path, level, col, row, format):
|
265 | 269 | )
|
266 | 270 |
|
267 | 271 | (opts, args) = parser.parse_args()
|
268 |
| - # Load config file if specified |
269 |
| - if opts.config is not None: |
270 |
| - app.config.from_pyfile(opts.config) |
271 |
| - # Overwrite only those settings specified on the command line |
| 272 | + config = {} |
| 273 | + config_file = opts.config |
| 274 | + # Set only those settings specified on the command line |
272 | 275 | for k in dir(opts):
|
273 |
| - if not k.startswith('_') and getattr(opts, k) is None: |
274 |
| - delattr(opts, k) |
275 |
| - app.config.from_object(opts) |
276 |
| - # Set slide directory |
| 276 | + v = getattr(opts, k) |
| 277 | + if not k.startswith('_') and v is not None: |
| 278 | + config[k] = v |
| 279 | + # Set slide directory if specified |
277 | 280 | try:
|
278 |
| - app.config['SLIDE_DIR'] = args[0] |
| 281 | + config['SLIDE_DIR'] = args[0] |
279 | 282 | except IndexError:
|
280 | 283 | pass
|
| 284 | + app = create_app(config, config_file) |
281 | 285 |
|
282 | 286 | app.run(host=opts.host, port=opts.port, threaded=True)
|
0 commit comments