937 lines
35 KiB
Python
937 lines
35 KiB
Python
#!/usr/bin/env python3
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
import configparser
|
|
import json
|
|
import math
|
|
import shutil
|
|
import sys
|
|
import time
|
|
import warnings
|
|
import zipfile
|
|
from hashlib import sha256 as hashalgo
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import jinja2
|
|
from markupsafe import Markup
|
|
from PIL import Image
|
|
|
|
import htmlmin
|
|
from flaskext.markdown import Markdown
|
|
|
|
|
|
class Objectify(object):
|
|
def __init__(self, obj=None, **kwargs):
|
|
if obj is None:
|
|
obj = kwargs
|
|
else:
|
|
obj = {**obj, **kwargs}
|
|
self.__dict__ = obj
|
|
|
|
def __str__(self):
|
|
return str(self.__dict__)
|
|
|
|
def __repr__(self):
|
|
return f'{type(self).__name__}({repr(self.__dict__)})'
|
|
|
|
def __getitem__(self, idx):
|
|
return self.__dict__[idx]
|
|
|
|
def __setitem__(self, idx, val):
|
|
self.__dict__[idx] = val
|
|
|
|
|
|
Image.MAX_IMAGE_PIXELS = None
|
|
|
|
jinjaEnvironment = jinja2.Environment(
|
|
loader=jinja2.FileSystemLoader(
|
|
str(Path('tpl').absolute())
|
|
)
|
|
)
|
|
|
|
md = Markdown(app=Objectify(dict(jinja_env=jinjaEnvironment)),
|
|
extensions=['fenced_code'],
|
|
safe_mode=True)
|
|
|
|
|
|
def treat_split(l): return list(filter(len, map(str.strip, l)))
|
|
|
|
|
|
def jsonp_dumps(obj, padding='callback'):
|
|
return Markup(f'{padding}({json.dumps(obj, sort_keys=True)});')
|
|
|
|
|
|
def jsonv_dumps(obj, var='variable'):
|
|
return Markup(f'var {var} = {json.dumps(obj, sort_keys=True)};')
|
|
|
|
|
|
def json_dumps(obj):
|
|
return Markup(f'{json.dumps(obj, sort_keys=True)}')
|
|
|
|
|
|
def digest_for(bts):
|
|
m = hashalgo()
|
|
m.update(bts)
|
|
return m.hexdigest()
|
|
|
|
|
|
class LeveledItem(object):
|
|
def __init__(self, item, *children):
|
|
self.item = item
|
|
self.children = LeveledList(children)
|
|
|
|
def __str__(self):
|
|
return repr(self)
|
|
|
|
def __repr__(self):
|
|
return f'{type(self).__name__}({repr(self.item)}, *{repr(self.children)})'
|
|
|
|
def to_jsonable(self, depth=1):
|
|
return {
|
|
'item': self.item,
|
|
'children': self.children.to_jsonable(depth),
|
|
}
|
|
|
|
def transform(self, dct: dict):
|
|
return type(self)(dct[self.item], *self.children.transform(dct))
|
|
|
|
def leveled_transform(self, *dcts):
|
|
head, *tail = dcts
|
|
return type(self)(head[self.item], *self.children.leveled_transform(*tail))
|
|
|
|
def linearify(self):
|
|
return LeveledList([self.item, *self.children.linearify()])
|
|
|
|
|
|
class LeveledList(list):
|
|
def __new__(cls, base=None):
|
|
return list.__new__(cls, base)
|
|
|
|
def to_jsonable(self, depth=2):
|
|
return [
|
|
child if depth <= 1 else child.to_jsonable(depth-1)
|
|
for child in self
|
|
]
|
|
|
|
def transform(self, dct: dict):
|
|
return type(self)([
|
|
i.transform(dct) if isinstance(i, LeveledItem) else dct[i]
|
|
for i in self
|
|
])
|
|
|
|
def leveled_transform(self, *dcts):
|
|
head, *_ = dcts
|
|
return type(self)([
|
|
i.leveled_transform(*dcts) if isinstance(i,
|
|
LeveledItem) else head[i]
|
|
for i in self
|
|
])
|
|
|
|
def linearify(self):
|
|
return type(self)(self._linearify())
|
|
|
|
def _linearify(self):
|
|
for l in [
|
|
i.linearify() if isinstance(i, (LeveledList, LeveledItem)) else [i]
|
|
for i in self
|
|
]:
|
|
yield from l
|
|
yield from []
|
|
|
|
|
|
def parse_leveled_list(file: Path) -> Tuple[LeveledList, Dict[str, int]]:
|
|
k2l: Dict[str, int] = dict()
|
|
lst = LeveledList()
|
|
lines = treat_split(file.read_text().splitlines())
|
|
for lnumb, line in enumerate(lines):
|
|
itm = line[1:]
|
|
if line[0] == '-':
|
|
lst.append(LeveledItem(itm))
|
|
elif line[0] == '=':
|
|
lst[-1].children.append(itm)
|
|
else:
|
|
raise RuntimeError('cannot determine nestedfulness level')
|
|
k2l[itm] = lnumb
|
|
return lst, k2l
|
|
|
|
|
|
class ImageScalingDefinition(object):
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
|
|
def resize(self, img: Image):
|
|
raise NotImplementedError
|
|
|
|
|
|
class ImageThumbnailerDefinition(ImageScalingDefinition):
|
|
def get_resize_res(self, img: Image):
|
|
x = self.x
|
|
y = self.y
|
|
if x is not None or y is not None:
|
|
dx, dy = img.size
|
|
if x is None or y is None:
|
|
if x is None:
|
|
if dy >= y:
|
|
x = dx
|
|
else:
|
|
x = math.ceil(dx*(y/dy))
|
|
elif y is None:
|
|
if dx >= x:
|
|
y = dy
|
|
else:
|
|
y = math.ceil(dy*(x/dx))
|
|
if not (x >= dx and y >= dy):
|
|
return (x, y)
|
|
|
|
def resize(self, img: Image):
|
|
tpl = self.get_resize_res(img)
|
|
if tpl is not None:
|
|
img.thumbnail(tpl)
|
|
|
|
|
|
def remove_transparency(im, bg_colour=(0xF5, 0xF5, 0xF5)):
|
|
if im.mode in ('RGBA', 'LA') or (im.mode == 'P' and 'transparency' in im.info):
|
|
alpha = im.convert('RGBA').split()[-1]
|
|
bg = Image.new("RGB", im.size, bg_colour)
|
|
bg.paste(im, mask=alpha)
|
|
im.close()
|
|
return bg
|
|
else:
|
|
return im
|
|
|
|
|
|
def get_images(images_directory, output_directory):
|
|
print('Setting image paths...', flush=True)
|
|
images_source = list(filter(lambda x: x.name != '.git',
|
|
images_directory.rglob('*.*')))
|
|
images_sizes = dict(
|
|
thumb=ImageThumbnailerDefinition(300, 300),
|
|
small=ImageThumbnailerDefinition(900, 900),
|
|
dcent=ImageThumbnailerDefinition(1440, 1440),
|
|
large=ImageThumbnailerDefinition(2560, 2560),
|
|
full=ImageThumbnailerDefinition(None, None)
|
|
)
|
|
images_source = {
|
|
image.name: (
|
|
image,
|
|
digest_for(image.read_bytes())
|
|
)
|
|
for image in images_source
|
|
}
|
|
images_source = {
|
|
k: Objectify(
|
|
source=i,
|
|
files=Objectify(
|
|
thumb=output_directory.joinpath(f'thumbnail_{d}.jpg'),
|
|
small=output_directory.joinpath(f'small_{d}.jpg'),
|
|
dcent=output_directory.joinpath(f'decent_{d}.jpg'),
|
|
large=output_directory.joinpath(f'large_{d}.jpg'),
|
|
full=output_directory.joinpath(f'full_{d}{i.suffix}')
|
|
),
|
|
formats=Objectify(),
|
|
dimens=Objectify(),
|
|
sizes=Objectify()
|
|
)
|
|
for k, (i, d) in images_source.items()
|
|
}
|
|
print('Ensuring all images were converted...', flush=True)
|
|
showProfiler = False
|
|
profiled_images = list()
|
|
for image_name, image_source in images_source.items():
|
|
already_printed = False
|
|
start = time.time()
|
|
for size_name, image_size in images_sizes.items():
|
|
image_destination = image_source.files[size_name]
|
|
if not image_destination.exists():
|
|
if not already_printed:
|
|
print(f'Current image: {image_name}', flush=True)
|
|
already_printed = True
|
|
convert_image(
|
|
image_source,
|
|
image_size,
|
|
size_name,
|
|
image_destination,
|
|
image_name
|
|
)
|
|
end = time.time()
|
|
if end-start > 1:
|
|
showProfiler = True
|
|
profiled_images.append(((end-start), image_name))
|
|
image_source.dimens = Objectify({
|
|
size_name: Image.open(image_source.files[size_name]).size
|
|
for size_name in images_sizes
|
|
})
|
|
image_source.sizes = Objectify({
|
|
size_name: image_source.files[size_name].stat().st_size
|
|
for size_name in images_sizes
|
|
})
|
|
image_source.formats = Objectify({
|
|
size_name: image_source.files[size_name].suffix[1:].upper()
|
|
for size_name in images_sizes
|
|
})
|
|
if showProfiler:
|
|
profiled_images.sort()
|
|
profiled_images.reverse()
|
|
print('Profiled conversion times:', flush=True)
|
|
for i, (d, p) in enumerate(profiled_images):
|
|
s = ' %03d: % 6.2fs - %s ' % (i+1, d, p)
|
|
if len(s) < 70:
|
|
s += ' '*(70-len(s))
|
|
s += 'x'.join(list(map(str, images_source[p].dimens.full)))
|
|
print(s, flush=True)
|
|
total = sum(list(map(lambda a: a[0], profiled_images)))
|
|
print(' Total: %.3fs' % total, flush=True)
|
|
return images_source
|
|
|
|
|
|
def convert_image(image_source,
|
|
image_size,
|
|
size_name,
|
|
image_destination,
|
|
image_name
|
|
):
|
|
img = Image.open(image_source.source)
|
|
resz = image_size.get_resize_res(img)
|
|
# These prints are necessary for the CI runner to not kill the process
|
|
print(f'[{size_name}] ', end='')
|
|
if (
|
|
resz is None
|
|
and
|
|
image_source.source.suffix == image_destination.suffix
|
|
):
|
|
print('did not resize: ', end='')
|
|
shutil.copyfile(
|
|
image_source.source,
|
|
image_destination
|
|
)
|
|
resz = img.size
|
|
else:
|
|
print('got resized [', end='')
|
|
print('x'.join(list(map(str, img.size))), end='')
|
|
image_size.resize(img)
|
|
print('::', end='')
|
|
print('x'.join(list(map(str, img.size))), end='')
|
|
print(']', end='')
|
|
img = remove_transparency(img)
|
|
img.save(image_destination)
|
|
resz = img.size
|
|
print(': ', end='')
|
|
print(f'{image_name}', flush=True)
|
|
img.close()
|
|
del img
|
|
|
|
|
|
def parse_art_info(images_description: configparser.ConfigParser,
|
|
tags_description: configparser.ConfigParser,
|
|
tag_groups_description: configparser.ConfigParser,
|
|
artists_description: configparser.ConfigParser,
|
|
social_description: configparser.ConfigParser,
|
|
bodyparts_description: configparser.ConfigParser
|
|
):
|
|
print('Parsing art info...', flush=True)
|
|
tags = dict()
|
|
social = dict()
|
|
images: Dict[str, Any] = dict()
|
|
artists = dict()
|
|
bodyparts = dict()
|
|
tag_groups = dict()
|
|
pending_tags = list(set(tags_description.sections())-{'DEFAULT'})
|
|
pending_social = list(set(social_description.sections())-{'DEFAULT'})
|
|
pending_images = list(set(images_description.sections())-{'DEFAULT'})
|
|
pending_artists = list(set(artists_description.sections())-{'DEFAULT'})
|
|
pending_bodyparts = list(set(bodyparts_description.sections())-{'DEFAULT'})
|
|
pending_tag_groups = list(
|
|
set(tag_groups_description.sections())-{'DEFAULT'})
|
|
delayed_images = set()
|
|
|
|
def interpret_tag_operations(tag_sequence):
|
|
tags = []
|
|
for tag in tag_sequence:
|
|
if tag.startswith('!'):
|
|
tag2 = tag[1:]
|
|
tags = [t for t in tags if t != tag2]
|
|
elif tag not in tags:
|
|
tags.append(tag)
|
|
return tags
|
|
|
|
def failsafe_set(this, image_def, attr, hard=True, default=None, mapper=lambda a: a):
|
|
this[attr] = mapper(image_def[attr].replace("''", "'") if attr in image_def else (
|
|
this[attr] if hard else this.get(attr, default)))
|
|
|
|
def getBodyPart(bp):
|
|
if bp not in bodyparts:
|
|
if bp in bodyparts_description:
|
|
bp_def = bodyparts_description[bp]
|
|
bodyparts[bp] = bp_def.get('name', bp).strip()
|
|
else:
|
|
bodyparts[bp] = bp.strip()
|
|
warnings.warn(
|
|
f'Body part {repr(bp)} not found in INI file')
|
|
return bp
|
|
for bp in pending_bodyparts:
|
|
getBodyPart(bp)
|
|
while len(pending_images) > 0:
|
|
image_name, *pending_images = pending_images
|
|
image_def = images_description[image_name]
|
|
if(
|
|
image_def.get('ignore', 'false') == 'false'
|
|
and
|
|
image_name not in images
|
|
):
|
|
this = dict()
|
|
parent = None
|
|
if 'partof' in image_def:
|
|
parent = image_def['partof']
|
|
elif 'sameas' in image_def:
|
|
parent = image_def['sameas']
|
|
oldtags = []
|
|
if parent is not None and parent not in images:
|
|
if image_name in delayed_images:
|
|
raise ValueError(
|
|
f'Parent dependency of {repr(image_name)} ({repr(parent)}) cannot be ignored'
|
|
)
|
|
pending_images = [parent, image_name, *pending_images]
|
|
delayed_images.add(image_name)
|
|
continue
|
|
elif parent is not None:
|
|
this = images[parent].copy()
|
|
oldtags = this['tags'].copy()
|
|
failsafe_set(this, image_def, 'name')
|
|
failsafe_set(this, image_def, 'date')
|
|
failsafe_set(this, image_def, 'artist')
|
|
failsafe_set(this, image_def, 'technology')
|
|
failsafe_set(this, image_def, 'tags', False, '')
|
|
failsafe_set(this, image_def, 'link', False, None)
|
|
failsafe_set(this, image_def, 'notes', False, '')
|
|
cd = this.get('color', dict()).copy()
|
|
for key in image_def:
|
|
if key.startswith('color.'):
|
|
value: Optional[str] = image_def[key].strip()
|
|
set_unknown = False
|
|
if value in ('?', '??', '???'):
|
|
set_unknown = True
|
|
if value in ('', '-', '?', '??', '???'):
|
|
value = None
|
|
key2 = key[6:].split('@')
|
|
getBodyPart(key2[0])
|
|
getBodyPart(key2[-1])
|
|
k = key2[-1]
|
|
cd[k] = None if value is None else dict(
|
|
targetOnImage=key2[-1],
|
|
targetAppropriate=key2[0],
|
|
palleteChoiceAccurate=key2[0] == key2[-1],
|
|
colorUsed=value.upper(),
|
|
colorIsLight=determineLightOrDark(value.upper())
|
|
)
|
|
if set_unknown:
|
|
cd.pop(k, None)
|
|
if isinstance(this['date'], str):
|
|
this['date'] = list(
|
|
map(int, treat_split(this['date'].split('-'))))
|
|
if isinstance(this['tags'], str):
|
|
this['tags'] = treat_split(this['tags'].split(','))
|
|
this['tags'] = interpret_tag_operations(oldtags+this['tags'])
|
|
pending_artists.append(this['artist'])
|
|
pending_tags.append(this['technology'])
|
|
pending_tags += this['tags']
|
|
this['color'] = cd
|
|
images[image_name] = this
|
|
pending_tags = list(set(pending_tags))
|
|
pending_artists = list(set(pending_artists))
|
|
print('Parsing artist info...', flush=True)
|
|
for pending_artist in pending_artists:
|
|
this = dict()
|
|
if pending_artist in artists_description:
|
|
artist_def = artists_description[pending_artist]
|
|
sns = list()
|
|
this['name'] = pending_artist
|
|
failsafe_set(this, artist_def, 'name')
|
|
for sn in artist_def:
|
|
if sn != 'name':
|
|
this[sn] = artist_def[sn]
|
|
sns.append(sn)
|
|
pending_social.append(sn)
|
|
this['@'] = sorted(sns)
|
|
else:
|
|
warnings.warn(
|
|
f'Artist {repr(pending_artist)} not found in INI file')
|
|
this['name'] = pending_artist
|
|
artists[pending_artist] = this
|
|
pending_social = list(set(pending_social))
|
|
print('Parsing tag info...', flush=True)
|
|
for pending_tag in pending_tags:
|
|
this = dict()
|
|
if pending_tag in tags_description:
|
|
tag_def = tags_description[pending_tag]
|
|
if 'name' in tag_def:
|
|
failsafe_set(this, tag_def, 'name')
|
|
if 'icon' in tag_def:
|
|
failsafe_set(this, tag_def, 'icon')
|
|
if 'taggroup' in tag_def:
|
|
failsafe_set(this, tag_def, 'taggroup')
|
|
if 'position' in tag_def:
|
|
failsafe_set(this, tag_def, 'position', mapper=int)
|
|
else:
|
|
warnings.warn(
|
|
f'Tag {repr(pending_tag)} not found in INI file')
|
|
if 'name' not in this:
|
|
this['name'] = pending_tag
|
|
if 'icon' not in this:
|
|
this['icon'] = tags_description['DEFAULT']['icon']
|
|
if 'taggroup' not in this:
|
|
this['taggroup'] = 'DEFAULT'
|
|
if 'taggroup' not in this:
|
|
this['position'] = int(tags_description['DEFAULT']['position'])
|
|
pending_tag_groups.append(this['taggroup'])
|
|
tags[pending_tag] = this
|
|
pending_tag_groups = list(set(pending_tag_groups))
|
|
print('Parsing tag group info...', flush=True)
|
|
for pending_tag_group in [*pending_tag_groups, 'DEFAULT']:
|
|
this = dict()
|
|
if pending_tag_group in tag_groups_description:
|
|
tag_def = tag_groups_description[pending_tag_group]
|
|
if 'name' in tag_def:
|
|
failsafe_set(this, tag_def, 'name')
|
|
if 'required' in tag_def:
|
|
failsafe_set(this, tag_def, 'required',
|
|
mapper=lambda a: a == 'True')
|
|
if 'single' in tag_def:
|
|
failsafe_set(this, tag_def, 'single',
|
|
mapper=lambda a: a == 'True')
|
|
if 'position' in tag_def:
|
|
failsafe_set(this, tag_def, 'position', mapper=int)
|
|
else:
|
|
warnings.warn(
|
|
f'Tag group {repr(pending_tag_group)} not found in INI file')
|
|
if 'name' not in this:
|
|
this['name'] = pending_tag_group
|
|
if 'required' not in this:
|
|
this['required'] = tag_groups_description['DEFAULT']['required'] == 'True'
|
|
if 'single' not in this:
|
|
this['single'] = tag_groups_description['DEFAULT']['single'] == 'True'
|
|
if 'position' not in this:
|
|
this['position'] = int(
|
|
tag_groups_description['DEFAULT']['position'])
|
|
tag_groups[pending_tag_group] = this
|
|
print('Parsing social network info...', flush=True)
|
|
for pending_s in pending_social:
|
|
this = dict()
|
|
if pending_s in social_description:
|
|
social_def = social_description[pending_s]
|
|
if 'name' in social_def:
|
|
failsafe_set(this, social_def, 'name')
|
|
if 'icon' in social_def:
|
|
failsafe_set(this, social_def, 'icon')
|
|
else:
|
|
warnings.warn(
|
|
f'Social Platform {repr(pending_s)} not found in INI file')
|
|
if 'name' not in this:
|
|
this['name'] = pending_s
|
|
if 'icon' not in this:
|
|
this['icon'] = social_description['DEFAULT']['icon']
|
|
social[pending_s] = this
|
|
return tags, tag_groups, social, images, artists, bodyparts
|
|
|
|
|
|
def run(configfile):
|
|
configfolder = configfile.parent
|
|
config = get_config_parser_from_file(configfile)
|
|
if config['cfg'].get('lookAt', False):
|
|
return run(configfolder.joinpath(config['cfg']['lookAt']))
|
|
output_directory = Path(config['cfg']['output_directory'])
|
|
images_directory = Path(config['cfg']['images_directory'])
|
|
image_notes_path = Path(config['cfg']['image_notes_directory'])
|
|
input_images_files = set(map(lambda path: path.name, filter(
|
|
Path.is_file, images_directory.glob('*'))))
|
|
used_images_files = set()
|
|
webmanifest_template = json.loads(
|
|
Path(config['cfg']['webmanifest']).read_text())
|
|
# template_directory = get_config_parser_from_file(
|
|
# configfolder.joinpath(config['cfg']['template_directory']))
|
|
images_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['images_description']))
|
|
tags_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['tags_description']))
|
|
tag_groups_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['tag_groups_description']))
|
|
artists_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['artists_description']))
|
|
social_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['social_description']))
|
|
bodyparts_description = get_config_parser_from_file(
|
|
configfolder.joinpath(config['cfg']['bodyparts_description']))
|
|
pages = treat_split(config['cfg']['pages'].split(','))
|
|
tags, tag_groups, social, images_, artists, bodyparts = parse_art_info(
|
|
images_description,
|
|
tags_description,
|
|
tag_groups_description,
|
|
artists_description,
|
|
social_description,
|
|
bodyparts_description
|
|
)
|
|
sorted_artists = list(map(lambda a: a[0], sorted(
|
|
artists.items(), key=lambda a: a[1]['name'])))
|
|
# Image notes
|
|
image_notes_path.mkdir(parents=True, exist_ok=True)
|
|
for img in images_.keys():
|
|
imgnotepath = image_notes_path.joinpath(f'{img}.md')
|
|
if not imgnotepath.exists():
|
|
imgnotepath.touch(exist_ok=True)
|
|
imgnotepath.write_text('\n')
|
|
image_notes = dict(map(
|
|
lambda img: (img, image_notes_path.joinpath(f'{img}.md').read_text()),
|
|
images_.keys()))
|
|
for k, img in images_.items():
|
|
img['notes'] = image_notes[k].rstrip()
|
|
del image_notes
|
|
# Grouping and sorting tags
|
|
all_tags_by_group = dict(
|
|
map(lambda a: (a['taggroup'], list()), tags.values()))
|
|
all_tags_by_group['DEFAULT'] = list()
|
|
for tag_id, tag in tags.items():
|
|
group_id = tag['taggroup']
|
|
all_tags_by_group[group_id].append(tag_id)
|
|
for k, groups in all_tags_by_group.items():
|
|
all_tags_by_group[k] = sorted(
|
|
groups,
|
|
key=lambda i: (tag_groups[tags[i]['taggroup']]
|
|
['position'], tags[i]['position'], tags[i]['name'])
|
|
)
|
|
for image in images_.values():
|
|
image['tags'] = sorted(
|
|
image['tags'],
|
|
key=lambda i: (tag_groups[tags[i]['taggroup']]
|
|
['position'], tags[i]['position'], tags[i]['name'])
|
|
)
|
|
tag_groups_sorted = sorted(
|
|
tag_groups.keys(),
|
|
key=lambda g: (tag_groups[g]['position'], tag_groups[g]['name']))
|
|
tag_groups_nonempty_sorted = list(filter(
|
|
lambda g: len(all_tags_by_group[g]),
|
|
sorted(tag_groups.keys(),
|
|
key=lambda g: (tag_groups[g]['position'], tag_groups[g]['name']))))
|
|
# Ensuring tag policies
|
|
for image_file, image_data in sorted(images_.items()):
|
|
image_tags = [image_data['technology']]+image_data['tags']
|
|
image_tags_grouped = dict(
|
|
map(lambda a: (a, list()), tag_groups.keys()))
|
|
for image_tag in image_tags:
|
|
image_tags_grouped[tags[image_tag]['taggroup']].append(image_tag)
|
|
for tag_group_name, tag_group in sorted(tag_groups.items()):
|
|
itg = image_tags_grouped[tag_group_name]
|
|
if tag_group_name == 'DEFAULT' and len(itg) > 0:
|
|
print(f'On image {repr(image_file)}: ')
|
|
print(f' Using group {repr(tag_group_name)} is discouraged')
|
|
print(f' Found: {itg}')
|
|
print(f' Aceptable: {all_tags_by_group[tag_group_name]}')
|
|
elif tag_group_name == 'DEFAULT':
|
|
continue
|
|
elif len(itg) == 0 and tag_group['required']:
|
|
print(f'On image {repr(image_file)}:')
|
|
print(
|
|
f' At least one tag of group {repr(tag_group_name)} is required')
|
|
print(f' Found: {itg}')
|
|
print(f' Aceptable: {all_tags_by_group[tag_group_name]}')
|
|
elif len(itg) > 1 and tag_group['single']:
|
|
print(f'On image {repr(image_file)}:')
|
|
print(
|
|
f' Up to one tag of group {repr(tag_group_name)} is admitted')
|
|
print(f' Found: {itg}')
|
|
print(f' Aceptable: {all_tags_by_group[tag_group_name]}')
|
|
all_images = get_images(images_directory, output_directory)
|
|
image_stereotypes = 'thumb,small,dcent,large,full'.split(',')
|
|
imgsrc2imgurl = Objectify({
|
|
s: {
|
|
k: v.files[s].name
|
|
for k, v in all_images.items()
|
|
}
|
|
for s in image_stereotypes
|
|
})
|
|
# print(all_images)
|
|
js_tags = jsonv_dumps(tags, 'tags')
|
|
js_social = jsonv_dumps(social, 'social')
|
|
js_artists = jsonv_dumps(artists, 'artists')
|
|
print('Processing pages...', flush=True)
|
|
for page in pages:
|
|
mes = treat_split(config[page].get('me', '').split('||'))
|
|
name = config[page]['name'].strip()
|
|
language = config[page]['language'].strip()
|
|
keywords = config[page].get('keywords', '').strip()
|
|
description = config[page].get('description', '').strip()
|
|
if not keywords:
|
|
keywords = None
|
|
if not description:
|
|
description = None
|
|
color_list = treat_split(config[page].get(
|
|
'colors', '').strip().split(','))
|
|
canonical_colors = dict()
|
|
color_repeated = {True: 0, False: 0}
|
|
for clr in color_list:
|
|
c = config[page]['color.'+clr].strip()
|
|
if c.startswith('#'):
|
|
canonical_colors[clr] = dict(
|
|
color=c,
|
|
thisColor=clr,
|
|
rootColor=clr,
|
|
repeated=False,
|
|
colorIsLight=determineLightOrDark(c)
|
|
)
|
|
color_repeated[False] += 1
|
|
elif c.startswith('@'):
|
|
c2 = c
|
|
c = canonical_colors[c2[1:]]['color']
|
|
r = canonical_colors[c2[1:]]['rootColor']
|
|
canonical_colors[clr] = dict(
|
|
color=c,
|
|
thisColor=clr,
|
|
rootColor=r,
|
|
repeated=True,
|
|
colorIsLight=determineLightOrDark(c)
|
|
)
|
|
color_repeated[True] += 1
|
|
else:
|
|
raise NotImplementedError
|
|
|
|
print(f'Processing page {repr(name)}...', flush=True)
|
|
template = jinjaEnvironment.get_template(
|
|
f"{config[page].get('template', page)}.html"
|
|
)
|
|
imgorg, key2line = parse_leveled_list(
|
|
Path(config[page]['sorted'].strip()))
|
|
for i in imgorg.linearify():
|
|
used_images_files.add(i)
|
|
# print(jsonp_dumps(imgorg.transform(key2line).linearify()))
|
|
|
|
images__ = {l: images_[k] for k, l in key2line.items()}
|
|
images = list(map(
|
|
lambda a: a[1],
|
|
sorted(list(images__.items()))
|
|
))
|
|
js_images = jsonv_dumps(images, 'images')
|
|
img_formats = [
|
|
all_images[i].formats.__dict__ for i in imgorg.linearify()]
|
|
img_dimens = [
|
|
all_images[i].dimens.__dict__ for i in imgorg.linearify()]
|
|
img_sizes = [
|
|
all_images[i].sizes.__dict__ for i in imgorg.linearify()]
|
|
|
|
js_canonical_colors = jsonv_dumps(canonical_colors, 'canonical_colors')
|
|
js_color_repeated = jsonv_dumps(color_repeated, 'color_repeated')
|
|
js_color_list = jsonv_dumps(color_list, 'color_list')
|
|
js_bodyparts = jsonv_dumps(bodyparts, 'bodyparts')
|
|
|
|
org_hierarchical = imgorg.transform(key2line).to_jsonable()
|
|
org_linear = imgorg.transform(imgsrc2imgurl.small).linearify()
|
|
imgs_hierarchical = imgorg.leveled_transform(
|
|
imgsrc2imgurl.small, imgsrc2imgurl.thumb).linearify()
|
|
imgs_thumb = imgorg.transform(imgsrc2imgurl.thumb).linearify()
|
|
imgs_small = imgorg.transform(imgsrc2imgurl.small).linearify()
|
|
imgs_dcent = imgorg.transform(imgsrc2imgurl.dcent).linearify()
|
|
imgs_large = imgorg.transform(imgsrc2imgurl.large).linearify()
|
|
imgs_full = imgorg.transform(imgsrc2imgurl.full).linearify()
|
|
js_org_hierarchical = jsonv_dumps(org_hierarchical, 'org_hierarchical')
|
|
js_org_linear = jsonv_dumps(org_linear, 'org_linear')
|
|
js_imgs_hierarchical = jsonv_dumps(
|
|
imgs_hierarchical, 'imgs_hierarchical')
|
|
js_imgs_thumb = jsonv_dumps(imgs_thumb, 'imgs_thumb')
|
|
js_imgs_small = jsonv_dumps(imgs_small, 'imgs_small')
|
|
js_imgs_dcent = jsonv_dumps(imgs_dcent, 'imgs_dcent')
|
|
js_imgs_large = jsonv_dumps(imgs_large, 'imgs_large')
|
|
js_imgs_full = jsonv_dumps(imgs_full, 'imgs_full')
|
|
js_formats = jsonv_dumps(img_formats, 'formats')
|
|
js_dimens = jsonv_dumps(img_dimens, 'dimens')
|
|
js_sizes = jsonv_dumps(img_sizes, 'sizes')
|
|
js_name = jsonv_dumps(name, 'name')
|
|
js_mes = jsonv_dumps(mes, 'mes')
|
|
js_tag_groups_sorted = jsonv_dumps(
|
|
tag_groups_sorted, 'tag_groups_sorted')
|
|
js_all_tags_by_group = jsonv_dumps(
|
|
all_tags_by_group, 'all_tags_by_group')
|
|
js_tag_groups = jsonv_dumps(tag_groups, 'tag_groups')
|
|
js_tag_groups_nonempty_sorted = jsonv_dumps(
|
|
tag_groups_nonempty_sorted, 'tag_groups_nonempty_sorted')
|
|
js_sorted_artists = jsonv_dumps(sorted_artists, 'sorted_artists')
|
|
print(f'Generating ZIP thumbs for {repr(name)}...', flush=True)
|
|
zip_thumb = zipFiles(
|
|
output_directory.joinpath(page+'_thumb.zip'),
|
|
*[output_directory.joinpath(img) for img in imgs_thumb]
|
|
)
|
|
print(f'Generating ZIP smalls for {repr(name)}...', flush=True)
|
|
zip_small = zipFiles(
|
|
output_directory.joinpath(page+'_small.zip'),
|
|
*[output_directory.joinpath(img) for img in imgs_small]
|
|
)
|
|
print(f'Generating ZIP dcents for {repr(name)}...', flush=True)
|
|
zip_dcent = zipFiles(
|
|
output_directory.joinpath(page+'_dcent.zip'),
|
|
*[output_directory.joinpath(img) for img in imgs_dcent]
|
|
)
|
|
print(f'Generating ZIP larges for {repr(name)}...', flush=True)
|
|
zip_large = zipFiles(
|
|
output_directory.joinpath(page+'_large.zip'),
|
|
*[output_directory.joinpath(img) for img in imgs_large]
|
|
)
|
|
print(f'Generating ZIP fulls for {repr(name)}...', flush=True)
|
|
zip_full = zipFiles(
|
|
output_directory.joinpath(page+'_full.zip'),
|
|
*[output_directory.joinpath(img) for img in imgs_full]
|
|
)
|
|
zips = [zip_thumb, zip_small, zip_dcent, zip_large, zip_full]
|
|
zips = [dict(name=zp.name, size=zp.stat().st_size) for zp in zips]
|
|
for zp in zips:
|
|
zp['fmtd'] = sizeFmt(zp['size'])
|
|
js_zips = jsonv_dumps(zips, 'zips')
|
|
print(f'Rendering page {repr(name)}...', flush=True)
|
|
webmanifest = {
|
|
**webmanifest_template,
|
|
'short_name': name,
|
|
'name': name,
|
|
'description': description,
|
|
'start_url': f'./{page}.html',
|
|
}
|
|
js_webmanifest = jsonv_dumps(webmanifest, 'webmanifest')
|
|
rendered = template.render(
|
|
description=description,
|
|
keywords=keywords,
|
|
language=language,
|
|
name=name,
|
|
org_hierarchical=org_hierarchical,
|
|
org_linear=org_linear,
|
|
imgs_hierarchical=imgs_hierarchical,
|
|
imgs_thumb=imgs_thumb,
|
|
imgs_small=imgs_small,
|
|
imgs_dcent=imgs_dcent,
|
|
imgs_large=imgs_large,
|
|
imgs_full=imgs_full,
|
|
formats=img_formats,
|
|
dimens=img_dimens,
|
|
sizes=img_sizes,
|
|
artists=artists,
|
|
images=images,
|
|
social=social,
|
|
tags=tags,
|
|
zips=zips,
|
|
page=page,
|
|
mes=mes,
|
|
canonical_colors=canonical_colors,
|
|
color_repeated=color_repeated,
|
|
color_list=color_list,
|
|
bodyparts=bodyparts,
|
|
webmanifest=webmanifest,
|
|
tag_groups_sorted=tag_groups_sorted,
|
|
all_tags_by_group=all_tags_by_group,
|
|
tag_groups=tag_groups,
|
|
sorted_artists=sorted_artists,
|
|
tag_groups_nonempty_sorted=tag_groups_nonempty_sorted,
|
|
js_tag_groups_nonempty_sorted=js_tag_groups_nonempty_sorted,
|
|
js_sorted_artists=js_sorted_artists,
|
|
js_tag_groups_sorted=js_tag_groups_sorted,
|
|
js_all_tags_by_group=js_all_tags_by_group,
|
|
js_tag_groups=js_tag_groups,
|
|
js_org_hierarchical=js_org_hierarchical,
|
|
js_org_linear=js_org_linear,
|
|
js_imgs_hierarchical=js_imgs_hierarchical,
|
|
js_imgs_thumb=js_imgs_thumb,
|
|
js_imgs_small=js_imgs_small,
|
|
js_imgs_dcent=js_imgs_dcent,
|
|
js_imgs_large=js_imgs_large,
|
|
js_imgs_full=js_imgs_full,
|
|
js_formats=js_formats,
|
|
js_dimens=js_dimens,
|
|
js_sizes=js_sizes,
|
|
js_artists=js_artists,
|
|
js_images=js_images,
|
|
js_social=js_social,
|
|
js_tags=js_tags,
|
|
js_name=js_name,
|
|
js_zips=js_zips,
|
|
js_mes=js_mes,
|
|
js_canonical_colors=js_canonical_colors,
|
|
js_color_repeated=js_color_repeated,
|
|
js_color_list=js_color_list,
|
|
js_bodyparts=js_bodyparts,
|
|
js_webmanifest=js_webmanifest
|
|
)
|
|
minified = rendered
|
|
minified = htmlmin.minify(rendered, remove_empty_space=True)
|
|
output_directory.joinpath(f"{page}.html").write_text(minified)
|
|
output_directory.joinpath(f"{page}.json").write_text(
|
|
json_dumps(webmanifest))
|
|
if len(unused_images_files := (input_images_files.difference(used_images_files))) > 0:
|
|
print(f'Unused images: {sorted(list(unused_images_files))}')
|
|
print('All done!', flush=True)
|
|
|
|
|
|
def get_config_parser_from_file(file):
|
|
config = configparser.ConfigParser()
|
|
config.read(file)
|
|
return config
|
|
|
|
|
|
def printusage():
|
|
print("Usage:", file=sys.stderr)
|
|
print(
|
|
f" python{sys.version_info.major} -m {sys.argv[0]} <config.ini>",
|
|
file=sys.stderr
|
|
)
|
|
|
|
|
|
def determineLightOrDark(color): # True: light, False: dark
|
|
'''https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems'''
|
|
yr = 0.2126
|
|
yg = 0.7152
|
|
yb = 0.0722
|
|
if not (len(color) == 7 and color.startswith('#')):
|
|
raise ValueError(f'Color {repr(color)} should be in #RRGGBB format')
|
|
r = int(color[1:3], 16)
|
|
g = int(color[3:5], 16)
|
|
b = int(color[5:7], 16)
|
|
hsp = (
|
|
yr*r**2 +
|
|
yg*g**2 +
|
|
yb*b**2)**0.5
|
|
return hsp > 127
|
|
|
|
|
|
def zipFiles(out, *inputs):
|
|
if out.exists():
|
|
return out
|
|
zf = zipfile.ZipFile(
|
|
out,
|
|
mode='w',
|
|
compression=zipfile.ZIP_DEFLATED,
|
|
allowZip64=True,
|
|
compresslevel=9
|
|
)
|
|
for i, p in enumerate(inputs):
|
|
zf.writestr(
|
|
'%04d%s' % (i, p.suffix),
|
|
p.read_bytes(),
|
|
compress_type=zipfile.ZIP_DEFLATED,
|
|
compresslevel=9
|
|
)
|
|
zf.close()
|
|
return out
|
|
|
|
|
|
def sizeFmt(bytecount):
|
|
scale = ('B', 'KB', 'MB', 'GB', 'TB')
|
|
magnitude = 0
|
|
while bytecount > 2048:
|
|
magnitude += 1
|
|
bytecount /= 1024
|
|
return "%0.2f %s" % (bytecount, scale[magnitude])
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) == 2:
|
|
configfile = Path(sys.argv[1])
|
|
if configfile.exists():
|
|
run(configfile)
|
|
else:
|
|
printusage()
|
|
else:
|
|
printusage()
|