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;
}
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);
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";
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.
// scroll offset completely off.
img.style.display = 'none';
setTimeout(function() {
- position_image(img, backend_width, backend_height, offset, box);
+ position_image(img, backend_width, backend_height, offset, false);
img.style.display = null;
}, 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() {
+ position_image(box, backend_width, backend_height, offset, true);
+ box.style.display = null;
+ }, 1);
+ }
+}
+
function display_image_num(num, offset)
{
var screen_size = find_width();
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) {
- var url;
- var dpr = find_dpr();
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 (box) {
img.style.top = Math.min(top + height, screen_size[1] - 24) / dpr + "px";
+ img.style.width = (width / dpr) + "px";
+ img.style.height = "24px";
} else {
img.style.top = (top / dpr) + "px";
img.style.lineHeight = (height / dpr) + "px";
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)";
}
}
}
return $Sesse::pr0n::Config::image_base . "cache/$dir/$id-$width-$height-nobox.$format";
}
-sub get_infobox_cache_location {
- my ($id, $width, $height, $dpr) = @_;
- 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";
- }
-}
-
sub ensure_disk_location_exists {
my ($r, $id) = @_;
my $dir = POSIX::floor($id / 256);
}
}
-sub ensure_infobox_cached {
- my ($r, $filename, $id, $dbwidth, $dbheight, $dpr, $xres, $yres) = @_;
-
- my ($new_dbwidth, $new_dbheight);
-
- 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');
- }
-
- # We need the exact width so we can make one in the right size.
- my ($width, $height);
-
- # 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');
- } else {
- $width = $dbwidth;
- $height = $dbheight;
- }
- 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:');
-
- $width = 1;
- $height = 1;
- }
-
- $err = $img->write(filename => $cachename, quality => 90, depth => 8);
- log_info($r, "New infobox cache: $width x $height for $id.jpg");
- }
-
- return ($cachename, 'image/png');
-}
-
sub get_mimetype_from_filename {
my $filename = shift;
my MIME::Type $type = $mimetypes->mimeTypeOf($filename);
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",
}
}
- 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 {
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");
}
# 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;
- $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;
- $filename = $6;
+ $filename = $5;
$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;
- $filename = $3;
+ $filename = $2;
$xres = -1;
$yres = -1;
- $infobox = 1 if (defined($2) && $2 eq 'box/');
}
$dpr //= 1;
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));
# 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 {
- 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');
- }
+ 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
# 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");
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) {
- 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");
}
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);
}
if (!$regen_mipmaps) {
@files = grep { !/mipmap/ } @files;
}
- my @boxres = ();
my @noboxres = ();
my $any_old = 0;
for my $c (@files) {
}
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;
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;
);
-- 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,
);
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;