before custom

This commit is contained in:
2024-07-14 16:16:40 +02:00
parent 0702b93405
commit d7b8f65627
10 changed files with 789 additions and 101 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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)

View File

@@ -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
View 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()

View File

@@ -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")

View File

@@ -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):
# """ # """