662 lines
23 KiB
Python
662 lines
23 KiB
Python
#!/usr/bin/env python3
|
|
|
|
#
|
|
# Deep Zoom Tools
|
|
#
|
|
# Copyright (c) 2008-2019, Daniel Gasienica <daniel@gasienica.ch>
|
|
# Copyright (c) 2008-2011, OpenZoom <http://openzoom.org>
|
|
# Copyright (c) 2010, Boris Bluntschli <boris@bluntschli.ch>
|
|
# Copyright (c) 2008, Kapil Thangavelu <kapil.foss@gmail.com>
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without modification,
|
|
# are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
#
|
|
# 3. Neither the name of OpenZoom nor the names of its contributors may be used
|
|
# to endorse or promote products derived from this software without
|
|
# specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
|
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
|
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
#
|
|
|
|
import io
|
|
import math
|
|
import optparse
|
|
import os
|
|
import shutil
|
|
from urllib.parse import urlparse
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
import warnings
|
|
import xml.dom.minidom
|
|
|
|
import PIL.Image
|
|
|
|
from collections import deque
|
|
|
|
|
|
NS_DEEPZOOM = "http://schemas.microsoft.com/deepzoom/2008"
|
|
|
|
if hasattr(PIL.Image, "Resampling"):
|
|
_RESAMPLING = PIL.Image.Resampling
|
|
else:
|
|
_RESAMPLING = PIL.Image
|
|
|
|
_LANCZOS = _RESAMPLING.LANCZOS
|
|
_BICUBIC = _RESAMPLING.BICUBIC
|
|
_BILINEAR = _RESAMPLING.BILINEAR
|
|
_NEAREST = _RESAMPLING.NEAREST
|
|
|
|
DEFAULT_RESIZE_FILTER = _LANCZOS
|
|
DEFAULT_IMAGE_FORMAT = "jpg"
|
|
|
|
RESIZE_FILTERS = {
|
|
"cubic": _BICUBIC,
|
|
"bilinear": _BILINEAR,
|
|
"bicubic": _BICUBIC,
|
|
"nearest": _NEAREST,
|
|
"antialias": _LANCZOS,
|
|
}
|
|
|
|
IMAGE_FORMATS = {
|
|
"jpg": "jpg",
|
|
"png": "png",
|
|
}
|
|
|
|
|
|
|
|
|
|
class DeepZoomImageDescriptor(object):
|
|
def __init__(
|
|
self, width=None, height=None, tile_size=254, tile_overlap=1, tile_format="jpg"
|
|
):
|
|
self.width = width
|
|
self.height = height
|
|
self.tile_size = tile_size
|
|
self.tile_overlap = tile_overlap
|
|
self.tile_format = tile_format
|
|
self._num_levels = None
|
|
|
|
def open(self, source):
|
|
"""Intialize descriptor from an existing descriptor file."""
|
|
doc = xml.dom.minidom.parse(safe_open(source))
|
|
image = doc.getElementsByTagName("Image")[0]
|
|
size = doc.getElementsByTagName("Size")[0]
|
|
self.width = int(size.getAttribute("Width"))
|
|
self.height = int(size.getAttribute("Height"))
|
|
self.tile_size = int(image.getAttribute("TileSize"))
|
|
self.tile_overlap = int(image.getAttribute("Overlap"))
|
|
self.tile_format = image.getAttribute("Format")
|
|
|
|
def save(self, destination):
|
|
"""Save descriptor file."""
|
|
file = open(destination, "wb")
|
|
doc = xml.dom.minidom.Document()
|
|
image = doc.createElementNS(NS_DEEPZOOM, "Image")
|
|
image.setAttribute("xmlns", NS_DEEPZOOM)
|
|
image.setAttribute("TileSize", str(self.tile_size))
|
|
image.setAttribute("Overlap", str(self.tile_overlap))
|
|
image.setAttribute("Format", str(self.tile_format))
|
|
size = doc.createElementNS(NS_DEEPZOOM, "Size")
|
|
size.setAttribute("Width", str(self.width))
|
|
size.setAttribute("Height", str(self.height))
|
|
image.appendChild(size)
|
|
doc.appendChild(image)
|
|
descriptor = doc.toxml(encoding="UTF-8")
|
|
file.write(descriptor)
|
|
file.close()
|
|
|
|
@classmethod
|
|
def remove(self, filename):
|
|
"""Remove descriptor file (DZI) and tiles folder."""
|
|
_remove(filename)
|
|
|
|
@property
|
|
def num_levels(self):
|
|
"""Number of levels in the pyramid."""
|
|
if self._num_levels is None:
|
|
max_dimension = max(self.width, self.height)
|
|
self._num_levels = int(math.ceil(math.log(max_dimension, 2))) + 1
|
|
return self._num_levels
|
|
|
|
def get_scale(self, level):
|
|
"""Scale of a pyramid level."""
|
|
assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
|
|
max_level = self.num_levels - 1
|
|
return math.pow(0.5, max_level - level)
|
|
|
|
def get_dimensions(self, level):
|
|
"""Dimensions of level (width, height)"""
|
|
assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
|
|
scale = self.get_scale(level)
|
|
width = int(math.ceil(self.width * scale))
|
|
height = int(math.ceil(self.height * scale))
|
|
return (width, height)
|
|
|
|
def get_num_tiles(self, level):
|
|
"""Number of tiles (columns, rows)"""
|
|
assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
|
|
w, h = self.get_dimensions(level)
|
|
return (
|
|
int(math.ceil(float(w) / self.tile_size)),
|
|
int(math.ceil(float(h) / self.tile_size)),
|
|
)
|
|
|
|
def get_tile_bounds(self, level, column, row):
|
|
"""Bounding box of the tile (x1, y1, x2, y2)"""
|
|
assert 0 <= level and level < self.num_levels, "Invalid pyramid level"
|
|
offset_x = 0 if column == 0 else self.tile_overlap
|
|
offset_y = 0 if row == 0 else self.tile_overlap
|
|
x = (column * self.tile_size) - offset_x
|
|
y = (row * self.tile_size) - offset_y
|
|
level_width, level_height = self.get_dimensions(level)
|
|
w = self.tile_size + (1 if column == 0 else 2) * self.tile_overlap
|
|
h = self.tile_size + (1 if row == 0 else 2) * self.tile_overlap
|
|
w = min(w, level_width - x)
|
|
h = min(h, level_height - y)
|
|
return (x, y, x + w, y + h)
|
|
|
|
|
|
class DeepZoomCollection(object):
|
|
def __init__(
|
|
self,
|
|
filename,
|
|
image_quality=0.8,
|
|
max_level=7,
|
|
tile_size=256,
|
|
tile_format="jpg",
|
|
tile_background_color="#000000",
|
|
items=[],
|
|
):
|
|
self.source = filename
|
|
self.image_quality = image_quality
|
|
self.tile_size = tile_size
|
|
self.max_level = max_level
|
|
self.tile_format = tile_format
|
|
self.tile_background_color = tile_background_color
|
|
self.items = deque(items)
|
|
self.next_item_id = len(self.items)
|
|
# XML
|
|
self.doc = xml.dom.minidom.Document()
|
|
collection = self.doc.createElementNS(NS_DEEPZOOM, "Collection")
|
|
collection.setAttribute("xmlns", NS_DEEPZOOM)
|
|
collection.setAttribute("MaxLevel", str(self.max_level))
|
|
collection.setAttribute("TileSize", str(self.tile_size))
|
|
collection.setAttribute("Format", str(self.tile_format))
|
|
collection.setAttribute("Quality", str(self.image_quality))
|
|
# TODO: Append items passed in as argument
|
|
items = self.doc.createElementNS(NS_DEEPZOOM, "Items")
|
|
collection.appendChild(items)
|
|
collection.setAttribute("NextItemId", str(self.next_item_id))
|
|
self.doc.appendChild(collection)
|
|
|
|
@classmethod
|
|
def from_file(self, filename):
|
|
"""Open collection descriptor."""
|
|
doc = xml.dom.minidom.parse(safe_open(filename))
|
|
collection = doc.getElementsByTagName("Collection")[0]
|
|
image_quality = float(collection.getAttribute("Quality"))
|
|
max_level = int(collection.getAttribute("MaxLevel"))
|
|
tile_size = int(collection.getAttribute("TileSize"))
|
|
tile_format = collection.getAttribute("Format")
|
|
items = [
|
|
DeepZoomCollectionItem.from_xml(item)
|
|
for item in doc.getElementsByTagName("I")
|
|
]
|
|
|
|
collection = DeepZoomCollection(
|
|
filename,
|
|
image_quality=image_quality,
|
|
max_level=max_level,
|
|
tile_size=tile_size,
|
|
tile_format=tile_format,
|
|
items=items,
|
|
)
|
|
return collection
|
|
|
|
@classmethod
|
|
def remove(self, filename):
|
|
"""Remove collection file (DZC) and tiles folder."""
|
|
_remove(filename)
|
|
|
|
def append(self, source):
|
|
descriptor = DeepZoomImageDescriptor()
|
|
descriptor.open(source)
|
|
item = DeepZoomCollectionItem(
|
|
source, descriptor.width, descriptor.height, id=self.next_item_id
|
|
)
|
|
self.items.append(item)
|
|
self.next_item_id += 1
|
|
|
|
def save(self, pretty_print_xml=False):
|
|
"""Save collection descriptor."""
|
|
collection = self.doc.getElementsByTagName("Collection")[0]
|
|
items = self.doc.getElementsByTagName("Items")[0]
|
|
while len(self.items) > 0:
|
|
item = self.items.popleft()
|
|
i = self.doc.createElementNS(NS_DEEPZOOM, "I")
|
|
i.setAttribute("Id", str(item.id))
|
|
i.setAttribute("N", str(item.id))
|
|
i.setAttribute("Source", item.source)
|
|
# Size
|
|
size = self.doc.createElementNS(NS_DEEPZOOM, "Size")
|
|
size.setAttribute("Width", str(item.width))
|
|
size.setAttribute("Height", str(item.height))
|
|
i.appendChild(size)
|
|
items.appendChild(i)
|
|
self._append_image(item.source, item.id)
|
|
collection.setAttribute("NextItemId", str(self.next_item_id))
|
|
with open(self.source, "wb") as f:
|
|
if pretty_print_xml:
|
|
xml = self.doc.toprettyxml(encoding="UTF-8")
|
|
else:
|
|
xml = self.doc.toxml(encoding="UTF-8")
|
|
f.write(xml)
|
|
|
|
def _append_image(self, path, i):
|
|
descriptor = DeepZoomImageDescriptor()
|
|
descriptor.open(path)
|
|
files_path = _get_or_create_path(_get_files_path(self.source))
|
|
for level in reversed(range(self.max_level + 1)):
|
|
level_path = _get_or_create_path("%s/%s" % (files_path, level))
|
|
level_size = 2 ** level
|
|
images_per_tile = int(math.floor(self.tile_size / level_size))
|
|
column, row = self.get_tile_position(i, level, self.tile_size)
|
|
tile_path = "%s/%s_%s.%s" % (level_path, column, row, self.tile_format)
|
|
if not os.path.exists(tile_path):
|
|
tile_image = PIL.Image.new(
|
|
"RGB", (self.tile_size, self.tile_size), self.tile_background_color
|
|
)
|
|
if self.tile_format == "jpg":
|
|
jpeg_quality = int(self.image_quality * 100)
|
|
tile_image.save(tile_path, "JPEG", quality=jpeg_quality)
|
|
else:
|
|
tile_image.save(tile_path)
|
|
tile_image = PIL.Image.open(tile_path)
|
|
source_path = "%s/%s/%s_%s.%s" % (
|
|
_get_files_path(path),
|
|
level,
|
|
0,
|
|
0,
|
|
descriptor.tile_format,
|
|
)
|
|
# Local
|
|
if os.path.exists(source_path):
|
|
try:
|
|
source_image = PIL.Image.open(safe_open(source_path))
|
|
except IOError:
|
|
warnings.warn("Skipped invalid level: %s" % source_path)
|
|
continue
|
|
# Remote
|
|
else:
|
|
if level == self.max_level:
|
|
try:
|
|
source_image = PIL.Image.open(safe_open(source_path))
|
|
except IOError:
|
|
warnings.warn("Skipped invalid image: %s" % source_path)
|
|
return
|
|
# Expected width & height of the tile
|
|
e_w, e_h = descriptor.get_dimensions(level)
|
|
# Actual width & height of the tile
|
|
w, h = source_image.size
|
|
# Correct tile because of IIP bug where low-level tiles have
|
|
# wrong dimensions (they are too large)
|
|
if w != e_w or h != e_h:
|
|
# Resize incorrect tile to correct size
|
|
source_image = source_image.resize(
|
|
(e_w, e_h), DEFAULT_RESIZE_FILTER
|
|
)
|
|
# Store new dimensions
|
|
w, h = e_w, e_h
|
|
else:
|
|
w = int(math.ceil(w * 0.5))
|
|
h = int(math.ceil(h * 0.5))
|
|
source_image.thumbnail((w, h), DEFAULT_RESIZE_FILTER)
|
|
column, row = self.get_position(i)
|
|
x = (column % images_per_tile) * level_size
|
|
y = (row % images_per_tile) * level_size
|
|
tile_image.paste(source_image, (x, y))
|
|
tile_image.save(tile_path)
|
|
|
|
def get_position(self, z_order):
|
|
"""Returns position (column, row) from given Z-order (Morton number.)"""
|
|
column = 0
|
|
row = 0
|
|
for i in range(0, 32, 2):
|
|
offset = i // 2
|
|
# column
|
|
column_offset = i
|
|
column_mask = 1 << column_offset
|
|
column_value = (z_order & column_mask) >> column_offset
|
|
column |= column_value << offset
|
|
# row
|
|
row_offset = i + 1
|
|
row_mask = 1 << row_offset
|
|
row_value = (z_order & row_mask) >> row_offset
|
|
row |= row_value << offset
|
|
return int(column), int(row)
|
|
|
|
def get_z_order(self, column, row):
|
|
"""Returns the Z-order (Morton number) from given position."""
|
|
z_order = 0
|
|
for i in range(32):
|
|
z_order |= (column & 1 << i) << i | (row & 1 << i) << (i + 1)
|
|
return z_order
|
|
|
|
def get_tile_position(self, z_order, level, tile_size):
|
|
level_size = 2 ** level
|
|
x, y = self.get_position(z_order)
|
|
return (
|
|
int(math.floor((x * level_size) / tile_size)),
|
|
int(math.floor((y * level_size) / tile_size)),
|
|
)
|
|
|
|
|
|
class DeepZoomCollectionItem(object):
|
|
def __init__(self, source, width, height, id=0):
|
|
self.id = id
|
|
self.source = source
|
|
self.width = width
|
|
self.height = height
|
|
|
|
@classmethod
|
|
def from_xml(cls, xml):
|
|
id = int(xml.getAttribute("Id"))
|
|
source = xml.getAttribute("Source")
|
|
size = xml.getElementsByTagName("Size")[0]
|
|
width = int(size.getAttribute("Width"))
|
|
height = int(size.getAttribute("Height"))
|
|
return DeepZoomCollectionItem(source, width, height, id)
|
|
|
|
|
|
class ImageCreator(object):
|
|
"""Creates Deep Zoom images."""
|
|
|
|
def __init__(
|
|
self,
|
|
tile_size=254,
|
|
tile_overlap=1,
|
|
tile_format="jpg",
|
|
image_quality=0.8,
|
|
resize_filter=None,
|
|
copy_metadata=False,
|
|
):
|
|
self.tile_size = int(tile_size)
|
|
self.tile_format = tile_format
|
|
self.tile_overlap = _clamp(int(tile_overlap), 0, 10)
|
|
self.image_quality = _clamp(image_quality, 0, 1.0)
|
|
if not tile_format in IMAGE_FORMATS:
|
|
self.tile_format = DEFAULT_IMAGE_FORMAT
|
|
self.resize_filter = resize_filter
|
|
self.copy_metadata = copy_metadata
|
|
|
|
def get_image(self, level):
|
|
"""Returns the bitmap image at the given level."""
|
|
assert (
|
|
0 <= level and level < self.descriptor.num_levels
|
|
), "Invalid pyramid level"
|
|
width, height = self.descriptor.get_dimensions(level)
|
|
# don't transform to what we already have
|
|
if self.descriptor.width == width and self.descriptor.height == height:
|
|
return self.image
|
|
if (self.resize_filter is None) or (self.resize_filter not in RESIZE_FILTERS):
|
|
return self.image.resize((width, height), DEFAULT_RESIZE_FILTER)
|
|
return self.image.resize((width, height), RESIZE_FILTERS[self.resize_filter])
|
|
|
|
def tiles(self, level):
|
|
"""Iterator for all tiles in the given level. Returns (column, row) of a tile."""
|
|
columns, rows = self.descriptor.get_num_tiles(level)
|
|
for column in range(columns):
|
|
for row in range(rows):
|
|
yield (column, row)
|
|
|
|
def create(self, source, destination):
|
|
"""Creates Deep Zoom image from source file and saves it to destination."""
|
|
if isinstance(source, PIL.Image.Image):
|
|
self.image = source
|
|
else:
|
|
self.image = PIL.Image.open((source))
|
|
width, height = self.image.size
|
|
self.descriptor = DeepZoomImageDescriptor(
|
|
width=width,
|
|
height=height,
|
|
tile_size=self.tile_size,
|
|
tile_overlap=self.tile_overlap,
|
|
tile_format=self.tile_format,
|
|
)
|
|
# Create tiles
|
|
image_files = _get_or_create_path(_get_files_path(destination))
|
|
for level in range(self.descriptor.num_levels):
|
|
level_dir = _get_or_create_path(os.path.join(image_files, str(level)))
|
|
level_image = self.get_image(level)
|
|
for (column, row) in self.tiles(level):
|
|
bounds = self.descriptor.get_tile_bounds(level, column, row)
|
|
tile = level_image.crop(bounds)
|
|
format = self.descriptor.tile_format
|
|
tile_path = os.path.join(level_dir, "%s_%s.%s" % (column, row, format))
|
|
if self.descriptor.tile_format == "jpg":
|
|
jpeg_quality = int(self.image_quality * 100)
|
|
tile.save(tile_path, "JPEG", quality=jpeg_quality)
|
|
else:
|
|
tile.save(tile_path)
|
|
# Create descriptor
|
|
self.descriptor.save(destination)
|
|
|
|
|
|
class CollectionCreator(object):
|
|
"""Creates Deep Zoom collections."""
|
|
|
|
def __init__(
|
|
self,
|
|
image_quality=0.8,
|
|
tile_size=256,
|
|
max_level=7,
|
|
tile_format="jpg",
|
|
copy_metadata=False,
|
|
tile_background_color="#000000",
|
|
):
|
|
self.image_quality = image_quality
|
|
self.tile_size = tile_size
|
|
self.max_level = max_level
|
|
self.tile_format = tile_format
|
|
self.tile_background_color = tile_background_color
|
|
# TODO
|
|
self.copy_metadata = copy_metadata
|
|
|
|
def create(self, images, destination):
|
|
"""Creates a Deep Zoom collection from a list of images."""
|
|
collection = DeepZoomCollection(
|
|
destination,
|
|
image_quality=self.image_quality,
|
|
max_level=self.max_level,
|
|
tile_size=self.tile_size,
|
|
tile_format=self.tile_format,
|
|
tile_background_color=self.tile_background_color,
|
|
)
|
|
for image in images:
|
|
collection.append(image)
|
|
collection.save()
|
|
|
|
|
|
class DZI:
|
|
|
|
def __init__(self, input, output, options) -> None:
|
|
# Normalize the paths to ensure consistency
|
|
image_path = os.path.normpath(input)
|
|
output_path = os.path.normpath(output)
|
|
log = options.get("log_message")
|
|
log(image_path)
|
|
log(output_path)
|
|
# Create Deep Zoom Image creator with weird parameters
|
|
creator = ImageCreator(
|
|
tile_size=254,
|
|
tile_overlap=1,
|
|
tile_format="png",
|
|
image_quality=1,
|
|
)
|
|
|
|
# Create Deep Zoom image pyramid from source
|
|
creator.create(image_path, output_path)
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
def retry(attempts, backoff=2):
|
|
"""Retries a function or method until it returns or
|
|
the number of attempts has been reached."""
|
|
|
|
if backoff <= 1:
|
|
raise ValueError("backoff must be greater than 1")
|
|
|
|
attempts = int(math.floor(attempts))
|
|
if attempts < 0:
|
|
raise ValueError("attempts must be 0 or greater")
|
|
|
|
def deco_retry(f):
|
|
def f_retry(*args, **kwargs):
|
|
last_exception = None
|
|
for _ in range(attempts):
|
|
try:
|
|
return f(*args, **kwargs)
|
|
except Exception as exception:
|
|
last_exception = exception
|
|
time.sleep(backoff ** (attempts + 1))
|
|
raise last_exception
|
|
|
|
return f_retry
|
|
|
|
return deco_retry
|
|
|
|
|
|
def _get_or_create_path(path):
|
|
if not os.path.exists(path):
|
|
os.makedirs(path)
|
|
return path
|
|
|
|
|
|
def _clamp(val, min, max):
|
|
if val < min:
|
|
return min
|
|
elif val > max:
|
|
return max
|
|
return val
|
|
|
|
|
|
def _get_files_path(path):
|
|
return os.path.splitext(path)[0] + "_files"
|
|
|
|
|
|
def _remove(path):
|
|
os.remove(path)
|
|
tiles_path = _get_files_path(path)
|
|
shutil.rmtree(tiles_path)
|
|
|
|
|
|
@retry(3)
|
|
def safe_open(path):
|
|
# `urllib` in Python 2 supported both local paths as well as URLs. To
|
|
# continue this in Python 3, we manually add `file://` prefix if `path` is
|
|
# not a URL. This change is isolated to this function as we want the output
|
|
# XML to still have the original input paths instead of absolute paths:
|
|
has_scheme = bool(urlparse(path).scheme)
|
|
normalized_path = ("file://%s" % os.path.abspath(path)) if not has_scheme else path
|
|
return io.BytesIO(path)
|
|
|
|
|
|
################################################################################
|
|
|
|
|
|
def main():
|
|
parser = optparse.OptionParser(usage="Usage: %prog [options] filename")
|
|
|
|
parser.add_option(
|
|
"-d",
|
|
"--destination",
|
|
dest="destination",
|
|
help="Set the destination of the output.",
|
|
)
|
|
parser.add_option(
|
|
"-s",
|
|
"--tile_size",
|
|
dest="tile_size",
|
|
type="int",
|
|
default=254,
|
|
help="Size of the tiles. Default: 254",
|
|
)
|
|
parser.add_option(
|
|
"-f",
|
|
"--tile_format",
|
|
dest="tile_format",
|
|
default=DEFAULT_IMAGE_FORMAT,
|
|
help="Image format of the tiles (jpg or png). Default: jpg",
|
|
)
|
|
parser.add_option(
|
|
"-o",
|
|
"--tile_overlap",
|
|
dest="tile_overlap",
|
|
type="int",
|
|
default=1,
|
|
help="Overlap of the tiles in pixels (0-10). Default: 1",
|
|
)
|
|
parser.add_option(
|
|
"-q",
|
|
"--image_quality",
|
|
dest="image_quality",
|
|
type="float",
|
|
default=0.8,
|
|
help="Quality of the image output (0-1). Default: 0.8",
|
|
)
|
|
parser.add_option(
|
|
"-r",
|
|
"--resize_filter",
|
|
dest="resize_filter",
|
|
default=DEFAULT_RESIZE_FILTER,
|
|
help="Type of filter for resizing (bicubic, nearest, bilinear, antialias (best). Default: antialias",
|
|
)
|
|
|
|
(options, args) = parser.parse_args()
|
|
|
|
if not args:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
source = args[0]
|
|
|
|
if not options.destination:
|
|
if os.path.exists(source):
|
|
options.destination = os.path.splitext(source)[0] + ".dzi"
|
|
else:
|
|
options.destination = os.path.splitext(os.path.basename(source))[0] + ".dzi"
|
|
if options.resize_filter and options.resize_filter in RESIZE_FILTERS:
|
|
options.resize_filter = RESIZE_FILTERS[options.resize_filter]
|
|
|
|
creator = ImageCreator(
|
|
tile_size=options.tile_size,
|
|
tile_format=options.tile_format,
|
|
image_quality=options.image_quality,
|
|
resize_filter=options.resize_filter,
|
|
)
|
|
creator.create(source, options.destination)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|