From 1c40e761f79270009cd2b6bae77ae1d29dc3e0dd Mon Sep 17 00:00:00 2001 From: Ethan Dalool Date: Tue, 12 Sep 2023 19:43:44 -0700 Subject: [PATCH] Update /photography. --- voussoir.net/photography/dark.css | 330 ----------- voussoir.net/photography/generate_site.py | 669 +++++++++++++++++----- 2 files changed, 541 insertions(+), 458 deletions(-) delete mode 100644 voussoir.net/photography/dark.css diff --git a/voussoir.net/photography/dark.css b/voussoir.net/photography/dark.css deleted file mode 100644 index a05a3df..0000000 --- a/voussoir.net/photography/dark.css +++ /dev/null @@ -1,330 +0,0 @@ -:root -{ - --color_bodybg: #272822; - --color_codebg: rgba(255, 255, 255, 0.05); - --color_codeborder: rgba(255, 255, 255, 0.2); - --color_h1bg: #284142; - --color_htmlbg: #1b1c18; - --color_blockquotebg: rgba(0, 0, 0, 0.2); - --color_blockquoteedge: rgba(255, 255, 255, 0.2); - --color_inlinecodebg: rgba(255, 255, 255, 0.1); - --color_link: #ae81ff; - --color_maintext: #ddd; -} - -*, *:before, *:after -{ - box-sizing: inherit; -} - -html -{ - height: 100vh; - box-sizing: border-box; - - background-color: var(--color_htmlbg); - color: var(--color_maintext); - - font-family: Verdana, sans-serif; - font-size: 10pt; - margin: 0; -} - -body -{ - min-height: 100%; - width: fit-content; - margin-left: auto; - margin-right: auto; - margin-top: 0; - margin-bottom: 0; - padding: 8px; -} -body.noscrollbar::-webkit-scrollbar -{ - display: none; -} -body.noscrollbar -{ - scrollbar-width: none; -} - -header -{ - width: 100%; - max-width: 120em; - margin-left: auto; - margin-right: auto; - text-align: end; -} -header > * -{ - display: inline-block; - padding: 16px; - background-color: var(--color_bodybg); -} - -.album, -.photograph -{ - position: relative; - margin-left: auto; - margin-right: auto; - margin-top: 8vh; - margin-bottom: 8vh; -} - -.photograph -{ - padding: 2vh; - background-color: var(--color_bodybg); - border-radius: 16px; -} -article .photograph:first-of-type -{ - margin-top: 0; -} -article .photograph:last-of-type -{ - margin-bottom: 0; -} -.photograph img -{ - max-height: 92vh; - border-radius: 8px; -} -article .morelink -{ - font-size: 2em; - text-align: center; - margin-top: 0; -} -@media not print -{ - .photograph - { - box-shadow: #000 0px 0px 40px -10px; - } -} - -@media screen and (min-width: 600px) -{ - article - { - width: fit-content; - } -} - -@media screen and (max-width: 600px) -{ - .photograph - { - box-shadow: none; - } -} - -@media not all and (pointer: fine) -{ - #keyboardhint, - #scrollbartoggle - { - display: none; - } -} - -h1, h2, h3, h4, h5 -{ - margin-bottom: 0; - padding: 8px; -} -h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child -{ - margin-top: 0; -} -h2, h3, h4, h5 -{ - border-bottom: 1px solid var(--color_maintext); - /*background-color: var(--color_h1bg);*/ -} -h1 + p -{ - margin-top: 0; -} -p:last-child -{ - margin-bottom: 0; -} -h1 {font-size: 2.00em;} h1 * {font-size: inherit;} -h2 {font-size: 1.75em;} h2 * {font-size: inherit;} -h3 {font-size: 1.50em;} h3 * {font-size: inherit;} -h4 {font-size: 1.25em;} h4 * {font-size: inherit;} -h5 {font-size: 1.00em;} h5 * {font-size: inherit;} - -.header_anchor_link {display: none; font-size: 1.0em; text-decoration: none} -h1:hover > .header_anchor_link {display: initial;} -h2:hover > .header_anchor_link {display: initial;} -h3:hover > .header_anchor_link {display: initial;} -h4:hover > .header_anchor_link {display: initial;} -h5:hover > .header_anchor_link {display: initial;} - -a -{ - color: var(--color_link); - cursor: pointer; -} - -article * -{ - max-width: 100%; - word-wrap: break-word; -} - -#table_of_contents -{ - border: 1px solid var(--color_blockquoteedge); - padding-top: 8px; - padding-bottom: 8px; - border-radius: 8px; -} - -blockquote -{ - background-color: var(--color_blockquotebg); - margin-inline-start: 0; - margin-inline-end: 0; - border-left: 4px solid var(--color_blockquoteedge); - - padding: 8px; - padding-inline-start: 20px; - padding-inline-end: 20px; -} - -table -{ - border-collapse: collapse; - font-size: 1em; -} -table, table th, table td -{ - border: 1px solid var(--color_maintext); -} -table th, table td -{ - padding: 4px; -} - -ol ol, ul ul, ol ul, ul ol -{ - padding-inline-start: 20px; -} - -*:not(pre) > code -{ - background-color: var(--color_inlinecodebg); - border-radius: 4px; - line-height: 1.5; - padding-left: 4px; - padding-right: 4px; -} - -pre -{ - padding: 8px; - border: 1px solid var(--color_codeborder); - background-color: var(--color_codebg); - overflow-x: auto; -} - -code, -pre, -.highlight * -{ - font-family: monospace; -} - -/* -Thank you richleland for pre-building this Monokai style. -https://github.com/richleland/pygments-css -*/ -:root -{ - --color_monokai_bg: #272822; - --color_monokai_purple: #ae81ff; - --color_monokai_green: #a6e22e; - --color_monokai_pink: #f92672; - --color_monokai_white: #f8f8f2; - --color_monokai_orange: #fd971f; - --color_monokai_yellow: #e6db74; - --color_monokai_blue: #66d9ef; -} -.highlight .hll { background-color: #49483e } -.highlight { background-color: var(--color_monokai_bg); color: var(--color_monokai_white) } -.highlight .c { color: #75715e } /* Comment */ -.highlight .err { color: #960050; background-color: #1e0010 } /* Error */ -.highlight .k { color: var(--color_monokai_pink) } /* Keyword */ -.highlight .l { color: var(--color_monokai_purple) } /* Literal */ -.highlight .n { color: var(--color_monokai_white) } /* Name */ -.highlight .o { color: var(--color_monokai_pink) } /* Operator */ -.highlight .p { color: var(--color_monokai_white) } /* Punctuation */ -.highlight .ch { color: #75715e } /* Comment.Hashbang */ -.highlight .cm { color: #75715e } /* Comment.Multiline */ -.highlight .cp { color: #75715e } /* Comment.Preproc */ -.highlight .cpf { color: #75715e } /* Comment.PreprocFile */ -.highlight .c1 { color: #75715e } /* Comment.Single */ -.highlight .cs { color: #75715e } /* Comment.Special */ -.highlight .gd { color: var(--color_monokai_pink) } /* Generic.Deleted */ -.highlight .ge { font-style: italic } /* Generic.Emph */ -.highlight .gi { color: var(--color_monokai_green) } /* Generic.Inserted */ -.highlight .gs { font-weight: bold } /* Generic.Strong */ -.highlight .gu { color: #75715e } /* Generic.Subheading */ -.highlight .kc { color: var(--color_monokai_purple) } /* Keyword.Constant */ -.highlight .kd { color: var(--color_monokai_blue) } /* Keyword.Declaration */ -.highlight .kn { color: var(--color_monokai_pink) } /* Keyword.Namespace */ -.highlight .kp { color: var(--color_monokai_blue) } /* Keyword.Pseudo */ -.highlight .kr { color: var(--color_monokai_blue) } /* Keyword.Reserved */ -.highlight .kt { color: var(--color_monokai_blue) } /* Keyword.Type */ -.highlight .ld { color: var(--color_monokai_yellow) } /* Literal.Date */ -.highlight .m { color: var(--color_monokai_purple) } /* Literal.Number */ -.highlight .s { color: var(--color_monokai_yellow) } /* Literal.String */ -.highlight .na { color: var(--color_monokai_white) } /* Name.Attribute */ -.highlight .narg {color: var(--color_monokai_orange) } /* Custom Name.Argument */ -.highlight .nb { color: var(--color_monokai_blue) } /* Name.Builtin */ -.highlight .nc { color: var(--color_monokai_white) } /* Name.Class */ -.highlight .no { color: var(--color_monokai_blue) } /* Name.Constant */ -.highlight .nd { color: var(--color_monokai_green) } /* Name.Decorator */ -.highlight .ni { color: var(--color_monokai_white) } /* Name.Entity */ -.highlight .ne { color: var(--color_monokai_blue) } /* Name.Exception */ -.highlight .nf { color: var(--color_monokai_green) } /* Name.Function */ -.highlight .nl { color: var(--color_monokai_white) } /* Name.Label */ -.highlight .nn { color: var(--color_monokai_white) } /* Name.Namespace */ -.highlight .nx { color: var(--color_monokai_green) } /* Name.Other */ -.highlight .py { color: var(--color_monokai_white) } /* Name.Property */ -.highlight .nt { color: var(--color_monokai_pink) } /* Name.Tag */ -.highlight .nv { color: var(--color_monokai_white) } /* Name.Variable */ -.highlight .ow { color: var(--color_monokai_pink) } /* Operator.Word */ -.highlight .w { color: var(--color_monokai_white) } /* Text.Whitespace */ -.highlight .mb { color: var(--color_monokai_purple) } /* Literal.Number.Bin */ -.highlight .mf { color: var(--color_monokai_purple) } /* Literal.Number.Float */ -.highlight .mh { color: var(--color_monokai_purple) } /* Literal.Number.Hex */ -.highlight .mi { color: var(--color_monokai_purple) } /* Literal.Number.Integer */ -.highlight .mo { color: var(--color_monokai_purple) } /* Literal.Number.Oct */ -.highlight .sa { color: var(--color_monokai_white) } /* Literal.String.Affix */ -.highlight .sb { color: var(--color_monokai_yellow) } /* Literal.String.Backtick */ -.highlight .sc { color: var(--color_monokai_yellow) } /* Literal.String.Char */ -.highlight .dl { color: var(--color_monokai_yellow) } /* Literal.String.Delimiter */ -.highlight .sd { color: var(--color_monokai_yellow) } /* Literal.String.Doc */ -.highlight .s2 { color: var(--color_monokai_yellow) } /* Literal.String.Double */ -.highlight .se { color: var(--color_monokai_purple) } /* Literal.String.Escape */ -.highlight .sh { color: var(--color_monokai_yellow) } /* Literal.String.Heredoc */ -.highlight .si { color: var(--color_monokai_yellow) } /* Literal.String.Interpol */ -.highlight .sx { color: var(--color_monokai_yellow) } /* Literal.String.Other */ -.highlight .sr { color: var(--color_monokai_yellow) } /* Literal.String.Regex */ -.highlight .s1 { color: var(--color_monokai_yellow) } /* Literal.String.Single */ -.highlight .ss { color: var(--color_monokai_yellow) } /* Literal.String.Symbol */ -.highlight .bp { color: var(--color_monokai_white) } /* Name.Builtin.Pseudo */ -.highlight .fm { color: var(--color_monokai_blue) } /* Name.Function.Magic */ -.highlight .vc { color: var(--color_monokai_white) } /* Name.Variable.Class */ -.highlight .vg { color: var(--color_monokai_white) } /* Name.Variable.Global */ -.highlight .vi { color: var(--color_monokai_white) } /* Name.Variable.Instance */ -.highlight .vm { color: var(--color_monokai_white) } /* Name.Variable.Magic */ -.highlight .il { color: var(--color_monokai_purple) } /* Literal.Number.Integer.Long */ diff --git a/voussoir.net/photography/generate_site.py b/voussoir.net/photography/generate_site.py index 0b0fabc..e94839f 100644 --- a/voussoir.net/photography/generate_site.py +++ b/voussoir.net/photography/generate_site.py @@ -1,18 +1,43 @@ +import boto3 +import io import jinja2 import PIL.Image import sys +import urllib.parse + +import etiquette +import r2_credentials from voussoirkit import dotdict from voussoirkit import imagetools from voussoirkit import pathclass from voussoirkit import spinal +from voussoirkit import vlogging +log = vlogging.get_logger(__name__, 'photography_generate') + +pdb = etiquette.photodb.PhotoDB('D:\\Documents\\Photos\\_etiquette') + +s3 = boto3.resource('s3', + endpoint_url = f'https://{r2_credentials.get_account_id()}.r2.cloudflarestorage.com', + aws_access_key_id = r2_credentials.get_access_key(), + aws_secret_access_key = r2_credentials.get_access_secret(), +) + +bucket = s3.Bucket('voussoir') + +S3_EXISTING_FILES = set(item.key for item in bucket.objects.filter(Prefix="photography/")) +PUBLISH_TAGNAME = 'voussoir_net_publish' +HEADLINER_TAGNAME = 'voussoir_net_headliner' PHOTOGRAPHY_ROOTDIR = pathclass.Path(__file__).parent ATOM_FILE = PHOTOGRAPHY_ROOTDIR.with_child('photography.atom') DOMAIN_ROOTDIR = PHOTOGRAPHY_ROOTDIR.parent -CSS_CONTENT = PHOTOGRAPHY_ROOTDIR.with_child('dark.css').read('r', encoding='utf-8') -DOMAIN_WEBROOT = ('file:///' + DOMAIN_ROOTDIR.absolute_path) if '--test' in sys.argv else 'https://voussoir.net' -DOMAIN_WEBROOT = DOMAIN_WEBROOT.replace('\\', '/') +DOMAIN_WEBROOT = 'https://voussoir.net' +PHOTOGRAPHY_WEBROOT = 'https://voussoir.net/photography' +S3_WEBROOT = 'https://files.voussoir.net' + +SIZE_SMALL = 1440 +SIZE_TINY = 360 def webpath(path, anchor=None): path = path.relative_to(DOMAIN_ROOTDIR, simple=True).replace('\\', '/').lstrip('/') @@ -22,92 +47,129 @@ def webpath(path, anchor=None): return path class Photo: - def __init__(self, filepath): - self.filepath = filepath - self.thumbnail = make_thumbnail(filepath) - self.article_id = filepath.replace_extension('').basename - self.anchor = f'#{self.article_id}' - self.published = imagetools.get_exif_datetime(filepath) + def __init__(self, etq_photo, etq_album=None): + self.etq_photo = etq_photo + self.article_id = self.etq_photo.real_path.replace_extension('').basename + + if etq_album is None: + parent_key = 'photography' + else: + parent_key = f'photography/{etq_album.title}' + + self.s3_key = f'{parent_key}/{self.etq_photo.real_path.basename}' + self.small_key = f'{parent_key}/small_{self.etq_photo.real_path.basename}' + self.tiny_key = f'{parent_key}/tiny_{self.etq_photo.real_path.basename}' + + self.color_class = 'monochrome' if self.etq_photo.has_tag('monochrome') else '' + + self.s3_exists = self.s3_key in S3_EXISTING_FILES; + self.img_url = f'{S3_WEBROOT}/{self.s3_key}' + self.small_url = f'{S3_WEBROOT}/{self.small_key}' + self.tiny_url = f'{S3_WEBROOT}/{self.tiny_key}' + self.anchor_url = f'{DOMAIN_WEBROOT}/{parent_key}#{self.article_id}' + self.published = imagetools.get_exif_datetime(self.etq_photo.real_path) + + def prepare(self): + if not self.s3_exists: + self.s3_upload() + + def make_thumbnail(self, size): + image = PIL.Image.open(self.etq_photo.real_path.absolute_path) + icc = image.info.get('icc_profile') + (image_width, image_height) = image.size + exif = image.getexif() + (width, height) = imagetools.fit_into_bounds(image_width, image_height, size, size) + image = image.resize((width, height), PIL.Image.LANCZOS) + bio = io.BytesIO() + image.save(bio, format='jpeg', quality=75, exif=exif, icc_profile=icc) + bio.seek(0) + return bio + + def s3_upload(self): + log.info('Uploading %s as %s', self.etq_photo.real_path.absolute_path, self.s3_key) + bucket.upload_fileobj(self.make_thumbnail(SIZE_SMALL), self.small_key) + bucket.upload_fileobj(self.make_thumbnail(SIZE_TINY), self.tiny_key) + bucket.upload_fileobj(self.etq_photo.real_path.open('rb'), self.s3_key) + self.s3_exists = True + + def render_web(self, index=None, totalcount=None): + if totalcount is not None: + number_tag = f'#{index}/{totalcount}' + else: + number_tag = '' - def render_web(self, relative_directory=None): return f''' -
- +
+ + {number_tag}
''' def render_atom(self): - href = webpath(PHOTOGRAPHY_ROOTDIR, anchor=self.anchor) - imgsrc = webpath(self.thumbnail) return f''' {self.article_id} {self.article_id} - + {self.published.isoformat()} + ]]> ''' class Album: - def __init__(self, path): - self.path = path - self.article_id = path.basename - self.link = webpath(path) - self.published = imagetools.get_exif_datetime(sorted(path.glob_files('*.jpg'))[0]) - self.photos = list(spinal.walk( - self.path, - glob_filenames={'*.jpg'}, - exclude_filenames={'*_small*'}, - recurse=False, - yield_directories=False, - )) - self.photos.sort(key=lambda file: file.basename) - self.photos = [Photo(file) for file in self.photos] + def __init__(self, etq_album): + self.etq_album = etq_album + self.article_id = self.etq_album.title + self.photos = list(self.etq_album.get_photos()) + self.photos = [p for p in self.photos if p.has_tag(PUBLISH_TAGNAME)] + self.photos = [Photo(etq_photo=photo, etq_album=self.etq_album) for photo in self.photos] + self.photos.sort(key=lambda p: p.published) + # self.link = webpath(path) + self.web_url = f'{PHOTOGRAPHY_WEBROOT}/{self.article_id}' + self.published = self.photos[0].published - def render_web(self): - firsts = self.photos[:5] - remaining = self.photos[5:] - if remaining: - next_after_more = remaining[0] - else: - next_after_more = None + def prepare(self): + for photo in self.photos: + photo.prepare() + + def render_web(self, index=None, totalcount=None): + headliners = [p for p in self.photos if p.etq_photo.has_tag(HEADLINER_TAGNAME)] return jinja2.Template(''' ''').render( article_id=self.article_id, - directory=self.path, - album_path=webpath(self.path), - next_after_more=next_after_more, - firsts=firsts, - remaining=len(remaining), + web_url=self.web_url, + photos=self.photos, + headliners=headliners, ) def render_atom(self): photos = [] for photo in self.photos: - href = webpath(photo.filepath) - imgsrc = webpath(photo.thumbnail) - line = f'
'.replace('\\', '/') + line = f'
'.replace('\\', '/') photos.append(line) photos = '\n'.join(photos) return f''' {self.article_id} {self.article_id} - + {self.published.isoformat()} @@ -158,10 +202,229 @@ def write_directory_index(directory): {% if rss_link %} {% endif %} - {{directory.basename}} + {{doctitle}} @@ -178,9 +441,47 @@ def write_directory_index(directory): {%- endif -%} + {% if not is_root %} +

