before custom
This commit is contained in:
@@ -12,7 +12,8 @@ from cryptography.fernet import Fernet
|
|||||||
import requests
|
import requests
|
||||||
from woocommerce import API
|
from woocommerce import API
|
||||||
from utils.image_processing import ImageProcessor
|
from utils.image_processing import ImageProcessor
|
||||||
|
from config.encrypt_config import ConfigEncryptor
|
||||||
|
from utils.file_operations import FileProcessor
|
||||||
CREDENTIALS_FILE = "credentials.json"
|
CREDENTIALS_FILE = "credentials.json"
|
||||||
|
|
||||||
# Hardcoded key (replace with your generated key)
|
# Hardcoded key (replace with your generated key)
|
||||||
@@ -37,11 +38,8 @@ def save_credentials(url, consumer_key, consumer_secret, username, password):
|
|||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
}
|
}
|
||||||
credentials_str = json.dumps(credentials)
|
|
||||||
fernet = Fernet(KEY)
|
ConfigEncryptor(KEY).save_credentials(credentials)
|
||||||
encrypted = fernet.encrypt(credentials_str.encode())
|
|
||||||
with open("config.enc", "wb") as file:
|
|
||||||
file.write(encrypted)
|
|
||||||
|
|
||||||
|
|
||||||
def load_credentials():
|
def load_credentials():
|
||||||
@@ -230,7 +228,7 @@ def update_product(image_ids, product_id):
|
|||||||
f"Failed to update product with ID {product_id}. Error: {response.text}")
|
f"Failed to update product with ID {product_id}. Error: {response.text}")
|
||||||
|
|
||||||
|
|
||||||
def process_product_images(product_id, name_template, canvas_width, canvas_height):
|
def process_product_images( options):
|
||||||
"""
|
"""
|
||||||
Process images for a WooCommerce product by resizing and uploading them.
|
Process images for a WooCommerce product by resizing and uploading them.
|
||||||
|
|
||||||
@@ -240,7 +238,9 @@ def process_product_images(product_id, name_template, canvas_width, canvas_heigh
|
|||||||
canvas_width (int): The width of the canvas for resizing images.
|
canvas_width (int): The width of the canvas for resizing images.
|
||||||
canvas_height (int): The height of the canvas for resizing images.
|
canvas_height (int): The height of the canvas for resizing images.
|
||||||
"""
|
"""
|
||||||
print(name_template)
|
product_id = options.get("product_id")
|
||||||
|
if not product_id:
|
||||||
|
return
|
||||||
image_paths, product = get_product(product_id)
|
image_paths, product = get_product(product_id)
|
||||||
if not image_paths:
|
if not image_paths:
|
||||||
return
|
return
|
||||||
@@ -252,15 +252,11 @@ def process_product_images(product_id, name_template, canvas_width, canvas_heigh
|
|||||||
new_list = []
|
new_list = []
|
||||||
|
|
||||||
for image_id, file_path in image_paths.items():
|
for image_id, file_path in image_paths.items():
|
||||||
output_path = generate_output_path(
|
file = FileProcessor()
|
||||||
temp_output_directory,
|
img = ImageProcessor()
|
||||||
file_path,
|
output_path = file.generate_output_path(temp_output_directory, file_path, options, product)
|
||||||
name_template,
|
|
||||||
product,
|
img.resize_image(file_path, output_path, options)
|
||||||
canvas_width,
|
|
||||||
canvas_height,
|
|
||||||
)
|
|
||||||
resize_image(file_path, output_path, "")
|
|
||||||
new_id = upload_image(output_path)
|
new_id = upload_image(output_path)
|
||||||
if new_id:
|
if new_id:
|
||||||
old_list.append(image_id)
|
old_list.append(image_id)
|
||||||
@@ -293,14 +289,13 @@ def generate_output_path(
|
|||||||
sku = product.get("sku", "")
|
sku = product.get("sku", "")
|
||||||
slug = product.get("name", "")
|
slug = product.get("name", "")
|
||||||
title = product.get("slug", "")
|
title = product.get("slug", "")
|
||||||
pprint.pprint(product)
|
|
||||||
new_filename = template.format(
|
new_filename = template.format(
|
||||||
name=name, sku=sku, width=width, height=height, slug=slug, title=title
|
name=name, sku=sku, width=width, height=height, slug=slug, title=title
|
||||||
)
|
)
|
||||||
return os.path.join(temp_output_directory, new_filename + ext)
|
return os.path.join(temp_output_directory, new_filename + ext)
|
||||||
|
|
||||||
|
|
||||||
def process_all_products(name_template, canvas_width, canvas_height):
|
def process_all_products(options):
|
||||||
"""
|
"""
|
||||||
Process images for all WooCommerce products by resizing and uploading them.
|
Process images for all WooCommerce products by resizing and uploading them.
|
||||||
|
|
||||||
@@ -321,8 +316,9 @@ def process_all_products(name_template, canvas_width, canvas_height):
|
|||||||
break
|
break
|
||||||
|
|
||||||
for product in products:
|
for product in products:
|
||||||
|
options["product_id"] = product["id"]
|
||||||
process_product_images(
|
process_product_images(
|
||||||
product["id"], name_template, canvas_width, canvas_height
|
options
|
||||||
)
|
)
|
||||||
|
|
||||||
page += 1
|
page += 1
|
||||||
|
|||||||
@@ -58,6 +58,5 @@ if __name__ == "__main__":
|
|||||||
decryptor = ConfigDecryptor(DECRYPTION_KEY)
|
decryptor = ConfigDecryptor(DECRYPTION_KEY)
|
||||||
try:
|
try:
|
||||||
config = decryptor.decrypt()
|
config = decryptor.decrypt()
|
||||||
print(config)
|
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|||||||
@@ -1,53 +1,77 @@
|
|||||||
"""
|
|
||||||
Module for encrypting configuration files using Fernet symmetric encryption.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class ConfigEncryptor:
|
class ConfigEncryptor:
|
||||||
"""
|
def __init__(self, key, filename="config.enc"):
|
||||||
Class to handle encryption of configuration data.
|
self.key = key
|
||||||
"""
|
self.filename = filename
|
||||||
|
self.fernet = Fernet(self.key)
|
||||||
def __init__(self):
|
|
||||||
"""
|
|
||||||
Initialize the ConfigEncryptor with a generated encryption key.
|
|
||||||
"""
|
|
||||||
self.key = Fernet.generate_key()
|
|
||||||
|
|
||||||
def encrypt_config(self, data):
|
def encrypt_config(self, data):
|
||||||
"""
|
json_data = json.dumps(data)
|
||||||
Encrypt the configuration data and save it to 'config.enc'.
|
encrypted_data = self.fernet.encrypt(json_data.encode())
|
||||||
|
with open(self.filename, "wb") as encrypted_file:
|
||||||
Args:
|
encrypted_file.write(encrypted_data)
|
||||||
data (str): The configuration data to be encrypted.
|
|
||||||
"""
|
|
||||||
fernet = Fernet(self.key)
|
|
||||||
encrypted = fernet.encrypt(data.encode())
|
|
||||||
with open("config.enc", "wb") as encrypted_file:
|
|
||||||
encrypted_file.write(encrypted)
|
|
||||||
|
|
||||||
def get_key(self):
|
def get_key(self):
|
||||||
"""
|
|
||||||
Get the generated encryption key.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: The generated encryption key as a string.
|
|
||||||
"""
|
|
||||||
return self.key.decode()
|
return self.key.decode()
|
||||||
|
|
||||||
|
def save_credentials(self, credentials):
|
||||||
|
config = self.load_config()
|
||||||
|
if not config:
|
||||||
|
config = {"credentials": {}, "options": {}}
|
||||||
|
config["credentials"] = credentials
|
||||||
|
self.encrypt_config(config)
|
||||||
|
|
||||||
|
def save_options(self, options):
|
||||||
|
config = self.load_config()
|
||||||
|
if not config:
|
||||||
|
config = {"credentials": {}, "options": {}}
|
||||||
|
# Ensure options only contains serializable data
|
||||||
|
serializable_options = {k: v for k, v in options.items() if self.is_json_serializable(v)}
|
||||||
|
config["options"] = serializable_options
|
||||||
|
self.encrypt_config(config)
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
try:
|
||||||
|
with open(self.filename, "rb") as encrypted_file:
|
||||||
|
encrypted_data = encrypted_file.read()
|
||||||
|
decrypted_data = self.fernet.decrypt(encrypted_data).decode()
|
||||||
|
return json.loads(decrypted_data)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_json_serializable(value):
|
||||||
|
try:
|
||||||
|
json.dumps(value)
|
||||||
|
return True
|
||||||
|
except (TypeError, OverflowError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Define your key here
|
||||||
|
# Replace with your actual key
|
||||||
|
key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI="
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
CONFIG_DATA = """
|
config_data = {
|
||||||
{
|
"credentials": {
|
||||||
"url": "https://yourstore.com",
|
"url": "https://yourstore.com",
|
||||||
"consumer_key": "ck_yourconsumerkey",
|
"consumer_key": "ck_yourconsumerkey",
|
||||||
"consumer_secret": "cs_yoursecret",
|
"consumer_secret": "cs_yoursecret",
|
||||||
"username": "yourusername",
|
"username": "yourusername",
|
||||||
"password": "yourpassword"
|
"password": "yourpassword"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"canvas_width": 900,
|
||||||
|
"canvas_height": 900,
|
||||||
|
"template": "{slug}_{sku}_{width}x{height}",
|
||||||
|
"delete_images": False,
|
||||||
|
"background_color": "#FFFFFF"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"""
|
encryptor = ConfigEncryptor(key)
|
||||||
encryptor = ConfigEncryptor()
|
encryptor.encrypt_config(config_data)
|
||||||
print(f"Encryption key: {encryptor.get_key()}")
|
|
||||||
encryptor.encrypt_config(CONFIG_DATA)
|
|
||||||
|
|||||||
5
main.py
5
main.py
@@ -4,6 +4,7 @@ Main module for the Image Processor application.
|
|||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
import customtkinter
|
||||||
from ui.log_window import LogWindow
|
from ui.log_window import LogWindow
|
||||||
from ui.local_processing_tab import LocalProcessingTab
|
from ui.local_processing_tab import LocalProcessingTab
|
||||||
from ui.settings_tab import SettingsTab
|
from ui.settings_tab import SettingsTab
|
||||||
@@ -24,7 +25,7 @@ class ImageProcessorApp:
|
|||||||
"""
|
"""
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("Image Processor")
|
self.root.title("Image Processor")
|
||||||
self.root.geometry("700x400")
|
self.root.geometry("500x450")
|
||||||
|
|
||||||
self.tab_parent = ttk.Notebook(self.root)
|
self.tab_parent = ttk.Notebook(self.root)
|
||||||
self.log_window = None
|
self.log_window = None
|
||||||
@@ -64,6 +65,6 @@ if __name__ == "__main__":
|
|||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
print(f"File not found: {e}")
|
print(f"File not found: {e}")
|
||||||
|
|
||||||
root = tk.Tk()
|
root = customtkinter.CTk()
|
||||||
app = ImageProcessorApp(root)
|
app = ImageProcessorApp(root)
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from utils.image_processing import ImageProcessor
|
|||||||
from api.woocommerce_api import process_product_images, process_all_products
|
from api.woocommerce_api import process_product_images, process_all_products
|
||||||
from ui.options_window import OptionsWindow
|
from ui.options_window import OptionsWindow
|
||||||
from pprint import pformat, pprint
|
from pprint import pformat, pprint
|
||||||
|
from config.encrypt_config import ConfigEncryptor
|
||||||
|
|
||||||
class LocalProcessingTab:
|
class LocalProcessingTab:
|
||||||
"""
|
"""
|
||||||
@@ -30,11 +30,13 @@ class LocalProcessingTab:
|
|||||||
text (str): The text to display on the tab.
|
text (str): The text to display on the tab.
|
||||||
log (function): The function to log messages.
|
log (function): The function to log messages.
|
||||||
"""
|
"""
|
||||||
|
key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI="
|
||||||
|
|
||||||
self.log = log
|
self.log = log
|
||||||
self.tab = ttk.Frame(tab_parent)
|
self.tab = ttk.Frame(tab_parent)
|
||||||
self.root = self.tab.winfo_toplevel() # Store the root window reference
|
self.root = self.tab.winfo_toplevel() # Store the root window reference
|
||||||
tab_parent.add(self.tab, text=text)
|
tab_parent.add(self.tab, text=text)
|
||||||
|
self.config = ConfigEncryptor(key)
|
||||||
self.log_window = None
|
self.log_window = None
|
||||||
|
|
||||||
self.canvas_width = 900
|
self.canvas_width = 900
|
||||||
@@ -45,7 +47,7 @@ class LocalProcessingTab:
|
|||||||
self.background_color = "#000000"
|
self.background_color = "#000000"
|
||||||
self.image_format = "AUTO"
|
self.image_format = "AUTO"
|
||||||
self.image_size = "contain"
|
self.image_size = "contain"
|
||||||
|
self.load_config()
|
||||||
self.source_type = StringVar(value="local")
|
self.source_type = StringVar(value="local")
|
||||||
self.checkbox_var = BooleanVar(value=False)
|
self.checkbox_var = BooleanVar(value=False)
|
||||||
self.file = FileProcessor()
|
self.file = FileProcessor()
|
||||||
@@ -55,6 +57,21 @@ class LocalProcessingTab:
|
|||||||
self.setup_ui()
|
self.setup_ui()
|
||||||
self.update_options()
|
self.update_options()
|
||||||
|
|
||||||
|
def load_config(self):
|
||||||
|
|
||||||
|
config = self.config.load_config()
|
||||||
|
if config:
|
||||||
|
if options := config.get("options"):
|
||||||
|
self.canvas_width = options.get("canvas_width", 900)
|
||||||
|
self.canvas_height = options.get("canvas_height", 900)
|
||||||
|
self.template = options.get("template", "{slug}_{sku}_{width}x{height}")
|
||||||
|
self.delete_images = options.get("delete_images", False)
|
||||||
|
self.transparent = options.get("transparent", True)
|
||||||
|
self.background_color = options.get("background_color", "#000000")
|
||||||
|
self.image_format = options.get("image_format", "AUTO")
|
||||||
|
self.image_size = options.get("image_size", "contain")
|
||||||
|
|
||||||
|
|
||||||
def create_log_window(self):
|
def create_log_window(self):
|
||||||
"""
|
"""
|
||||||
Create and display the log window.
|
Create and display the log window.
|
||||||
@@ -127,7 +144,7 @@ class LocalProcessingTab:
|
|||||||
self.options_button = ttk.Button(
|
self.options_button = ttk.Button(
|
||||||
self.tab, text="Options", command=self.open_options_window
|
self.tab, text="Options", command=self.open_options_window
|
||||||
)
|
)
|
||||||
self.options_button.grid(row=4, column=0, padx=5, pady=5, sticky="w")
|
self.options_button.grid(row=2, column=3, columnspan=2, padx=5, pady=5, sticky="w")
|
||||||
|
|
||||||
self.button_start = Button(
|
self.button_start = Button(
|
||||||
self.tab, text="Start Processing", command=self.start_processing
|
self.tab, text="Start Processing", command=self.start_processing
|
||||||
@@ -251,7 +268,6 @@ class LocalProcessingTab:
|
|||||||
"""
|
"""
|
||||||
Apply the canvas size settings and update previews.
|
Apply the canvas size settings and update previews.
|
||||||
"""
|
"""
|
||||||
pprint(self.image_size)
|
|
||||||
self.image.set_image_size(self.image_size)
|
self.image.set_image_size(self.image_size)
|
||||||
|
|
||||||
def apply_background_color(self):
|
def apply_background_color(self):
|
||||||
@@ -261,7 +277,7 @@ class LocalProcessingTab:
|
|||||||
self.image.set_background_color(self.background_color)
|
self.image.set_background_color(self.background_color)
|
||||||
|
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Get the current processing options.
|
Get the current processing options.
|
||||||
|
|
||||||
@@ -321,7 +337,7 @@ class LocalProcessingTab:
|
|||||||
"image_format": {
|
"image_format": {
|
||||||
"type": "dropdown",
|
"type": "dropdown",
|
||||||
"label": "Image Format:",
|
"label": "Image Format:",
|
||||||
"options": ["AUTO", "JPEG", "PNG", "GIF"],
|
"options": ["AUTO", "JPEG", "PNG", "GIF", "DZI"],
|
||||||
"default": self.image_format
|
"default": self.image_format
|
||||||
},
|
},
|
||||||
"image_size": {
|
"image_size": {
|
||||||
@@ -351,10 +367,12 @@ class LocalProcessingTab:
|
|||||||
self.delete_images = options["delete_images"]
|
self.delete_images = options["delete_images"]
|
||||||
self.background_color = options["background_color"]
|
self.background_color = options["background_color"]
|
||||||
self.image_size = options["image_size"]
|
self.image_size = options["image_size"]
|
||||||
|
self.image_format = options["image_format"]
|
||||||
self.apply_canvas_size()
|
self.apply_canvas_size()
|
||||||
self.apply_background_color()
|
self.apply_background_color()
|
||||||
self.apply_image_size()
|
self.apply_image_size()
|
||||||
|
key = b"u4xTBY5Ns4WYdLvqMjEr138mpMmDEhhqTszKCcDy2cI="
|
||||||
|
self.config.save_options(self.get_options())
|
||||||
self.update_previews()
|
self.update_previews()
|
||||||
|
|
||||||
def pprint_log_message(self, obj):
|
def pprint_log_message(self, obj):
|
||||||
@@ -375,31 +393,18 @@ class LocalProcessingTab:
|
|||||||
source = self.source_type.get()
|
source = self.source_type.get()
|
||||||
options = self.get_options()
|
options = self.get_options()
|
||||||
|
|
||||||
# Log the current settings
|
|
||||||
self.pprint_log_message(options)
|
|
||||||
|
|
||||||
if source == "local":
|
if source == "local":
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self.file.process_directory_with_logging, args=(
|
target=self.file.process_directory_with_logging, args=(options,)
|
||||||
options,)
|
|
||||||
).start()
|
).start()
|
||||||
elif source == "product":
|
elif source == "product":
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=process_product_images,
|
target=process_product_images,
|
||||||
args=(
|
args=(options,)
|
||||||
options["product_id"],
|
|
||||||
options["canvas_width"],
|
|
||||||
options["canvas_height"],
|
|
||||||
options["log_message"],
|
|
||||||
),
|
|
||||||
).start()
|
).start()
|
||||||
elif source == "all_products":
|
elif source == "all_products":
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=process_all_products,
|
target=process_all_products,
|
||||||
args=(
|
args=(options,)
|
||||||
options["canvas_width"],
|
|
||||||
options["canvas_height"],
|
|
||||||
options["log_message"],
|
|
||||||
),
|
|
||||||
).start()
|
).start()
|
||||||
self.update_previews()
|
self.update_previews()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ class OptionsWindow(tk.Toplevel):
|
|||||||
def __init__(self, parent, apply_callback, current_options):
|
def __init__(self, parent, apply_callback, current_options):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.title("Options")
|
self.title("Options")
|
||||||
self.geometry("400x500")
|
self.geometry("500x500")
|
||||||
|
|
||||||
self.apply_callback = apply_callback
|
self.apply_callback = apply_callback
|
||||||
self.options = current_options
|
self.options = current_options
|
||||||
@@ -149,7 +149,7 @@ class OptionsWindow(tk.Toplevel):
|
|||||||
default (str): The default color.
|
default (str): The default color.
|
||||||
"""
|
"""
|
||||||
if default == "transparent":
|
if default == "transparent":
|
||||||
default = "#00000000"
|
default = "#ffffff"
|
||||||
var = tk.BooleanVar(value=True)
|
var = tk.BooleanVar(value=True)
|
||||||
else:
|
else:
|
||||||
var = tk.BooleanVar(value=False)
|
var = tk.BooleanVar(value=False)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import tkinter as tk
|
|||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
from api.woocommerce_api import save_credentials, load_credentials
|
from api.woocommerce_api import save_credentials, load_credentials
|
||||||
|
|
||||||
|
|
||||||
class SettingsTab:
|
class SettingsTab:
|
||||||
def __init__(self, tab_parent, text):
|
def __init__(self, tab_parent, text):
|
||||||
self.tab = ttk.Frame(tab_parent)
|
self.tab = ttk.Frame(tab_parent)
|
||||||
|
|||||||
651
utils/deepzoom.py
Normal file
651
utils/deepzoom.py
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
DEFAULT_RESIZE_FILTER = PIL.Image.ANTIALIAS
|
||||||
|
DEFAULT_IMAGE_FORMAT = "jpg"
|
||||||
|
|
||||||
|
RESIZE_FILTERS = {
|
||||||
|
"cubic": PIL.Image.CUBIC,
|
||||||
|
"bilinear": PIL.Image.BILINEAR,
|
||||||
|
"bicubic": PIL.Image.BICUBIC,
|
||||||
|
"nearest": PIL.Image.NEAREST,
|
||||||
|
"antialias": PIL.Image.ANTIALIAS,
|
||||||
|
}
|
||||||
|
|
||||||
|
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), PIL.Image.ANTIALIAS
|
||||||
|
)
|
||||||
|
# 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), PIL.Image.ANTIALIAS)
|
||||||
|
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), PIL.Image.ANTIALIAS)
|
||||||
|
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()
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from tkinter import filedialog, messagebox
|
from tkinter import filedialog, messagebox
|
||||||
from utils.image_processing import ImageProcessor
|
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
|
from utils.deepzoom import DZI
|
||||||
|
|
||||||
|
|
||||||
class FileProcessor:
|
class FileProcessor:
|
||||||
"""
|
"""
|
||||||
@@ -133,9 +134,11 @@ class FileProcessor:
|
|||||||
options (dict): Processing options.
|
options (dict): Processing options.
|
||||||
log (function): The log function to use.
|
log (function): The log function to use.
|
||||||
"""
|
"""
|
||||||
|
from utils.image_processing import ImageProcessor
|
||||||
image = ImageProcessor()
|
image = ImageProcessor()
|
||||||
image.set_background_color(options.get("background_color", "transparent"))
|
image.set_background_color(options.get("background_color", "transparent"))
|
||||||
image.set_image_size(options.get("image_size", "contain"))
|
image.set_image_size(options.get("image_size", "contain"))
|
||||||
|
format = options.get("image_format")
|
||||||
for file_path in image_paths:
|
for file_path in image_paths:
|
||||||
# output_path = os.path.join(
|
# output_path = os.path.join(
|
||||||
# output_directory, os.path.relpath(
|
# output_directory, os.path.relpath(
|
||||||
@@ -144,9 +147,12 @@ class FileProcessor:
|
|||||||
output_path = self.generate_output_path(output_directory, file_path, options)
|
output_path = self.generate_output_path(output_directory, file_path, options)
|
||||||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
self.log_message(f"Running: {file_path}", log)
|
self.log_message(f"Running: {file_path}", log)
|
||||||
image.resize_image(
|
if format == "DZI":
|
||||||
file_path, output_path, options
|
DZI(file_path, output_path, options)
|
||||||
)
|
else:
|
||||||
|
image.resize_image(
|
||||||
|
file_path, output_path, options
|
||||||
|
)
|
||||||
|
|
||||||
if os.path.exists(file_path) and options.get("delete_images", False):
|
if os.path.exists(file_path) and options.get("delete_images", False):
|
||||||
self.log_message(f"Removing: {file_path}", log)
|
self.log_message(f"Removing: {file_path}", log)
|
||||||
@@ -173,5 +179,14 @@ class FileProcessor:
|
|||||||
new_filename = options.get('template', '{name}').format(
|
new_filename = options.get('template', '{name}').format(
|
||||||
name=name, sku=sku, width=width, height=height, slug=slug, title=title
|
name=name, sku=sku, width=width, height=height, slug=slug, title=title
|
||||||
)
|
)
|
||||||
pprint(new_filename)
|
imgf = options.get("image_format", "AUTO")
|
||||||
return os.path.join(output_directory, new_filename + ext)
|
if imgf == "AUTO":
|
||||||
|
return os.path.join(output_directory, new_filename + ext)
|
||||||
|
elif imgf == "GIF":
|
||||||
|
return os.path.join(output_directory, new_filename + ".gif")
|
||||||
|
elif imgf == "PNG":
|
||||||
|
return os.path.join(output_directory, new_filename + ".png")
|
||||||
|
elif imgf == "JPEG":
|
||||||
|
return os.path.join(output_directory, new_filename + ".jpg")
|
||||||
|
elif imgf == "DZI":
|
||||||
|
return os.path.join(output_directory, new_filename + ".dzi")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import os
|
|||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
from wand.color import Color
|
from wand.color import Color
|
||||||
|
|
||||||
|
|
||||||
class ImageProcessor:
|
class ImageProcessor:
|
||||||
def __init__(self, canvas_width=900, canvas_height=900, background_color="transparent", image_size="fit"):
|
def __init__(self, canvas_width=900, canvas_height=900, background_color="transparent", image_size="fit"):
|
||||||
"""
|
"""
|
||||||
@@ -13,6 +12,7 @@ class ImageProcessor:
|
|||||||
self.background_color = Color(background_color)
|
self.background_color = Color(background_color)
|
||||||
self.image_size = image_size
|
self.image_size = image_size
|
||||||
|
|
||||||
|
|
||||||
def set_canvas_size(self, width, height):
|
def set_canvas_size(self, width, height):
|
||||||
"""
|
"""
|
||||||
Set the canvas size.
|
Set the canvas size.
|
||||||
@@ -88,8 +88,7 @@ class ImageProcessor:
|
|||||||
|
|
||||||
aspect_ratio_img = img.width / img.height
|
aspect_ratio_img = img.width / img.height
|
||||||
aspect_ratio_canvas = self.canvas_width / self.canvas_height
|
aspect_ratio_canvas = self.canvas_width / self.canvas_height
|
||||||
print(f"Image aspect ratio: {aspect_ratio_img}, Canvas aspect ratio: {aspect_ratio_canvas}")
|
|
||||||
print(f"Cover resized image size: {img.width}x{img.height}")
|
|
||||||
|
|
||||||
|
|
||||||
def _contain(self, img):
|
def _contain(self, img):
|
||||||
@@ -98,13 +97,10 @@ class ImageProcessor:
|
|||||||
"""
|
"""
|
||||||
aspect_ratio_img = img.width / img.height
|
aspect_ratio_img = img.width / img.height
|
||||||
aspect_ratio_canvas = self.canvas_width / self.canvas_height
|
aspect_ratio_canvas = self.canvas_width / self.canvas_height
|
||||||
print(f"Image aspect ratio: {aspect_ratio_img}, Canvas aspect ratio: {aspect_ratio_canvas}")
|
|
||||||
|
|
||||||
if aspect_ratio_img > aspect_ratio_canvas:
|
if aspect_ratio_img > aspect_ratio_canvas:
|
||||||
img.transform(resize=f"{self.canvas_width}x")
|
img.transform(resize=f"{self.canvas_width}x")
|
||||||
else:
|
else:
|
||||||
img.transform(resize=f"x{self.canvas_height}")
|
img.transform(resize=f"x{self.canvas_height}")
|
||||||
print(f"Cover resized image size: {img.width}x{img.height}")
|
|
||||||
|
|
||||||
# def _fit(self, img):
|
# def _fit(self, img):
|
||||||
# """
|
# """
|
||||||
|
|||||||
Reference in New Issue
Block a user