]> git.sesse.net Git - pr0n/commitdiff
Fix an (irrelevant) confusion about addEventListener. master
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 19 Mar 2023 23:33:07 +0000 (00:33 +0100)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 19 Mar 2023 23:33:07 +0000 (00:33 +0100)
doc/README
files/pr0n-fullscreen.css
files/pr0n-fullscreen.js
perl/Sesse/pr0n/Common.pm
perl/Sesse/pr0n/Image.pm
perl/Sesse/pr0n/Index.pm
perl/Sesse/pr0n/Upload.pm
perl/Sesse/pr0n/pr0n.pm
perl/make-jxl.pl [new file with mode: 0755]
perl/update-image-cache.pl
sql/pr0n.sql

index 42d467cebac4044c883ff89dc75fb58056999858..3b088c15f601d834fdfeec2266524c2dbf97e589 100644 (file)
@@ -15,6 +15,11 @@ backend pr0n {
 sub vcl_recv {
     if (req.http.host ~ "^pr0n\.sesse\.net(:[0-9]+)?$") {
         set req.backend_hint = pr0n;
 sub vcl_recv {
     if (req.http.host ~ "^pr0n\.sesse\.net(:[0-9]+)?$") {
         set req.backend_hint = pr0n;
+        if (req.http.accept ~ "(^|,)image/avif($|,|;)") {
+            set req.http.accept = "image/avif,*/*";
+        } else {
+            set req.http.accept = "*/*";
+        }
         if (req.method == "PUT") {
             return (pipe);
         }
         if (req.method == "PUT") {
             return (pipe);
         }
index b501128cd667a2ab7fa6a1d7cbf6fddfa65e1ab4..d4f897af0d05a1c9729001a199f628c6494b7344 100644 (file)
@@ -10,6 +10,9 @@ body {
 .container {
        text-align: center;
 }
 .container {
        text-align: center;
 }
+.container #optionmenu {
+       text-align: left;
+}
 
 img, #text {
        vertical-align: middle;
 
 img, #text {
        vertical-align: middle;
@@ -63,6 +66,17 @@ img, #text {
        display: none;
 }
 
        display: none;
 }
 
+.fsbox {
+       position: absolute;
+       transform-origin: top left;
+       background-color: white;
+       color: black;
+       font: 12px sans-serif;
+       line-height: 24px;
+       border-top: 1px solid black;
+       z-index: 1;
+}
+
 html, body {
        overflow: hidden;
 }
 html, body {
        overflow: hidden;
 }
index 076c626a9a81c49dc59a22189ead0eebef165f83..f08605cb1640f47490c13ec3b7ce3aa19559d6a4 100644 (file)
@@ -104,7 +104,7 @@ function rename_element(old_name, new_name)
        return elem;
 }
 
        return elem;
 }
 
-function display_image(url, backend_width, backend_height, elem_id, offset, box)
+function display_image(url, backend_width, backend_height, elem_id, offset)
 {
        // See if this image already exists in the DOM; if not, add it.
        var img = document.getElementById(elem_id);
 {
        // See if this image already exists in the DOM; if not, add it.
        var img = document.getElementById(elem_id);
@@ -112,7 +112,7 @@ function display_image(url, backend_width, backend_height, elem_id, offset, box)
                img = document.createElement("img");
                img.id = elem_id;
                img.alt = "";
                img = document.createElement("img");
                img.id = elem_id;
                img.alt = "";
-               img.className = box ? "fsbox" : "fsimg";
+               img.className = "fsimg";
        }
        img.style.position = "absolute";
        img.style.transformOrigin = "top left";
        }
        img.style.position = "absolute";
        img.style.transformOrigin = "top left";
@@ -120,7 +120,7 @@ function display_image(url, backend_width, backend_height, elem_id, offset, box)
 
        if (offset === 0) {
                img.src = url;
 
        if (offset === 0) {
                img.src = url;
-               position_image(img, backend_width, backend_height, offset, box);
+               position_image(img, backend_width, backend_height, offset, false);
        } else {
                // This is a preload, so wait for the main image to be ready.
                // The test for .complete is an old IE hack, which I don't know if is relevant anymore.
        } else {
                // This is a preload, so wait for the main image to be ready.
                // The test for .complete is an old IE hack, which I don't know if is relevant anymore.
@@ -128,15 +128,44 @@ function display_image(url, backend_width, backend_height, elem_id, offset, box)
                if (main_img === null || main_img.complete) {
                        img.src = url;
                } else {
                if (main_img === null || main_img.complete) {
                        img.src = url;
                } else {
-                       main_img.addEventListener('load', function() { img.src = url; }, false);
+                       main_img.addEventListener('load', function() { img.src = url; }, { 'once': true });
                }
 
                // Seemingly one needs to delay position_image(), or Firefox will set the initial
                // scroll offset completely off.
                img.style.display = 'none';
                setTimeout(function() {
                }
 
                // Seemingly one needs to delay position_image(), or Firefox will set the initial
                // scroll offset completely off.
                img.style.display = 'none';
                setTimeout(function() {
-                       position_image(img, backend_width, backend_height, offset, box);
-                       img.style.display = null;
+                       img.style.display = null;  // Must be done before position_image(), for measurement.
+                       position_image(img, backend_width, backend_height, offset, false);
+               }, 1);
+       }
+}
+
+function display_infobox(html, backend_width, backend_height, elem_id, offset)
+{
+       // See if this image already exists in the DOM; if not, add it.
+       var box = document.getElementById(elem_id);
+       if (box === null) {
+               box = document.createElement("div");
+               box.id = elem_id;
+               box.alt = "";
+               box.className = "fsbox";
+       }
+       box.style.position = "absolute";
+       box.style.transformOrigin = "top left";
+       box.innerHTML = html;
+       document.getElementById("main").appendChild(box);
+
+       if (offset === 0) {
+               position_image(box, backend_width, backend_height, offset, true);
+       } else {
+               // This is a preload.
+               // Seemingly one needs to delay position_image(), or Firefox will set the initial
+               // scroll offset completely off.
+               box.style.display = 'none';
+               setTimeout(function() {
+                       box.style.display = null;  // Must be done before position_image(), for measurement.
+                       position_image(box, backend_width, backend_height, offset, true);
                }, 1);
        }
 }
                }, 1);
        }
 }
