Skip to content

Commit e8f7b3e

Browse files
committed
Home: photo strip picks up new album photos automatically
Every album photo that is not in the hand-written `ticker:` list of index.html is now added to the strip (captioned "<album>, <city>"), in front of the curated ones. Image sizes for width/height are read from the file headers (JPEG/PNG/WebP) in _plugins/lab_pictures.rb, so the strip does not shift while photos load.
1 parent bad079c commit e8f7b3e

3 files changed

Lines changed: 77 additions & 5 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ Albums are listed at `/lab_pictures/` (linked from the footer and the People pag
306306
- The front matter is `title`, `name`, `location`, `folder` (the image folder) and `avatar` (the cover, relative to `images/lab_pictures/`).
307307
- The body lists captioned figures; each shows the `web/...webp` file and links to the original. Give every `<img>` its real `width` and `height` and a short `alt`.
308308
- Photos in the folder that the body does not list are still shown after them, without captions.
309-
3. Optional, for the Home photo strip: add a line to the `ticker:` list in the front matter of `index.html` (`src` is relative to `images/lab_pictures/`, e.g. `web/corl2022/DSC03166.webp`, plus `w`, `h`, `alt`, `cap`). Sponsor logos are the `sponsors:` list in the same place (files in `images/logo/`).
309+
3. **Home photo strip:** every album photo appears there automatically, captioned "<album>, <city>". To give a photo its own caption (or a fixed place in the strip), add a line to the `ticker:` list in the front matter of `index.html` (`src` is relative to `images/lab_pictures/`, e.g. `web/corl2022/DSC03166.webp`, plus `w`, `h`, `alt`, `cap`). Sponsor logos are the `sponsors:` list in the same place (files in `images/logo/`).
310310

311311
## Design system
312312

‎_plugins/lab_pictures.rb‎

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
# A folder without a _lab_pictures/*.md album gets a generated page at /lab_pictures/<folder>/
1818
# (layout "albums", title = the folder name in words, data 'auto_album' = true). _layouts/albums.html lists, after an
1919
# album's hand-written content, every photo of its folder that the content does not mention yet.
20+
#
21+
# Home photo strip: site.data['lab_ticker'] = every album photo that is not in the hand-written `ticker:`
22+
# list of index.html (new uploads first, captioned "<album>, <city>"), followed by that list. Each item has
23+
# 'src' (relative to images/lab_pictures/), 'w', 'h', 'alt', 'cap'. Image sizes are read from the files.
2024

2125
require 'jekyll'
2226

@@ -43,20 +47,23 @@ def generate(site)
4347

4448
src = "#{ROOT}/#{folder}/#{file}"
4549
webp = "#{ROOT}/web/#{folder}/#{stem}.webp"
46-
{ 'file' => file, 'stem' => stem, 'src' => src,
47-
'web' => File.exist?(File.join(site.source, webp)) ? webp : src }
50+
web = File.exist?(File.join(site.source, webp)) ? webp : src
51+
w, h = image_size(File.join(site.source, web))
52+
{ 'file' => file, 'stem' => stem, 'src' => src, 'web' => web, 'w' => w, 'h' => h }
4853
end
4954
photos[folder] = list unless list.empty?
5055
end
5156
site.data['lab_photos'] = photos
5257

5358
albums = site.collections['lab_pictures']&.docs || []
5459
covered = []
60+
names = {}
5561
albums.each do |doc|
5662
folder = doc.data['folder'] || doc.data['avatar'].to_s.split('/').first
5763
doc.data['folder'] = folder
5864
covered << folder
5965
doc.data['avatar_src'] = cover(site, folder, doc.data['avatar'], photos)
66+
names[folder] = [doc.data['title'] || doc.data['name'], doc.data['location']]
6067
end
6168

6269
(photos.keys - covered).each do |folder|
@@ -65,11 +72,75 @@ def generate(site)
6572
page.data.merge!('layout' => 'albums', 'title' => folder.tr('_-', ' ').split.map(&:capitalize).join(' '), 'folder' => folder,
6673
'auto_album' => true, 'avatar_src' => cover(site, folder, nil, photos))
6774
site.pages << page
75+
names[folder] = [page.data['title'], nil]
6876
end
77+
78+
site.data['lab_ticker'] = ticker(site, photos, names)
6979
end
7080

7181
private
7282

83+
# The hand-written strip from index.html's front matter, preceded by every other album photo.
84+
def ticker(site, photos, names)
85+
home = site.pages.find { |pg| pg.url == '/' }
86+
curated = (home&.data&.fetch('ticker', nil) || []).map { |t| t.transform_keys(&:to_s) }
87+
listed = curated.map { |t| t['src'] }
88+
extra = photos.flat_map do |folder, list|
89+
album, location = names[folder]
90+
cap = [album, location.to_s.split(',').first].compact.reject(&:empty?).join(', ')
91+
list.filter_map do |ph|
92+
rel = ph['web'].delete_prefix("#{ROOT}/")
93+
next if listed.include?(rel) || ph['w'].nil?
94+
95+
{ 'src' => rel, 'w' => ph['w'], 'h' => ph['h'], 'alt' => "Photo from #{album}", 'cap' => cap }
96+
end
97+
end
98+
extra + curated
99+
end
100+
101+
# [width, height] from a JPEG, PNG or WebP header, or nil.
102+
def image_size(path)
103+
File.open(path, 'rb') do |f|
104+
head = f.read(30).to_s
105+
if head.start_with?("\x89PNG".b)
106+
head[16, 8].unpack('NN')
107+
elsif head[0, 4] == 'RIFF' && head[8, 4] == 'WEBP'
108+
case head[12, 4]
109+
when 'VP8 ' then head[26, 4].unpack('vv').map { |v| v & 0x3fff }
110+
when 'VP8L'
111+
b = head[21, 4].unpack1('V')
112+
[(b & 0x3fff) + 1, ((b >> 14) & 0x3fff) + 1]
113+
when 'VP8X' then [head[24, 3], head[27, 3]].map { |v| "#{v}\0".unpack1('V') + 1 }
114+
end
115+
elsif head.start_with?("\xFF\xD8".b)
116+
jpeg_size(f)
117+
end
118+
end
119+
rescue StandardError
120+
nil
121+
end
122+
123+
def jpeg_size(f)
124+
f.seek(2)
125+
while (b = f.read(1))
126+
next unless b.ord == 0xFF
127+
128+
m = f.read(1)&.ord
129+
m = f.read(1)&.ord while m == 0xFF
130+
return nil unless m
131+
next if m == 0x01 || (0xD0..0xD8).cover?(m)
132+
133+
len = f.read(2)&.unpack1('n') or return nil
134+
if (0xC0..0xCF).cover?(m) && ![0xC4, 0xC8, 0xCC].include?(m)
135+
f.read(1)
136+
h, w = f.read(4).unpack('nn')
137+
return [w, h]
138+
end
139+
f.seek(len - 2, IO::SEEK_CUR)
140+
end
141+
nil
142+
end
143+
73144
# Web path of an album cover: the avatar's WebP copy if it exists, else the avatar, else the first photo.
74145
def cover(site, folder, avatar, photos)
75146
if avatar && !avatar.to_s.empty?

‎index.html‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
---
22
layout: default
33
body_class: home
4-
# Lab-life ticker. src is under images/lab_pictures/ (WebP copies from scripts/optimize_images.py); w/h = pixel size (prevents layout shift).
4+
# Lab-life ticker: hand-picked photos with captions. Album photos not listed here are added automatically in front of them
5+
# (_plugins/lab_pictures.rb). src is under images/lab_pictures/ (WebP copies from scripts/optimize_images.py); w/h = pixel size.
56
ticker:
67
- { src: web/corl2022/DSC03166.webp, w: 1600, h: 1067, alt: "Sandeep, Shubhankar and Po-han in Devonport, Auckland", cap: "CoRL 2022, Auckland" }
78
- { src: web/rss2022/IMG_0909.webp, w: 1600, h: 1200, alt: "Sai, Oguzhan and Philip attending RSS", cap: "RSS 2022, New York" }
@@ -140,7 +141,7 @@ <h2 id="life-title">Out in the field</h2>
140141
<div class="ticker" id="ticker" role="group" aria-label="Photos from conferences and field trips">
141142
<ul class="ticker__track">
142143
{%- for round in (1..2) %}
143-
{%- for p in page.ticker %}
144+
{%- for p in site.data.lab_ticker %}{% comment %} = new album photos, then the ticker: list above (_plugins/lab_pictures.rb) {% endcomment %}
144145
<li class="ticker__item"{% if round == 2 %} aria-hidden="true"{% endif %}>
145146
<figure>
146147
<img src="/images/lab_pictures/{{ p.src }}" alt="{% if round == 2 %}{% else %}{{ p.alt }}{% endif %}" width="{{ p.w }}" height="{{ p.h }}" loading="lazy" decoding="async">

0 commit comments

Comments
 (0)