Added tagging and workflow powerpoint

master
Jonathan Chan 2 years ago
parent a227b77ad3
commit d048ea6d43

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -1,21 +1,21 @@
// TO DO
// Create another script that can run
// SCRIPT LOGIC
// for site in sites:
// for year in years:
// register_images()
// VARIABLE DEFINITIONS
// VARIABLE DEFINITIONS
// images_to_register = All images to be registered for a given site and year (no target or seed)
// batch_images_to_register = Batch images to be registered (no target or seed)
// batch_images_all = Target, seed and batch images to be registered
var batch_size = 10;
var batch_size = 15;
// Must use forwardslashes in filepath, not backslashes
var batch_download_csv = File("C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/CoastSnap_Sites.csv")
// retreive site names from batch_download.csv
var csv_data=[];
batch_download_csv.open('r');
@ -26,8 +26,8 @@ while(!batch_download_csv.eof){
batch_download_csv.close();
var site_names = csv_data.toString().split(",")
// Images parent directory
var parent_folder_path = "C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/Images_Test";
// Retrieve images parent directory from CoastSnap_Sites.csv
var parent_folder_path = File(site_names[9]);
var batch_images_to_register = []; // Used in exportLayersToPNG
@ -88,6 +88,7 @@ function batch_register_images(batchIndex, site_path, site_name, images_to_regis
}
stackFiles(batch_images_all);
resizeLayers();
lockTarget();
selectAllLayers();
autoAlign();
@ -126,45 +127,72 @@ function imagesNotRegistered(site_images_all, photoshop_images) {
function stackFiles(sFiles){
var loadLayersFromScript = true;
var SCRIPTS_FOLDER = decodeURI(app.path + '/' + localize('$$$/ScriptingSupport/InstalledScripts=Presets/Scripts'));
$.evalFile( new File(SCRIPTS_FOLDER + '/Load Files into Stack.jsx'));
loadLayers.intoStack(sFiles, false);
};
function resizeLayers() {
var doc = app.activeDocument;
var targetImage = app.activeDocument.artLayers.getByName("Target.jpg")
// Retrieve Target image dimensions
var target_bounds = targetImage.bounds;
var target_width = target_bounds[2] - target_bounds[0];
var target_height = target_bounds[3] - target_bounds[1];
// Update Target Image dimensions
var new_width = 1280;
preferences.rulerUnits = Units.PIXELS;
var s = (new_width/target_width)*100;
targetImage.resize(s, s, AnchorPosition.TOPLEFT)
// Retrieve new Target image dimensions
var target_bounds = targetImage.bounds;
var target_width = target_bounds[2] - target_bounds[0];
var target_height = target_bounds[3] - target_bounds[1];
// Make layers similar dimensions to new Target
var layerName = doc.layers;
for (var ii = 0; ii < layerName.length; ii++) { //looping through all the layers
var current_layer = layerName[ii];
preferences.rulerUnits = Units.PIXELS;
var b = current_layer.bounds;
var layer_width = b[2]-b[0];
var layer_height = b[3]-b[1];
if (layer_height >= layer_width) {
var s = (target_height/layer_height)*100;
}
else {
var s = (target_width/layer_width)*100;
}
current_layer.resize(s, s, AnchorPosition.TOPLEFT)
}
}
function selectAllLayers() {
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') );
desc.putReference( charIDToTypeID('null'), ref );
executeAction( stringIDToTypeID('selectAllLayers'), desc, DialogModes.NO );
};
function autoAlign() {
var desc = new ActionDescriptor();
var ref = new ActionReference();
ref.putEnumerated( charIDToTypeID('Lyr '), charIDToTypeID('Ordn'), charIDToTypeID('Trgt') );
desc.putReference( charIDToTypeID('null'), ref );
desc.putEnumerated( charIDToTypeID('Usng'), charIDToTypeID('ADSt'), stringIDToTypeID('ADSContent') );
desc.putEnumerated( charIDToTypeID('Aply'), stringIDToTypeID('projection'), charIDToTypeID('Auto') );
desc.putBoolean( stringIDToTypeID('vignette'), false );
desc.putBoolean( stringIDToTypeID('radialDistort'), false );
executeAction( charIDToTypeID('Algn'), desc, DialogModes.NO );
@ -292,7 +320,6 @@ function exportLayersToPNG(target_width, target_height, batch_images_to_register
SavePNG(saveFile);
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
}
};
function dupLayers() {
@ -309,10 +336,10 @@ function dupLayers() {
function SavePNG(saveFile){
var pngOpts = new ExportOptionsSaveForWeb;
pngOpts.format = SaveDocumentType.PNG
pngOpts.format = SaveDocumentType.JPEG;
pngOpts.PNG8 = false;
pngOpts.transparency = true;
pngOpts.interlaced = false;
pngOpts.quality = 200;
pngOpts.quality = 100;
activeDocument.exportDocument(new File(saveFile),ExportType.SAVEFORWEB,pngOpts);
}

@ -12,11 +12,11 @@ import attr
import cv2
import numpy as np
import pandas as pd
import typer
from loguru import logger
from moviepy.editor import *
from utils import divide_chunks, datenum_to_datetime, nearest
from utils import divide_chunks, datenum_to_datetime, nearest, progressbar, RegisteredImage
import time # progress bar
from PIL import Image, ImageFont, ImageDraw
import os
@ -27,30 +27,144 @@ import openpyxl
import scipy.io as sio
from datetime import datetime, timedelta
# Enables command-line interface
app = typer.Typer()
#-----------------------------------------------------------------------------#
# Update this file path for Image Tagging - Tide Data
# Example: parent_dir = '/Users/admin/OneDrive - UNSW/My files/CoastSnap/'
parent_dir = r"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap"
parent_dir_ACTUAL = r"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap"
#-----------------------------------------------------------------------------#
# RETRIEVE IMAGES PARENT DIRECTORY IN .csv#
coastsnap_sites = pd.read_csv("C:/Users/z5079346/OneDrive - UNSW/Projects/Coastsnap_test/CoastSnap_Sites.csv")
parent_directory = coastsnap_sites.parent_directory[0]
print(parent_directory)
#-----------------------------------------------------------------------------#
@app.command()
def tagging(
folder: Path = typer.Argument(None, help="Folder with images"),
photoshop: bool = typer.Option(True, help="Have these images been registered with photoshop?"),
tide: bool = typer.Option(False, help="Do you want to add the tide to the image tag?")
):
folder = r"C:\Users\z5079346\OneDrive - UNSW\Projects\Coastsnap_test\Images_Test\cathieillaroo\Photoshop\2021"
'''
For each site in Images Parent Directory:
Iterate through years in reverse:
Iterate through images names (dates) in reverse:
Check if image name in 'Registered'? If not, tag and save
For this step, do image_name.replace('Processed' with 'Registered')
'''
def get_site_tide_data(parent_dir, site):
# Retrieve tide data for the given site
db = openpyxl.load_workbook(parent_dir + "/Database/CoastSnapDB.xlsx")
beach_data = db[site]
tide_filename = beach_data["B24"].value
if tide_filename == 'NO_TIDE.mat':
return False
mat_path = parent_dir + '/Tide Data/' + tide_filename
mat = sio.loadmat(mat_path)
tide_dict = mat['tide']
ts = tide_dict[0][0] # Format of tide/time Matlab data is gross
tt = list(ts[0]) # ts[0] = tides, ts[1] = times, ts[2] =
print(site + ": " +"Loading tide data... (this may take half a minute)")
tide_times = [datenum_to_datetime(i) for i in tt] # THIS STEP TAKES A LONG TIME
return tide_times, ts
def tag_image(image2tag):
image = Image.open(image2tag.pathname)
draw = ImageDraw.Draw(image)
image2tag.get_dimensions()
# White Text Box
rect_height = image2tag.height/20
# Create white text box
draw.rectangle((0, 0, image2tag.width, rect_height), fill='white')
# Tag image with text
draw.text((20, rect_height/4),image2tag.tag, font = image2tag.font, fill=(0, 0, 0))
registered_path_wrong = image2tag.pathname[:-4] + '_registered.jpg'
registered_path = registered_path_wrong.replace('Photoshop', 'Registered')
image = image.convert('RGB')
image.save(registered_path)
for site in os.listdir(parent_directory): # Loop through SITES
i=0
site_complete = False # A flag to stop tagging images when found
tide_data = False
font = None
photoshop_path = parent_directory +'/'+ site + '/Photoshop'
try: # Check if site contains 'Processed' directory
years_list = os.listdir(photoshop_path)
years_list.reverse()
except:
continue
for year in years_list: # Loop through YEARS
if site_complete: break
year_path = photoshop_path + '/' + year
image_list = os.listdir(year_path)
image_list.reverse()
for image_filename in image_list: # Loop through IMAGES
#print(image_filename)
registered_year_path = year_path.replace('Photoshop', 'Registered')
registered_image_path = registered_year_path + '/' + image_filename[:-4] + '_registered.jpg'
if site not in image_filename:
continue
# Check if image already tagged
if os.path.isfile(registered_image_path):
site_complete = True;
if i == 0:
print(site + ": " + str(i) + " images tagged")
elif image2tag.tide:
print(site + ": " + str(i) + " images tagged with tide")
else:
print(site + ": " + str(i) + " images tagged no tide")
break
else:
if not os.path.exists(registered_year_path): # Chech that registered/year directory exists
os.makedirs(registered_year_path) # if not, create it
i += 1
pathname = os.path.join(year_path, image_filename)
image2tag = RegisteredImage(pathname, image_filename) # Create image object
if i == 1: # Retrieve Tide Data once for each site
site_tide_data, ts = get_site_tide_data(parent_dir_ACTUAL, site)
font = image2tag.get_font()
if site_tide_data:
image2tag.get_tide(site_tide_data, ts)
image2tag.create_tag()
tag_image(image2tag)
"""
Tags images based on file name.
- Requires font file in coastsnap/fonts directory
"""
from PIL import Image, ImageFont, ImageDraw # Throws errors if this isn't here..
"""
def tag_images(folder):
photoshop = True
tide = False
# Get image paths
img_paths = [x for x in Path(folder).glob("*.jpg")]
@ -59,7 +173,7 @@ def tagging(
# Initialise white text box
rect_height = 1
width = 1
image_width = 1
# Check whether the directory 'tagged' exists or not
path_name = img_names[0]
@ -69,53 +183,27 @@ def tagging(
# Create a new directory because it does not exist
os.makedirs(tagged_dir)
# # Set Image Label Info Based on Target Image
# target_image_path = str(Path(path_name).parent.parent.parent) + r"\Target Image\Target.JPG"
# print(target_image_path)
# image = Image.open(target_image_path)
# img_fraction = 0.6 # Change this to change the font size
# fontsize = 1
# # White Text Box
# width, height = image.size
# rect_height = height/20
# rect_width = width/1.5
# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
# while font.getsize(txt)[0] < img_fraction*image.size[0]:
# # iterate until the text size is just larger than the criteria
# fontsize += 1
# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Registered\2022"
#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Target Image\Target.JPG"
# Tide Data
if tide:
# Retrieve the site name from the first image
filename = Path(img_names[0]).name
if photoshop == True:
fname = filename[6:-4]
else:
fname = filename
filename_list = fname.split(".")
filename_list = filename.split(".")
site_name = filename_list[6]
# Retrieve tide data for the given site
db = openpyxl.load_workbook(parent_dir + "Database/CoastSnapDB.xlsx")
db = openpyxl.load_workbook(parent_dir + "/Database/CoastSnapDB.xlsx")
beach_data = db[site_name]
tide_filename = beach_data["B24"].value
mat_path = parent_dir + 'Tide Data/' + tide_filename
print("Loading tide data... (this may take half a minute)")
mat_path = parent_dir + '/Tide Data/' + tide_filename
mat = sio.loadmat(mat_path)
tide_dict = mat['tide']
ts = tide_dict[0][0] # Format of tide/time Matlab data is gross
tt = list(ts[0]) # ts[0] = tides, ts[1] = times, ts[2] =
tide_times = [datenum_to_datetime(i) for i in tt]
print("Loading tide data... (this may take half a minute)")
tide_times = [datenum_to_datetime(i) for i in tt] # THIS STEP TAKES A LONG TIME
fontsize = 1
for index, img in enumerate(sorted(img_names)):
@ -126,12 +214,7 @@ def tagging(
# Retrieve tag information from file name
if photoshop == True:
fname = filename[6:-4]
else:
fname = filename
filename_list = fname.split(".")
filename_list = filename.split(".")
posix_time = filename_list[0]
date = filename_list[3].split("_")
hour = date[1]
@ -178,12 +261,12 @@ def tagging(
# Set the fontsize, such that the tag covers 50% the width of the first image
if index == 0:
img_fraction = 0.6 # Change this to change the font size
img_fraction = 0.8 # Change this to change the font size
# White Text Box
width, height = image.size
rect_height = height/20
rect_width = width/1.5
image_width, image_height = image.size
rect_height = image_height/20
rect_width = image_width/1.5
font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
while font.getsize(txt)[0] < img_fraction*image.size[0]:
@ -193,18 +276,43 @@ def tagging(
font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
# Create white text box
draw.rectangle((0, 0, width, rect_height), fill='white')
draw.rectangle((0, 0, image_width, rect_height), fill='white')
# Tag image with text
draw.text((20, rect_height/4),txt, font = font, fill=(0, 0, 0))
new_name = fname[:-4] + '_registered.jpg'
new_name = filename + '_registered.jpg'
print(new_name + '\n')
new_path = tagged_dir + "/" + new_name
image = image.convert('RGB')
image.save(new_path)
logger.info(f"Tagged Images Saved")
logger.info(f"Tagged Images for " + + " Saved")
if __name__ == "__main__":
app()
tag_images(folder)
"""
# # Set Image Label Info Based on Target Image (PUT AT LINE 90)
# target_image_path = str(Path(path_name).parent.parent.parent) + r"\Target Image\Target.JPG"
# print(target_image_path)
# image = Image.open(target_image_path)
# img_fraction = 0.6 # Change this to change the font size
# fontsize = 1
# # White Text Box
# width, height = image.size
# rect_height = height/20
# rect_width = width/1.5
# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
# while font.getsize(txt)[0] < img_fraction*image.size[0]:
# # iterate until the text size is just larger than the criteria
# fontsize += 1
# font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Registered\2022"
#"C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images\cathieillaroo\Target Image\Target.JPG"

@ -0,0 +1,138 @@
import numpy as np
from datetime import datetime, timedelta
import sys
from time import strptime
from PIL import Image, ImageFont
def divide_chunks(l, n):
"""
Splits a list into chunks of length n. Used to process the images in chunks.
"""
for i in range(0, len(l), n):
yield l[i : i + n]
# Sourced from https://gist.github.com/victorkristof/b9d794fe1ed12e708b9d
def datenum_to_datetime(datenum):
"""
Convert Matlab datenum into Python datetime.
:param datenum: Date in datenum format
:return: Datetime object corresponding to datenum.
"""
days = datenum % 1
hours = days % 1 * 24
minutes = hours % 1 * 60
seconds = np.round(minutes % 1 * 60)
return datetime.fromordinal(int(datenum)) \
+ timedelta(days=int(days)) \
+ timedelta(hours=int(hours)) \
+ timedelta(minutes=int(minutes)) \
+ timedelta(seconds=int(seconds)) \
- timedelta(days=366)
# Sourced from https://stackoverflow.com/questions/32237862/find-the-closest-date-to-a-given-date
def nearest(items, pivot):
"""
This function will return the datetime in items
which is the closest to the date pivot
"""
return min(items, key=lambda x: abs(x - pivot))
def progressbar(it, prefix="", size=60, out=sys.stdout): # Python3.3+
count = len(it)
def show(j):
x = int(size*j/count)
print("{}[{}{}] {}/{}".format(prefix, u"#"*x, "."*(size-x), j, count),
end='\r', file=out, flush=True)
show(0)
for i, item in enumerate(it):
yield item
show(i+1)
print("\n", flush=True, file=out)
class RegisteredImage():
def __init__(self, pathname, filename):
self.pathname = pathname
self.filename = filename
filename_list = filename.split(".")
date = filename_list[3].split("_")
if 'snap' in filename_list:
self.contributor = filename_list[8] # Mitch filename format
else:
self.contributor = filename_list[6] # Leaman filename format
self.site = filename_list[6]
self.posix_time = filename_list[0]
self.timezone = filename_list[4]
self.year = filename_list[5]
self.month = '{:02d}'.format(strptime(filename_list[2],'%b').tm_mon) # Ensure 2-digit format
self.day = date[0]
self.hour = date[1]
self.minute = date[2]
self.second = date[3]
self.tide = None
self.tag = None
self.width = None
self.height = None
self.font = None
def get_tide(self, site_tide_data, ts):
# Account for daylight savings
# ASSUMPTION: All .mat tide files are either AEST or AEDT
if self.timezone == 'AEDT':
self.hour = str(int(self.hour) - 1)
date_string = self.year + '-' + self.month + '-' + self.day + ' ' + self.hour + ':' + self.minute + ':' + self.second
img_datetime = datetime.strptime(date_string, "%Y-%m-%d %H:%M:%S") # Image date/time as a datetime object
tide_date = nearest(site_tide_data, img_datetime)
mat_index = site_tide_data.index(tide_date) # Retrieve the index of the .mat tide/time
mat_tide = round(ts[1][mat_index][0], 2) # Associated tide
#print('Image date/time: ' + date_string)
#print('Tide record: ' + str(tide_date))
self.tide = "{:.2f}".format(mat_tide)
def create_tag(self):
# Create image tag
hour = self.hour.zfill(2)
minute = self.minute.zfill(2)
if self.tide:
tide = self.tide
if float(self.tide) >= 0:
tide = '+' + self.tide
self.tag = ('Date:' + self.year + '/' + self.month + '/' + self.day +
' Time:' + hour + ':' + minute +
' Tide:' + tide + 'm AHD' +
' Contributor:' + self.contributor)
else:
self.tag = ('Date:' + self.year + '/' + self.month + '/' + self.day +
' Time:' + hour + ':' + minute +
' Contributor:' + self.contributor)
def get_dimensions(self):
image = Image.open(self.pathname)
# White Text Box
self.width, self.height = image.size
def get_font(self):
image = Image.open(self.pathname)
fontsize = 1
if self.tide:
img_fraction = 0.95
else:
img_fraction = 0.85
font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
generic_large_tag = "1641775682.Mon.Jan.10_11_48_02.AEDT.2022.byron.snap.KateThornborough123"
while font.getsize(generic_large_tag)[0] < img_fraction*image.size[0]:
# iterate until the text size is just larger than the criteria
fontsize += 1
font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)
self.font = ImageFont.truetype("fonts/Courier New Bold.ttf", fontsize)

@ -8,6 +8,7 @@ dependencies:
- opencv
- numpy
- loguru
- typer
- attrs
- pandas
- black

@ -0,0 +1,38 @@
site_name,root_id,limit,type,Registration Capacity,Comment,parent_directory
alex,487451,300,CoastSnap Station,Good,Hand rail in foreground,C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images
birubi,548070,300,CoastSnap Station,Good,Structures in foreground,
blacksmiths,269988,300,CoastSnap Station,Good,,
broulee,269990,300,CoastSnap Station,Good,Bad images small and tagged,
buddina,487447,300,CoastSnap Station,Ok,,
burleigh,303934,300,CoastSnap Station,Good,Distant view. Building.,
byron,269991,300,CoastSnap Station,Ok,Distant view. Zoomed images turn small,
cathieillaroo,393016,300,CoastSnap Station,,,
cathielagoon,392988,300,CoastSnap Station,,,
coogee,286418,300,DIY,,,
coolum,487449,300,CoastSnap Station,,,
cooya,275690,300,CoastSnap Station,Very Bad,,Bad Seed Images
cowbay,275692,300,CoastSnap Station,,,
era,297634,300,CoastSnap Station,,,
fourmile,275689,300,CoastSnap Station,,,
frankston,425039,300,CoastSnap Station,,,
garie,296239,300,CoastSnap Station,,,
hungry,296903,300,CoastSnap Station,,,
kirra,270011,300,CoastSnap Station,,,
macsnth,257393,300,DIY,,,
macssth,404754,300,DIY,,,
manly,242186,300,CoastSnap Station,,,
moffat,487448,300,CoastSnap Station,,,
newell,275691,300,CoastSnap Station,,,
nthnarra,243537,300,CoastSnap Station,,,
queenscliff,269334,300,CoastSnap Station,,,
rainbow,451612,300,CoastSnap Station,,,
seaford,421320,300,CoastSnap Station,,,
shortpoint,269992,300,CoastSnap Station,,,
stockton1,269985,300,CoastSnap Station,,,
stockton2,269986,300,CoastSnap Station,,,
stockton3,269987,300,CoastSnap Station,,,
tomakin,269989,300,CoastSnap Station,,,
tugun,269993,300,CoastSnap Station,,,
wamberal,299431,300,CoastSnap Station,,,
wonga,268307,300,CoastSnap Station,,,
woolgooga,435190,300,CoastSnap Station,,,
1 site_name root_id limit type Registration Capacity Comment parent_directory
2 alex 487451 300 CoastSnap Station Good Hand rail in foreground C:\Users\z5079346\OneDrive - UNSW\My files\CoastSnap\Images
3 birubi 548070 300 CoastSnap Station Good Structures in foreground
4 blacksmiths 269988 300 CoastSnap Station Good
5 broulee 269990 300 CoastSnap Station Good Bad images small and tagged
6 buddina 487447 300 CoastSnap Station Ok
7 burleigh 303934 300 CoastSnap Station Good Distant view. Building.
8 byron 269991 300 CoastSnap Station Ok Distant view. Zoomed images turn small
9 cathieillaroo 393016 300 CoastSnap Station
10 cathielagoon 392988 300 CoastSnap Station
11 coogee 286418 300 DIY
12 coolum 487449 300 CoastSnap Station
13 cooya 275690 300 CoastSnap Station Very Bad Bad Seed Images
14 cowbay 275692 300 CoastSnap Station
15 era 297634 300 CoastSnap Station
16 fourmile 275689 300 CoastSnap Station
17 frankston 425039 300 CoastSnap Station
18 garie 296239 300 CoastSnap Station
19 hungry 296903 300 CoastSnap Station
20 kirra 270011 300 CoastSnap Station
21 macsnth 257393 300 DIY
22 macssth 404754 300 DIY
23 manly 242186 300 CoastSnap Station
24 moffat 487448 300 CoastSnap Station
25 newell 275691 300 CoastSnap Station
26 nthnarra 243537 300 CoastSnap Station
27 queenscliff 269334 300 CoastSnap Station
28 rainbow 451612 300 CoastSnap Station
29 seaford 421320 300 CoastSnap Station
30 shortpoint 269992 300 CoastSnap Station
31 stockton1 269985 300 CoastSnap Station
32 stockton2 269986 300 CoastSnap Station
33 stockton3 269987 300 CoastSnap Station
34 tomakin 269989 300 CoastSnap Station
35 tugun 269993 300 CoastSnap Station
36 wamberal 299431 300 CoastSnap Station
37 wonga 268307 300 CoastSnap Station
38 woolgooga 435190 300 CoastSnap Station

Binary file not shown.
Loading…
Cancel
Save