@@ -160,19 +189,11 @@ function display_image_num(num, offset)
        var url = window.location.origin + "/" + evt + "/" + backend_width + "x" + backend_height + "/" + filename;
        var elem_id = num;
 
        var url = window.location.origin + "/" + evt + "/" + backend_width + "x" + backend_height + "/" + filename;
        var elem_id = num;
 
-       display_image(url, adjusted_size[2], adjusted_size[3], elem_id, offset, false);
+       display_image(url, adjusted_size[2], adjusted_size[3], elem_id, offset);
 
        if (global_infobox) {
 
        if (global_infobox) {
-               var url;
-               var dpr = find_dpr();
                var elem_id = num + "_box";
                var elem_id = num + "_box";
-               if (dpr == 1) {
-                       url = window.location.origin + "/" + evt + "/" + backend_width + "x" + backend_height + "/box/" + filename;
-               } else {
-                       url = window.location.origin + "/" + evt + "/" + backend_width + "x" + backend_height + "@" + dpr.toFixed(2) + "/box/" + filename;
-               }
-               display_image(url, adjusted_size[2], adjusted_size[3], elem_id, offset, true);
-               document.getElementById(elem_id).style.transform += " scale(" + (1.0 / dpr) + ")";
+               display_infobox(global_image_list[num][4], adjusted_size[2], adjusted_size[3], elem_id, offset);
        }
 
        if (offset === 0) {
        }
 
        if (offset === 0) {
@@ -253,7 +274,13 @@ function position_image(img, backend_width, backend_height, offset, box)
        img.style.transform = "translate(" + extra_x_offset + "px,0px)";
 
        if (box) {
        img.style.transform = "translate(" + extra_x_offset + "px,0px)";
 
        if (box) {
-               img.style.top = Math.min(top + height, screen_size[1] - 24) / dpr + "px";
+               img.style.top = Math.min(top + height, screen_size[1] - 24 * dpr) / dpr + "px";
+               img.style.height = "24px";
+               img.style.width = null;
+               img.style.whiteSpace = 'nowrap';
+               // Hide the box if there's no room for all the text.
+               img.style.opacity = (img.clientWidth < width / dpr + 10) ? null : 0.0;
+               img.style.width = (width / dpr) + "px";
        } else {
                img.style.top = (top / dpr) + "px";
                img.style.lineHeight = (height / dpr) + "px";
        } else {
                img.style.top = (top / dpr) + "px";
                img.style.lineHeight = (height / dpr) + "px";
@@ -288,7 +315,7 @@ function update_shown_images()
                //    inum !== global_image_num + 1) {
                //      to_remove.push(child);
                //}
                //    inum !== global_image_num + 1) {
                //      to_remove.push(child);
                //}
-               if (inum !== global_image_num) {
+               if (inum !== global_image_num || (child.className === "fsbox" && !global_infobox)) {
                        to_remove.push(child);
                }
        }
                        to_remove.push(child);
                }
        }
@@ -531,7 +558,7 @@ function set_swipe_pos(x, transition)
                        var inum = parseInt(child.id.replace("_box", ""));
                        var offset = inum - global_image_num;
                        child.style.transition = transition;
                        var inum = parseInt(child.id.replace("_box", ""));
                        var offset = inum - global_image_num;
                        child.style.transition = transition;
-                       child.style.transform = "translate(" + (x + find_width()[0] * offset / dpr) + "px,0px) scale(" + (1.0 / dpr) + ")";
+                       child.style.transform = "translate(" + (x + find_width()[0] * offset / dpr) + "px,0px)";
                }
        }
 }
                }
        }
 }
index 72be2184d99a84bcc9245939a791ffa488cc6b9e..101cee6283603405dfcc5830dd9e277e837a0613 100644 (file)
@@ -22,6 +22,7 @@ use HTML::Entities;
 use URI::Escape;
 use File::Basename;
 use Crypt::Eksblowfish::Bcrypt;
 use URI::Escape;
 use File::Basename;
 use Crypt::Eksblowfish::Bcrypt;