{{doctitle}}

+ {% endif %} + {% for item in items %} - {{item.render_web()}} + {{item.render_web(index=loop.index, totalcount=none if is_root else (items|length))}} {% endfor %} + + +
+

Ethan Dalool

+

Contact me: photography@voussoir.net

+
+ +

+ + ''').render( - css_content=CSS_CONTENT, - directory=directory, + is_root=is_root, + doctitle=doctitle, rss_link=rss_link, back_link=back_link, items=items, ) - write(directory.with_child('index.html'), page) - - if rss_link: - write_atom(items) + return html def write_atom(items): atom = jinja2.Template(''' voussoir.net/photography - + voussoir.net/photography {% for item in items %} @@ -343,21 +736,41 @@ def write_atom(items): '''.strip()).render(items=items) write(ATOM_FILE, atom) -def make_thumbnail(photo): - small_name = photo.replace_extension('').basename + '_small' - small_name = photo.parent.with_child(small_name).add_extension(photo.extension) - if small_name.is_file: - return small_name - image = PIL.Image.open(photo.absolute_path) - icc = image.info.get('icc_profile') - (image_width, image_height) = image.size - exif = image.getexif() - (width, height) = imagetools.fit_into_bounds(image_width, image_height, 1440, 1440) - image = image.resize((width, height), PIL.Image.LANCZOS) - image.save(small_name.absolute_path, quality=75, exif=exif, icc_profile=icc) - print(small_name) - return small_name +# write_directory_index(PHOTOGRAPHY_ROOTDIR) +# for directory in PHOTOGRAPHY_ROOTDIR.walk_directories(): +# write_directory_index(directory) -write_directory_index(PHOTOGRAPHY_ROOTDIR) -for directory in PHOTOGRAPHY_ROOTDIR.walk_directories(): - write_directory_index(directory) +@vlogging.main_decorator +def main(argv): + singlephotos = list(pdb.search(tag_mays=[PUBLISH_TAGNAME], has_albums=False, yield_albums=False, yield_photos=True).results) + singlephotos += list(pdb.search(tag_mays=['voussoir_net_publish_single'], yield_albums=False, yield_photos=True).results) + singlephotos = [Photo(p) for p in singlephotos] + singlephotos.sort(key=lambda i: i.published, reverse=True) + + albums = list(pdb.search(tag_musts=[PUBLISH_TAGNAME], tag_forbids=['voussoir_net_publish_single'], has_albums=True, yield_albums=True, yield_photos=False).results) + albums = [Album(a) for a in albums] + albums.sort(key=lambda i: i.published, reverse=True) + + items = singlephotos + albums + items.sort(key=lambda i: i.published, reverse=True) + + for item in items: + item.prepare() + + log.info('Writing homepage') + homepage_html = make_webpage(items, is_root=True, doctitle='photography') + homepage_file = PHOTOGRAPHY_ROOTDIR.with_child('photography.html') + homepage_file.write('w', homepage_html) + + for album in albums: + album_html = make_webpage(album.photos, is_root=False, doctitle=album.article_id) + album_file = PHOTOGRAPHY_ROOTDIR.with_child(album.article_id).replace_extension('html') + log.info('Writing %s', album_file.absolute_path) + album_file.write('w', album_html) + + write_atom(items) + + return 0 + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:]))