diff --git a/_plugins/gallery_items.rb b/_plugins/gallery_items.rb index 0ad0144..e17efd3 100644 --- a/_plugins/gallery_items.rb +++ b/_plugins/gallery_items.rb @@ -1,6 +1,53 @@ require 'nokogiri' module GalleryItems + JPEG_START_OF_FRAME = [0xC0, 0xC1, 0xC2, 0xC3, 0xC5, 0xC6, 0xC7, + 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF].freeze + + # Read just the dimensions from a local JPEG. The rendered gallery needs + # aspect ratios to size each chronological pair without cropping either photo. + def jpeg_dimensions(path) + File.open(path, 'rb') do |file| + return nil unless file.read(2) == "\xFF\xD8".b + + while (prefix = file.read(1)) + next unless prefix.getbyte(0) == 0xFF + + marker = file.read(1)&.getbyte(0) + break unless marker + next if marker == 0xFF || marker == 0x01 || (0xD0..0xD7).cover?(marker) + break if marker == 0xD9 || marker == 0xDA + + size = file.read(2)&.unpack1('n') + break unless size && size >= 2 + if JPEG_START_OF_FRAME.include?(marker) + dimensions = file.read(5) + return nil unless dimensions&.length == 5 + + height, width = dimensions.byteslice(1, 4).unpack('n2') + return [width, height] if width.positive? && height.positive? + return nil + end + file.seek(size - 2, IO::SEEK_CUR) + end + end + nil + rescue Errno::ENOENT, EOFError + nil + end + + def gallery_image_ratio(content) + image = Nokogiri::HTML::DocumentFragment.parse(content).at_css('img') + source = image&.[]('src') + return 1.5 unless source&.start_with?('/img/arts/') + + path = File.join(File.expand_path('..', __dir__), source.delete_prefix('/')) + width, height = jpeg_dimensions(path) + return 1.5 unless width && height + + (width.to_f / height).round(4) + end + # Uploaded batches share one source document, but each photo is a gallery item. # Keeping this at render time also fixes batches that have already been published. def gallery_items(content) diff --git a/_sass/_layout.scss b/_sass/_layout.scss index d30a369..1997798 100644 --- a/_sass/_layout.scss +++ b/_sass/_layout.scss @@ -227,16 +227,22 @@ hr { } .art .post-list { - column-count: 2; - column-gap: $spacing-unit; + .gallery-row { + display: flex; + gap: $spacing-unit; + align-items: flex-start; + list-style: none; + margin: 0 0 $spacing-unit; - > li { - display: inline-block; - width: 100%; - margin-top: 0; - margin-bottom: $spacing-unit; - break-inside: avoid; - -webkit-column-break-inside: avoid; + > li { + flex: var(--photo-ratio) 1 0; + min-width: 0; + margin: 0; + } + + > li:only-child { + flex: 0 0 calc((100% - #{$spacing-unit}) / 2); + } } .post-content > p:last-child { @@ -249,11 +255,17 @@ hr { height: auto; margin: 0; // Reserve space before lazy images load, then use their natural proportions. - aspect-ratio: auto 3 / 2; + aspect-ratio: auto var(--photo-ratio, 3 / 2); } @include media-query($on-palm) { - column-count: 1; + .gallery-row { + display: block; + + > li { + margin-bottom: $spacing-unit; + } + } } } diff --git a/art.html b/art.html index 7e6fd71..8f13e72 100644 --- a/art.html +++ b/art.html @@ -28,14 +28,17 @@ wide_gallery: true -
subscribe via rss
diff --git a/scripts/tests/gallery_items_test.rb b/scripts/tests/gallery_items_test.rb index e21f26b..9d5d752 100644 --- a/scripts/tests/gallery_items_test.rb +++ b/scripts/tests/gallery_items_test.rb @@ -27,4 +27,10 @@ single = '

Existing artwork content
' raise 'Legacy artwork changed' unless renderer.gallery_items(legacy) == [legacy] + +portrait = '
'
+landscape = '
'
+raise 'Portrait ratio incorrect' unless renderer.gallery_image_ratio(portrait) < 1
+raise 'Landscape ratio incorrect' unless renderer.gallery_image_ratio(landscape) > 1
+raise 'Video fallback ratio incorrect' unless renderer.gallery_image_ratio('') == 1.5
puts 'Gallery item regression checks passed'