+use File::Temp;
 
 BEGIN {
        use Exporter ();
 
 BEGIN {
        use Exporter ();
@@ -207,26 +208,15 @@ sub get_dbh {
 
 sub get_disk_location {
        my ($r, $id) = @_;
 
 sub get_disk_location {
        my ($r, $id) = @_;
-        my $dir = POSIX::floor($id / 256);
+       my $dir = POSIX::floor($id / 256);
        return $Sesse::pr0n::Config::image_base . "images/$dir/$id.jpg";
 }
 
 sub get_cache_location {
        return $Sesse::pr0n::Config::image_base . "images/$dir/$id.jpg";
 }
 
 sub get_cache_location {
-       my ($id, $width, $height) = @_;
-        my $dir = POSIX::floor($id / 256);
-
-       return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-$width-$height-nobox.jpg";
-}
-
-sub get_infobox_cache_location {
-       my ($id, $width, $height, $dpr) = @_;
-        my $dir = POSIX::floor($id / 256);
+       my ($id, $width, $height, $format) = @_;
+       my $dir = POSIX::floor($id / 256);
 
 
-       if ($dpr == 1) {
-               return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-$width-$height-box.png";
-       } else {
-               return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-$width-$height-box\@$dpr.png";
-       }
+       return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-$width-$height-nobox.$format";
 }
 
 sub ensure_disk_location_exists {
 }
 
 sub ensure_disk_location_exists {
@@ -236,19 +226,21 @@ sub ensure_disk_location_exists {
        my $img_dir = $Sesse::pr0n::Config::image_base . "/images/$dir/";
        if (! -d $img_dir) {
                log_info($r, "Need to create new image directory $img_dir");
        my $img_dir = $Sesse::pr0n::Config::image_base . "/images/$dir/";
        if (! -d $img_dir) {
                log_info($r, "Need to create new image directory $img_dir");
-               mkdir($img_dir) or die "Couldn't create new image directory $img_dir";
+               mkdir($img_dir);  # Ignore errors, there could be a race.
+               -d $img_dir or die "Couldn't create new image directory $img_dir";
        }
 
        my $cache_dir = $Sesse::pr0n::Config::image_base . "/cache/$dir/";
        if (! -d $cache_dir) {
                log_info($r, "Need to create new cache directory $cache_dir");
        }
 
        my $cache_dir = $Sesse::pr0n::Config::image_base . "/cache/$dir/";
        if (! -d $cache_dir) {
                log_info($r, "Need to create new cache directory $cache_dir");
-               mkdir($cache_dir) or die "Couldn't create new image directory $cache_dir";
+               mkdir($cache_dir);  # Ignore errors, there could be a race.
+               -d $cache_dir or die "Couldn't create new cache directory $cache_dir";
        }
 }
 
 sub get_mipmap_location {
        my ($r, $id, $width, $height) = @_;
        }
 }
 
 sub get_mipmap_location {
        my ($r, $id, $width, $height) = @_;
-        my $dir = POSIX::floor($id / 256);
+       my $dir = POSIX::floor($id / 256);
 
        return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-mipmap-$width-$height.jpg";
 }
 
        return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-mipmap-$width-$height.jpg";
 }
@@ -527,17 +519,9 @@ sub read_original_image {
        }
        my $err;
 
        }
        my $err;
 
-       # ImageMagick can handle NEF files, but it does it by calling dcraw as a delegate.
-       # The delegate support is rather broken and causes very odd stuff to happen when
-       # more than one thread does this at the same time. Thus, we simply do it ourselves.
        if ($filename =~ /\.(nef|cr2)$/i) {
        if ($filename =~ /\.(nef|cr2)$/i) {
-               # this would suffice if ImageMagick gets to fix their handling
-               # $physical_fname = "NEF:$physical_fname";
-               
-               open DCRAW, "-|", "dcraw", "-w", "-c", $physical_fname
-                       or error("dcraw: $!");
-               $err = $magick->Read(file => \*DCRAW);
-               close(DCRAW);
+               $physical_fname = "NEF:$physical_fname";
+               $err = $magick->Read($physical_fname);
        } else {
                # We always want YCbCr JPEGs. Setting this explicitly here instead of using
                # RGB is slightly faster (no colorspace conversion needed) and works equally
        } else {
                # We always want YCbCr JPEGs. Setting this explicitly here instead of using
                # RGB is slightly faster (no colorspace conversion needed) and works equally
@@ -572,17 +556,26 @@ sub read_original_image {
 }
 
 sub ensure_cached {
 }
 
 sub ensure_cached {
-       my ($r, $filename, $id, $dbwidth, $dbheight, $xres, $yres, @otherres) = @_;
-
-       my ($new_dbwidth, $new_dbheight);
+       my ($r, $avif_ok, $jxl_ok, $filename, $id, $dbwidth, $dbheight, $xres, $yres, @otherres) = @_;
 
        my $fname = get_disk_location($r, $id);
        unless (defined($xres) && (!defined($dbwidth) || !defined($dbheight) || $xres < $dbwidth || $yres < $dbheight || $xres == -1)) {
                return ($fname, undef);
        }
 
 
        my $fname = get_disk_location($r, $id);
        unless (defined($xres) && (!defined($dbwidth) || !defined($dbheight) || $xres < $dbwidth || $yres < $dbheight || $xres == -1)) {
                return ($fname, undef);
        }
 
-       my $cachename = get_cache_location($id, $xres, $yres);
-       my $err;
+       # See if we have an up-to-date JPEG-XL or AVIF to serve.
+       # (We never generate them on-the-fly, since they're so slow.)
+       my $cachename = get_cache_location($id, $xres, $yres, 'jxl');
+       if ($jxl_ok && -r $cachename and (-M $cachename <= -M $fname)) {
+               return ($cachename, 'image/jxl');
+       }
+
+       $cachename = get_cache_location($id, $xres, $yres, 'avif');
+       if ($avif_ok && -r $cachename and (-M $cachename <= -M $fname)) {
+               return ($cachename, 'image/avif');
+       }
+
+       $cachename = get_cache_location($id, $xres, $yres, 'jpg');
        if (! -r $cachename or (-M $cachename > -M $fname)) {
                # If we are in overload mode (aka Slashdot mode), refuse to generate
                # new thumbnails.
        if (! -r $cachename or (-M $cachename > -M $fname)) {
                # If we are in overload mode (aka Slashdot mode), refuse to generate
                # new thumbnails.
@@ -591,140 +584,119 @@ sub ensure_cached {
                        error($r, 'System is in overload mode, not doing any scaling');
                }
 
                        error($r, 'System is in overload mode, not doing any scaling');
                }
 
-               my $img;
-               ($img, $new_dbwidth, $new_dbheight) = make_mipmap($r, $filename, $id, $dbwidth, $dbheight, $xres, $yres, @otherres);
-
-               while (defined($xres) && defined($yres)) {
-                       my ($nxres, $nyres) = (shift @otherres, shift @otherres);
-                       my $cachename = get_cache_location($id, $xres, $yres);
-                       
-                       my $cimg;
-                       if (defined($nxres) && defined($nyres)) {
-                               # we have more resolutions to scale, so don't throw
-                               # the image away
-                               $cimg = $img->Clone();
-                       } else {
-                               $cimg = $img;
-                       }
-               
-                       my $width = $img->Get('columns');
-                       my $height = $img->Get('rows');
-                       my ($nwidth, $nheight) = scale_aspect($width, $height, $xres, $yres);
-
-                       my $filter = 'Lanczos';
-                       my $quality = 87;
-                       my $sf = "1x1";
-
-                       if ($xres != -1) {
-                               $cimg->Resize(width=>$nwidth, height=>$nheight, filter=>$filter, 'sampling-factor'=>$sf);
-                       }
-
-                       # Strip EXIF tags etc.
-                       $cimg->Strip();
+               make_cache($r, $filename, $id, $dbwidth, $dbheight, 'jpg', $xres, $yres, @otherres);
+       }
 
 
-                       {
-                               my %parms = (
-                                       filename => $cachename,
-                                       quality => $quality
-                               );
-                               if (($nwidth >= 640 && $nheight >= 480) ||
-                                   ($nwidth >= 480 && $nheight >= 640)) {
-                                       $parms{'interlace'} = 'Plane';
-                               }
-                               if (defined($sf)) {
-                                       $parms{'sampling-factor'} = $sf;
-                               }
-                               $err = $cimg->write(%parms);
-                       }
+       return ($cachename, 'image/jpeg');
+}
 
 
-                       undef $cimg;
+sub make_cache {
+       my ($r, $filename, $id, $dbwidth, $dbheight, $format, $xres, $yres, @otherres) = @_;
 
 
-                       ($xres, $yres) = ($nxres, $nyres);
+       my ($img, $new_dbwidth, $new_dbheight) = make_mipmap($r, $filename, $id, $dbwidth, $dbheight, $xres, $yres, @otherres);
 
 
-                       log_info($r, "New cache: $nwidth x $nheight for $id.jpg");
-               }
-               
-               undef $img;
-               if ($err) {
-                       log_warn($r, "$fname: $err");
-                       $err =~ /(\d+)/;
-                       if ($1 >= 400) {
-                               #@$magick = ();
-                               error($r, "$fname: $err");
-                       }
-               }
-       }
-       
        # Update the SQL database if it doesn't contain the required info
        if (!defined($dbwidth) && defined($new_dbwidth)) {
                log_info($r, "Updating width/height for $id: $new_dbwidth x $new_dbheight");
                update_image_info($r, $id, $new_dbwidth, $new_dbheight);
        }
 
        # Update the SQL database if it doesn't contain the required info
        if (!defined($dbwidth) && defined($new_dbwidth)) {
                log_info($r, "Updating width/height for $id: $new_dbwidth x $new_dbheight");
                update_image_info($r, $id, $new_dbwidth, $new_dbheight);
        }
 
-       return ($cachename, 'image/jpeg');
-}
-
-sub ensure_infobox_cached {
-       my ($r, $filename, $id, $dbwidth, $dbheight, $dpr, $xres, $yres) = @_;
+       my $err;
+       while (defined($xres) && defined($yres)) {
+               my ($nxres, $nyres) = (shift @otherres, shift @otherres);
+               my $cachename = get_cache_location($id, $xres, $yres, $format);
+               
+               my $cimg;
+               if (defined($nxres) && defined($nyres)) {
+                       # we have more resolutions to scale, so don't throw
+                       # the image away
+                       $cimg = $img->Clone();
+               } else {
+                       $cimg = $img;
+               }
+       
+               my $width = $img->Get('columns');
+               my $height = $img->Get('rows');
+               my ($nwidth, $nheight) = scale_aspect($width, $height, $xres, $yres);
 
 
-       my ($new_dbwidth, $new_dbheight);
+               my $filter = 'Lanczos';
+               my $quality = 87;
+               my $sf = "1x1";
 
 
-       my $fname = get_disk_location($r, $id);
-       my $cachename = get_infobox_cache_location($id, $xres, $yres, $dpr);
-       my $err;
-       if (! -r $cachename or (-M $cachename > -M $fname)) {
-               # If we are in overload mode (aka Slashdot mode), refuse to generate
-               # new thumbnails.
-               if (Sesse::pr0n::Overload::is_in_overload($r)) {
-                       log_warn($r, "In overload mode, not scaling $id to $xres x $yres");
-                       error($r, 'System is in overload mode, not doing any scaling');
+               if ($xres != -1) {
+                       $cimg->Resize(width=>$nwidth, height=>$nheight, filter=>$filter);
                }
 
                }
 
-               # We need the exact width so we can make one in the right size.
-               my ($width, $height);
+               # Strip EXIF tags etc.
+               $cimg->Strip();
 
 
-               # This is slow, but should fortunately almost never happen, so don't bother
-               # special-casing it.
-               if (!defined($dbwidth) || !defined($dbheight)) {
-                       my $img = read_original_image($r, $filename, $id, $dbwidth, $dbheight, 0);
-                       $new_dbwidth = $width = $img->Get('columns');
-                       $new_dbheight = $height = $img->Get('rows');
+               if ($format eq 'jpg') {
+                       my %parms = (
+                               filename => $cachename,
+                               quality => $quality
+                       );
+                       if (($nwidth >= 640 && $nheight >= 480) ||
+                           ($nwidth >= 480 && $nheight >= 640)) {
+                               $parms{'interlace'} = 'Plane';
+                       }
+                       if (defined($sf)) {
+                               $parms{'sampling-factor'} = $sf;
+                       }
+                       $err = $cimg->write(%parms);
+               } elsif ($format eq 'avif') {
+                       # ImageMagick doesn't have AVIF support until version 7,
+                       # and Debian hasn't packaged that even in unstable as of 2021.
+                       # So we'll need to do it the manual way. (We don't use /tmp, for security reasons.)
+                       (my $dirname = $cachename) =~ s,/[^/]*$,,;
+                       my ($fh, $raw_filename) = File::Temp::tempfile('tmp.XXXXXXXX', DIR => $dirname, SUFFIX => '.ycbcr');
+                       # Write a Y4M header, so that we get the chroma range correct.
+                       printf $fh "YUV4MPEG2 W%d H%d F25:1 Ip A1:1 C444 XYSCSS=444 XCOLORRANGE=FULL\nFRAME\n", $nwidth, $nheight;
+                       my %parms = (
+                               file => $fh,
+                               filename => $raw_filename,
+                               interlace => 'Plane'
+                       );
+                       $cimg->write(%parms);
+                       close($fh);
+                       my $ivf_filename;
+                       ($fh, $ivf_filename) = File::Temp::tempfile('tmp.XXXXXXXX', DIR => $dirname, SUFFIX => '.ivf');
+                       close($fh);
+                       system('aomenc', '--quiet', '--cpu-used=0', '--bit-depth=10', '--end-usage=q', '--cq-level=13', '--target-bitrate=0', '--good', '--aq-mode=1', '--matrix-coefficients=bt601', '-o', $ivf_filename, $raw_filename);
+                       unlink($raw_filename);
+                       system('MP4Box', '-quiet', '-add-image', "$ivf_filename:primary", '-ab', 'avif', '-ab', 'miaf', '-new', $cachename);
+                       unlink($ivf_filename);
+               } elsif ($format eq 'jxl') {
+                       # Similar, for JPEG-XL.
+                       (my $dirname = $cachename) =~ s,/[^/]*$,,;
+                       my ($fh, $raw_filename) = File::Temp::tempfile('tmp.XXXXXXXX', DIR => $dirname, SUFFIX => '.ppm');
+                       my %parms = (
+                               file => $fh,
+                               filename => $raw_filename
+                       );
+                       $cimg->write(%parms);
+                       close($fh);
+                       system('cjxl', '-p', $raw_filename, $cachename);
+                       unlink($raw_filename);
                } else {
                } else {
-                       $width = $dbwidth;
-                       $height = $dbheight;
+                       die "Unknown format $format";
                }
                }
-               my $img = Image::Magick->new;
 
 
-               if (defined($xres) && defined($yres)) {
-                       ($width, $height) = scale_aspect($width, $height, $xres, $yres);
-               }
-               $height = 24 * $dpr;
-               $img->Set(size=>($width . "x" . $height));
-               $img->Read('xc:white');
-
-               my $info = Image::ExifTool::ImageInfo($fname);
-               if (make_infobox($img, $info, $r, $dpr)) {
-                       $img->Quantize(colors=>16, dither=>'False');
-
-                       # Since the image is grayscale, ImageMagick overrides us and writes this
-                       # as grayscale anyway, but at least we get rid of the alpha channel this
-                       # way.
-                       $img->Set(type=>'Palette');
-               } else {
-                       # Not enough room for the text, make a tiny dummy transparent infobox
-                       @$img = ();
-                       $img->Set(size=>"1x1");
-                       $img->Read('null:');
+               undef $cimg;
 
 
-                       $width = 1;
-                       $height = 1;
-               }
+               ($xres, $yres) = ($nxres, $nyres);
 
 
-               $err = $img->write(filename => $cachename, quality => 90, depth => 8);
-               log_info($r, "New infobox cache: $width x $height for $id.jpg");
+               log_info($r, "New cache: $nwidth x $nheight ($format) for $id");
+       }
+       
+       undef $img;
+       if ($err) {
+               log_warn($r, "$filename: $err");
+               $err =~ /(\d+)/;
+               if ($1 >= 400) {
+                       #@$magick = ();
+                       error($r, "$filename: $err");
+               }
        }
        }
-
-       return ($cachename, 'image/png');
 }
 
 sub get_mimetype_from_filename {
 }
 
 sub get_mimetype_from_filename {
@@ -734,8 +706,8 @@ sub get_mimetype_from_filename {
        return $type;
 }
 
        return $type;
 }
 
-sub make_infobox {
-       my ($img, $info, $r, $dpr) = @_;
+sub make_infobox_parts {
+       my ($info) = @_;
 
        # The infobox is of the form
        # "Time - date - focal length, shutter time, aperture, sensitivity, exposure bias - flash",
 
        # The infobox is of the form
        # "Time - date - focal length, shutter time, aperture, sensitivity, exposure bias - flash",
@@ -850,57 +822,7 @@ sub make_infobox {
                }
        }
 
                }
        }
 
-       return 0 if (scalar @parts == 0);
-
-       # Find the required width
-       my $th = 0;
-       my $tw = 0;
-
-       for my $part (@parts) {
-               my $font;
-               if ($part->[1]) {
-                       $font = '/usr/share/fonts/truetype/msttcorefonts/Arial_Bold.ttf';
-               } else {
-                       $font = '/usr/share/fonts/truetype/msttcorefonts/Arial.ttf';
-               }
-
-               my (undef, undef, $h, undef, $w) = ($img->QueryFontMetrics(text=>$part->[0], font=>$font, pointsize=>12*$dpr));
-
-               $tw += $w;
-               $th = $h if ($h > $th);
-       }
-
-       return 0 if ($tw > $img->Get('columns'));
-
-       my $x = 0;
-       my $y = $img->Get('rows') - 24*$dpr;
-
-       # Hit exact DCT blocks
-       $y -= ($y % 8);
-
-       my $points = sprintf "%u,%u %u,%u", $x, $y, ($img->Get('columns') - 1), ($img->Get('rows') - 1);
-       my $lpoints = sprintf "%u,%u %u,%u", $x, $y, ($img->Get('columns') - 1), $y;
-       $img->Draw(primitive=>'rectangle', stroke=>'white', fill=>'white', points=>$points);
-       $img->Draw(primitive=>'line', stroke=>'black', strokewidth=>$dpr, points=>$lpoints);
-
-       # Start writing out the text
-       $x = ($img->Get('columns') - $tw) / 2;
-
-       my $room = ($img->Get('rows') - $dpr - $y - $th);
-       $y = ($img->Get('rows') - $dpr) - $room/2;
-       
-       for my $part (@parts) {
-               my $font;
-               if ($part->[1]) {
-                       $font = '/usr/share/fonts/truetype/msttcorefonts/Arial_Bold.ttf';
-               } else {
-                       $font = '/usr/share/fonts/truetype/msttcorefonts/Arial.ttf';
-               }
-               $img->Annotate(text=>$part->[0], font=>$font, pointsize=>12*$dpr, x=>int($x), y=>int($y));
-               $x += ($img->QueryFontMetrics(text=>$part->[0], font=>$font, pointsize=>12*$dpr))[4];
-       }
-
-       return 1;
+       return @parts;
 }
 
 sub gcd {
 }
 
 sub gcd {
@@ -1004,10 +926,6 @@ sub get_all_cache_urls {
                        push @ret, "/$event/$1x$2/$filename";
                } elsif ($fname =~ /^$id-(\d+)-(\d+)-nobox\.jpg$/) {
                        push @ret, "/$event/$1x$2/nobox/$filename";
                        push @ret, "/$event/$1x$2/$filename";
                } elsif ($fname =~ /^$id-(\d+)-(\d+)-nobox\.jpg$/) {
                        push @ret, "/$event/$1x$2/nobox/$filename";
-               } elsif ($fname =~ /^$id--1--1-box\.png$/) {
-                       push @ret, "/$event/box/$filename";
-               } elsif ($fname =~ /^$id-(\d+)-(\d+)-box\.png$/) {
-                       push @ret, "/$event/$1x$2/box/$filename";
                } else {
                        log_warn($r, "Couldn't find a purging URL for $fname");
                }
                } else {
                        log_warn($r, "Couldn't find a purging URL for $fname");
                }
@@ -1032,7 +950,7 @@ sub get_server_name {
 
 sub log_info {
        my ($r, $msg) = @_;
 
 sub log_info {
        my ($r, $msg) = @_;
-       if (defined($r->logger)) {
+       if (defined($r->{'logger'})) {
                $r->logger->({ level => 'info', message => $msg });
        } else {
                print STDERR "[INFO] $msg\n";
                $r->logger->({ level => 'info', message => $msg });
        } else {
                print STDERR "[INFO] $msg\n";
@@ -1041,7 +959,7 @@ sub log_info {
 
 sub log_warn {
        my ($r, $msg) = @_;
 
 sub log_warn {
        my ($r, $msg) = @_;
-       if (defined($r->logger)) {
+       if (defined($r->{'logger'})) {
                $r->logger->({ level => 'warn', message => $msg });
        } else {
                print STDERR "[WARN] $msg\n";
                $r->logger->({ level => 'warn', message => $msg });
        } else {
                print STDERR "[WARN] $msg\n";
@@ -1050,7 +968,7 @@ sub log_warn {
 
 sub log_error {
        my ($r, $msg) = @_;
 
 sub log_error {
        my ($r, $msg) = @_;
-       if (defined($r->logger)) {
+       if (defined($r->{'logger'})) {
                $r->logger->({ level => 'error', message => $msg });
        } else {
                print STDERR "[ERROR] $msg\n";
                $r->logger->({ level => 'error', message => $msg });
        } else {
                print STDERR "[ERROR] $msg\n";
index f12a15891792ed1ba0e195e815c4c157df2d1b29..f37e7ac8f986a09ce750a1258ad899e2be017770 100644 (file)
@@ -15,24 +15,20 @@ sub handler {
 
        # Find the event and file name (nobox/ is for compatibility with legacy URLs).
        my ($event,$filename,$xres,$yres,$dpr);
 
        # Find the event and file name (nobox/ is for compatibility with legacy URLs).
        my ($event,$filename,$xres,$yres,$dpr);
-       my $infobox = 0;
-       if ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/original/((?:no)?box/)?([a-zA-Z0-9._()-]+)$#) {
+       if ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/original/?([a-zA-Z0-9._()-]+)$#) {
                $event = $1;
                $event = $1;
-               $filename = $3;
-               $infobox = 1 if (defined($2) && $2 eq 'box/');
-       } elsif ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/(\d+)x(\d+)(?:\@(\d+(?:\.\d+)?))?/((?:no)?box/)?([a-zA-Z0-9._()-]+)$#) {
+               $filename = $2;
+       } elsif ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/(\d+)x(\d+)(?:\@(\d+(?:\.\d+)?))?/([a-zA-Z0-9._()-]+)$#) {
                $event = $1;
                $event = $1;
-               $filename = $6;
+               $filename = $5;
                $xres = $2;
                $yres = $3;
                $dpr = $4;
                $xres = $2;
                $yres = $3;
                $dpr = $4;
-               $infobox = 1 if (defined($5) && $5 eq 'box/');
-       } elsif ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/((?:no)?box/)?([a-zA-Z0-9._()-]+)$#) {
+       } elsif ($r->path_info =~ m#^/([a-zA-Z0-9-]+)/([a-zA-Z0-9._()-]+)$#) {
                $event = $1;
                $event = $1;
-               $filename = $3;
+               $filename = $2;
                $xres = -1;
                $yres = -1;
                $xres = -1;
                $yres = -1;
-               $infobox = 1 if (defined($2) && $2 eq 'box/');
        }
        $dpr //= 1;
 
        }
        $dpr //= 1;
 
@@ -46,8 +42,8 @@ sub handler {
                undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename);
        return error($r, "Could not find $event/$filename", 404, "File not found") unless (defined($ref));
 
                undef, $event, Sesse::pr0n::Common::get_server_name($r), $filename);
        return error($r, "Could not find $event/$filename", 404, "File not found") unless (defined($ref));
 
-       if (defined($xres) && defined($yres) && defined($ref->{'render_id'}) && !$infobox) {
-               # We have a render, we're not asked for the original, and we do not have infobox.
+       if (defined($xres) && defined($yres) && defined($ref->{'render_id'})) {
+               # We have a render, and we're not asked for the original.
                $ref = $dbh->selectrow_hashref('SELECT id,filename,width,height FROM images WHERE id=?', 
                        undef, $ref->{'render_id'});
                return error($r, "Could not find render of $event/$filename", 404, "File not found") unless (defined($ref));
                $ref = $dbh->selectrow_hashref('SELECT id,filename,width,height FROM images WHERE id=?', 
                        undef, $ref->{'render_id'});
                return error($r, "Could not find render of $event/$filename", 404, "File not found") unless (defined($ref));
@@ -58,16 +54,17 @@ sub handler {
        $dbwidth = $ref->{'width'};
        $dbheight = $ref->{'height'};
 
        $dbwidth = $ref->{'width'};
        $dbheight = $ref->{'height'};
 
+       my $res = Plack::Response->new(200);
+
        # Scale if we need to do so
        my ($fname, $mime_type);
        # Scale if we need to do so
        my ($fname, $mime_type);
-       if ($infobox) {
-               ($fname, $mime_type) = Sesse::pr0n::Common::ensure_infobox_cached($r, $filename, $id, $dbwidth, $dbheight, $dpr, $xres, $yres);
-       } else {
-               ($fname, $mime_type) = Sesse::pr0n::Common::ensure_cached($r, $filename, $id, $dbwidth, $dbheight, $xres, $yres);
-       }
+       my $accept = $r->header('Accept');
+       my $avif_ok = (defined($accept) && $accept =~ /(^|,)image\/avif($|,|;)/);
+       my $jxl_ok = (defined($accept) && $accept =~ /(^|,)image\/jxl($|,|;)/);
+       ($fname, $mime_type) = Sesse::pr0n::Common::ensure_cached($r, $avif_ok, $jxl_ok, $filename, $id, $dbwidth, $dbheight, $xres, $yres);
+       $res->header('Vary' => 'Accept');
 
        # Output the image to the user
 
        # Output the image to the user
-       my $res = Plack::Response->new(200);
 
        if (!defined($mime_type)) {
                $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
 
        if (!defined($mime_type)) {
                $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
index cb83a9252933dcb9c19dae44de0b13666c835b7b..d72be98bc82799abc98e5f01d1d8a9171eb5fb81 100644 (file)
@@ -58,7 +58,7 @@ sub handler {
        
        my $where;
        if ($event eq '+all') {
        
        my $where;
        if ($event eq '+all') {
-               $where = '';
+               $where = ' AND (event,vhost) IN ( SELECT event,vhost FROM events WHERE NOT hidden )';
        } else {
                $where = ' AND event=' . $dbh->quote($event);
        }
        } else {
                $where = ' AND event=' . $dbh->quote($event);
        }
@@ -161,7 +161,51 @@ sub handler {
        # Find all images related to this event.
        my $limit = (defined($start) && defined($num) && !$settings{'fullscreen'}) ? (" LIMIT $num OFFSET " . ($start-1)) : "";
 
        # Find all images related to this event.
        my $limit = (defined($start) && defined($num) && !$settings{'fullscreen'}) ? (" LIMIT $num OFFSET " . ($start-1)) : "";
 
-       my $q = $dbh->prepare("SELECT *, (date - INTERVAL '6 hours')::date AS day FROM images WHERE vhost=? $where AND NOT is_render ORDER BY (date - INTERVAL '6 hours')::date $datesort,takenby,date,filename $limit")
+       my $extra_joins = "";
+       my $extra_fields = "";
+       if ($settings{'fullscreen'}) {
+               $extra_joins = <<"EOF";
+    LEFT JOIN exif_info exif_prog ON images.id=exif_prog.image AND exif_prog.key = 'ExposureProgram'
+    LEFT JOIN exif_info exif_focal ON images.id=exif_focal.image AND exif_focal.key = 'FocalLength'
+    LEFT JOIN exif_info exif_shutter ON images.id=exif_shutter.image AND exif_shutter.key = 'ExposureTime'
+    LEFT JOIN exif_info exif_fnum ON images.id=exif_fnum.image AND exif_fnum.key = 'FNumber'
+    LEFT JOIN exif_info exif_iso1 ON images.id=exif_iso1.image AND exif_iso1.key = 'ISO'
+    LEFT JOIN exif_info exif_iso2 ON images.id=exif_iso2.image AND exif_iso2.key = 'ISOSetting'
+    LEFT JOIN exif_info exif_ev1 ON images.id=exif_ev1.image AND exif_ev1.key = 'ExposureBiasValue'
+    LEFT JOIN exif_info exif_ev2 ON images.id=exif_ev2.image AND exif_ev2.key = 'ExposureCompensation'
+    LEFT JOIN exif_info exif_date ON images.id=exif_date.image AND exif_date.key = 'DateTimeOriginal'
+    LEFT JOIN exif_info exif_model ON images.id=exif_model.image AND exif_model.key = 'Model'
+    LEFT JOIN exif_info exif_flash ON images.id=exif_flash.image AND exif_flash.key = 'Flash'
+EOF
+               $extra_fields = <<"EOF";
+    exif_prog.value AS "ExposureProgram",
+    exif_focal.value AS "FocalLength",
+    exif_shutter.value AS "ExposureTime",
+    exif_fnum.value AS "FNumber",
+    exif_iso1.value AS "ISO",
+    exif_iso2.value AS "ISOSetting",
+    exif_ev1.value AS "ExposureBiasValue",
+    exif_ev2.value AS "ExposureCompensation",
+    exif_date.value AS "DateTimeOriginal",
+    exif_model.value AS "Model",
+    exif_flash.value AS "Flash",
+EOF
+       }
+
+       my $q = $dbh->prepare(<<"EOF")
+SELECT *,
+  $extra_fields
+  (date - INTERVAL '6 hours')::date AS day
+FROM
+  images
+  $extra_joins
+WHERE
+  vhost=?
+  $where
+  AND NOT is_render
+ORDER BY (date - INTERVAL '6 hours')::date $datesort,takenby,date,filename
+$limit
+EOF
                or return dberror($r, "prepare()");
        $q->execute(Sesse::pr0n::Common::get_server_name($r))
                or return dberror($r, "image enumeration");
                or return dberror($r, "prepare()");
        $q->execute(Sesse::pr0n::Common::get_server_name($r))
                or return dberror($r, "image enumeration");
@@ -178,11 +222,13 @@ sub handler {
                while (my $ref = $q->fetchrow_hashref()) {
                        my $width = defined($ref->{'width'}) ? $ref->{'width'} : -1;
                        my $height = defined($ref->{'height'}) ? $ref->{'height'} : -1;
                while (my $ref = $q->fetchrow_hashref()) {
                        my $width = defined($ref->{'width'}) ? $ref->{'width'} : -1;
                        my $height = defined($ref->{'height'}) ? $ref->{'height'} : -1;
-                       push @files, [ $ref->{'event'}, $ref->{'filename'}, $width, $height ];
+                       my @parts = Sesse::pr0n::Common::make_infobox_parts($ref);
+                       @parts = map { $_->[1] ? "<strong>" . HTML::Entities::encode_entities($_->[0]) . "</strong>" : HTML::Entities::encode_entities($_->[0]) } @parts;
+                       push @files, [ $ref->{'event'}, $ref->{'filename'}, $width, $height, join('', @parts) ];
                }
                
                for my $i (0..$#files) {
                }
                
                for my $i (0..$#files) {
-                       my $line = sprintf "        [ \"%s\", \"%s\", %d, %d ]", @{$files[$i]};
+                       my $line = sprintf "        [ \"%s\", \"%s\", %d, %d, \"%s\" ]", @{$files[$i]};
                        $line .= "," unless ($i == $#files);
                        $io->print($line . "\n");
                }
                        $line .= "," unless ($i == $#files);
                        $io->print($line . "\n");
                }
index 6f44f7ac12a32b69ec5345f109b0e21a358db045..0fdce3ad9ea553f0c4f93268b3bb6c137d1c064f 100644 (file)
@@ -97,7 +97,7 @@ sub handler {
                                # Make cache while we're at it.
                                # FIXME: Ideally we'd want to ensure cache of -1x-1 here as well (for NEFs), but that would
                                # preclude mipmapping in its current form.
                                # Make cache while we're at it.
                                # FIXME: Ideally we'd want to ensure cache of -1x-1 here as well (for NEFs), but that would
                                # preclude mipmapping in its current form.
-                               Sesse::pr0n::Common::ensure_cached($r, $filename, $newid, undef, undef, 320, 256);
+                               Sesse::pr0n::Common::ensure_cached($r, 0, 0, $filename, $newid, undef, undef, 320, 256);
                                
                                # OK, we got this far, commit
                                $dbh->commit;
                                
                                # OK, we got this far, commit
                                $dbh->commit;
index 3c9161b94b1da8b117db489c8b4b6c6c89ae7736..2517b849b7e0439df869c56c9c36e4a7605ad132 100644 (file)
@@ -71,7 +71,6 @@ sub handler {
                return Sesse::pr0n::Index::handler($r);
        } elsif ($uri =~ m#^/[a-zA-Z0-9-]+/
                           (\d+x\d+ ( \@\d+(\.\d+)? )? / | original/ )?
                return Sesse::pr0n::Index::handler($r);
        } elsif ($uri =~ m#^/[a-zA-Z0-9-]+/
                           (\d+x\d+ ( \@\d+(\.\d+)? )? / | original/ )?
-                           ((?:no)?box/)?
                            [a-zA-Z0-9._()-]+$#x) {
                return Sesse::pr0n::Image::handler($r);
        }
                            [a-zA-Z0-9._()-]+$#x) {
                return Sesse::pr0n::Image::handler($r);
        }
diff --git a/perl/make-jxl.pl b/perl/make-jxl.pl
new file mode 100755 (executable)
index 0000000..0c5b708
--- /dev/null
@@ -0,0 +1,45 @@
+#! /usr/bin/perl
+
+use lib qw(.);
+use DBI;
+use POSIX;
+use Sesse::pr0n::Common;
+use strict;
+use warnings;
+
+use Sesse::pr0n::Config;
+eval {
+       require Sesse::pr0n::Config_local;
+};
+
+my $dbh = DBI->connect("dbi:Pg:dbname=pr0n;host=" . $Sesse::pr0n::Config::db_host,
+       $Sesse::pr0n::Config::db_username, $Sesse::pr0n::Config::db_password)
+       or die "Couldn't connect to PostgreSQL database: " . DBI->errstr;
+$dbh->{RaiseError} = 1;
+
+# TODO: Do we need to care about renders?
+for my $id (@ARGV) {
+       my $dir = POSIX::floor($id / 256);
+       my $base = $Sesse::pr0n::Config::image_base . "cache/$dir";
+       my @res = ();
+       for my $file (<$base/$id-*-nobox.jpg>) {   # TODO: --1--1.jpg, too.
+               my $fname = File::Basename::basename($file);
+               my ($width, $height) = $fname =~ /^$id-(\d+)-(\d+)-nobox\.jpg$/ or die $fname;
+               (my $jxl_file = $file) =~ s/jpg$/jxl/;
+               unless (-r $jxl_file) {
+                       push @res, ($width, $height);
+                       print "$id to $width x $height...\n";
+               }
+       }
+       if (scalar @res > 0) {
+               my $filename = Sesse::pr0n::Common::get_disk_location({}, $id);
+
+               # Look up the width/height in the database.
+               my ($dbwidth, $dbheight);
+               my $ref = $dbh->selectrow_hashref('SELECT width,height FROM images WHERE id=?', undef, $id);
+               $dbwidth = $ref->{'width'};
+               $dbheight = $ref->{'height'};
+
+               Sesse::pr0n::Common::make_cache({}, $filename, $id, $dbwidth, $dbheight, 'jxl', @res);
+       }       
+}
index 0ed6931bfe63ca4a2fcf648fa3e03202d765360b..f50d3a0b046b5539d820bbe8365a52dbc398845e 100755 (executable)
@@ -103,7 +103,6 @@ while (my $ref = $q->fetchrow_hashref) {
        if (!$regen_mipmaps) {
                @files = grep { !/mipmap/ } @files;
        }
        if (!$regen_mipmaps) {
                @files = grep { !/mipmap/ } @files;
        }
-       my @boxres = ();
        my @noboxres = ();
        my $any_old = 0;
        for my $c (@files) {
        my @noboxres = ();
        my $any_old = 0;
        for my $c (@files) {
@@ -113,8 +112,6 @@ while (my $ref = $q->fetchrow_hashref) {
                }
                if ($c =~ /$id-(\d+)-(\d+)-nobox\.jpg/ || $c =~ /$id-(-1)-(-1)-nobox\.jpg/) {
                        push @noboxres, [$1, $2];
                }
                if ($c =~ /$id-(\d+)-(\d+)-nobox\.jpg/ || $c =~ /$id-(-1)-(-1)-nobox\.jpg/) {
                        push @noboxres, [$1, $2];
-               } elsif ($c =~ /$id-(\d+)-(\d+)-box\.png/ || $c =~ /$id-(-1)-(-1)-box\.png/) {
-                       push @boxres, [$1, $2];
                }
        }
        next unless $any_old;
                }
        }
        next unless $any_old;
@@ -122,9 +119,6 @@ while (my $ref = $q->fetchrow_hashref) {
        if (scalar @noboxres > 0) {
                Sesse::pr0n::Common::ensure_cached($r, $ref->{'filename'}, $id, $ref->{'width'}, $ref->{'height'}, sort_res(@noboxres));
        }
        if (scalar @noboxres > 0) {
                Sesse::pr0n::Common::ensure_cached($r, $ref->{'filename'}, $id, $ref->{'width'}, $ref->{'height'}, sort_res(@noboxres));
        }
-       if (scalar @boxres > 0) {
-               Sesse::pr0n::Common::ensure_infobox_cached($r, $ref->{'filename'}, $id, $ref->{'width'}, $ref->{'height'}, 1, sort_res(@boxres));
-       }
        
        my @newfiles = glob("../cache/$dir/$id-*.jpg");
        my %a = map { $_ => 1 } @files;
        
        my @newfiles = glob("../cache/$dir/$id-*.jpg");
        my %a = map { $_ => 1 } @files;
index 371e05cf4fdfef765c35318276fef867dad6ac57..d3541ed27656b7d420995cae56c14b16d1cb1561 100644 (file)
@@ -71,7 +71,8 @@ CREATE TABLE users (
 );
 
 -- Mainly used for manual queries -- usually too slow to be very useful
 );
 
 -- Mainly used for manual queries -- usually too slow to be very useful
--- for web views in the long run.
+-- for web views in the long run (except for the special case of the
+-- partial index below).
 CREATE TABLE exif_info (
     image integer NOT NULL REFERENCES images (id) ON DELETE CASCADE,
     key varchar NOT NULL,
 CREATE TABLE exif_info (
     image integer NOT NULL REFERENCES images (id) ON DELETE CASCADE,
     key varchar NOT NULL,
@@ -81,6 +82,9 @@ CREATE TABLE exif_info (
 );
 
 CLUSTER exif_info_pkey ON exif_info;
 );
 
 CLUSTER exif_info_pkey ON exif_info;
+CREATE UNIQUE INDEX exif_info_fast_listing ON exif_info (image, key) INCLUDE (value)
+  WHERE key IN ('ExposureProgram', 'FocalLength', 'ExposureTime', 'FNumber', 'ISO', 'ISOSetting',
+                'ExposureBiasValue', 'ExposureCompensation', 'DateTimeOriginal', 'Model', 'Flash');
 
 GRANT INSERT ON TABLE deleted_images TO pr0n;
 GRANT INSERT,SELECT,UPDATE,DELETE ON TABLE events TO pr0n;
 
 GRANT INSERT ON TABLE deleted_images TO pr0n;
 GRANT INSERT,SELECT,UPDATE,DELETE ON TABLE events TO pr0n;