13 Commits

Author SHA1 Message Date
187a7cd02f GenericTable: prevent passing NULL to strtotime 2022-07-14 16:45:32 +02:00
8414843bbf Prevent current page from being 0 if no items are present 2022-07-14 16:45:17 +02:00
474c387786 Add double-density thumbnails to albums and photo pages 2022-07-08 23:53:28 +02:00
12407d797d Address deprecation notices for certain function signatures 2022-07-08 23:52:03 +02:00
64d7433a56 Thumbnails: crop from original size if 2x is unavailable 2022-07-08 23:49:29 +02:00
58b7204fbf Do not delete thumbnail queue when replacing an asset
Thumbnails are normally created on demand, e.g. when processing the format codes in a post's body text.
Normally, the temporary URL is only used once to generate thumbnails ad-hoc. However, when cache is
enabled, a reference to the asset may be used in a cached version of a formatted body text, skipping
the normal thumbnail generation routine.

When an asset is replaced, currently, all thumbnails are removed and references to them are removed
from the database. In case the asset is still referenced in a cached formatted body text, this could lead
to an error when requesting the thumbnail, as the thumbnail request is no longer present in the system.

As we do not know what posts use particular assets at this point in the code, it is best to work around this
issue by unsetting the thumbnail filenames rather than deleting the entries outright. This effectively
generates them again on the next request.

In the future, we should aim to keep track of what posts make use of assets, so cache may be invalidated
in a more targeted way.
2022-07-08 23:49:20 +02:00
36a2779381 Don't try to generate double-density thumbs for small images 2022-07-08 23:49:13 +02:00
44bb501d13 Write new thumbnail filenames to parent Image object as well 2022-07-08 23:48:53 +02:00
9010123d18 Thumbnail class: minor refactor of generate method 2022-07-08 23:48:45 +02:00
e3b67c4022 Thumbnail class: refactor getUrl method 2022-07-08 23:48:38 +02:00
2bcdc5fe6e Split Image::getImageUrls from Image::getInlineImage 2022-07-08 23:48:30 +02:00
edfad992cc Rewrite Image::getInlineImage to support double density displays 2022-07-08 23:48:19 +02:00
357d95f6ff Add Image::getInlineImage method 2022-07-08 23:47:55 +02:00
13 changed files with 95 additions and 115 deletions

7
.gitignore vendored
View File

@@ -1,2 +1,5 @@
composer-setup.php
composer.phar
.DS_Store
composer.lock
config.php
hashru.sublime-project
hashru.sublime-workspace

View File

@@ -13,22 +13,12 @@ The Kabuki codebase requires the following PHP extensions to be enabled for full
## Setup
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. For development, consider starting from `config-dev.php.dist`.
Ensure you have a MySQL database running with credentials matching your `config.php`. For development, consider the /dev/docker-compose.yml file.
Run `composer install`. If you do not have composer installed globally, run it from the project directory as follows:
```
wget -O composer-setup.php https://getcomposer.org/installer
php composer-setup.php --install-dir=.
php ./composer.phar install
```
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file.
## Running
For development purposes, simply run the `server` script provided in the root of this repository.
This will start a PHP development server on `127.0.0.1:8080`.
This will start a PHP development server on `hashru.local:8080`.
For a production environment, please set up a proper PHP-FPM environment instead.

View File

@@ -1,36 +0,0 @@
<?php
/*****************************************************************************
* config.php
* Contains general settings for the project.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
const DEBUG = true;
const CACHE_ENABLED = true;
const CACHE_KEY_PREFIX = 'hashru_';
// Basedir and base URL of the project.
const BASEDIR = __DIR__;
const BASEURL = 'http://127.0.0.1:8080'; // no trailing /
// Reply-To e-mail header address
const REPLY_TO_ADDRESS = 'no-reply@my.domain.tld';
// Assets dir and url, where assets are plentiful. (In wwwroot!)
const ASSETSDIR = BASEDIR . '/public/assets';
const ASSETSURL = BASEURL . '/assets';
// Thumbs dir and url, where thumbnails for assets reside.
const THUMBSDIR = BASEDIR . '/public/thumbs';
const THUMBSURL = BASEURL . '/thumbs';
// Database server, username, password, name
const DB_SERVER = '127.0.0.1';
const DB_USER = 'hashru';
const DB_PASS = 'hashru';
const DB_NAME = 'hashru_pics';
const DB_LOG_QUERIES = false;
const SITE_TITLE = 'HashRU Pics';
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';

View File

@@ -1,11 +0,0 @@
version: '3'
services:
mysql:
image: mysql:latest
ports:
- 3306:3306
environment:
MYSQL_USER: 'hashru'
MYSQL_PASSWORD: 'hashru'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: 'hashru_pics'

View File

@@ -606,12 +606,14 @@ class Asset
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
(a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
ORDER BY a.date_captured DESC, a.id_asset DESC'
a.date_captured <= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY a.date_captured DESC'
: '
FROM assets AS a
WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
ORDER BY date_captured ASC, a.id_asset ASC')
WHERE date_captured >= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY date_captured ASC')
. '
LIMIT 1',
[
@@ -637,12 +639,14 @@ class Asset
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
(a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
ORDER BY a.date_captured ASC, a.id_asset ASC'
a.date_captured >= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY a.date_captured ASC'
: '
FROM assets AS a
WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
ORDER BY date_captured DESC, a.id_asset DESC')
WHERE date_captured <= {datetime:date_captured} AND
a.id_asset != {int:id_asset}
ORDER BY date_captured DESC')
. '
LIMIT 1',
[

View File

@@ -488,7 +488,7 @@ class Database
/**
* This function can be used to insert data into the database in a secure way.
*/
public function insert($method = 'replace', $table, $columns, $data)
public function insert($method, $table, $columns, $data)
{
// With nothing to insert, simply return.
if (empty($data))

View File

@@ -43,7 +43,7 @@ class GenericTable
$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
// Figure out where we are on the whole, too.
$numPages = ceil($this->recordCount / $this->items_per_page);
$numPages = max(1, ceil($this->recordCount / $this->items_per_page));
$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
// Let's bear a few things in mind...
@@ -234,7 +234,9 @@ class GenericTable
else
$pattern = $options['data']['pattern'];
if (!is_numeric($rowData[$options['data']['timestamp']]))
if (!isset($rowData[$options['data']['timestamp']]))
$timestamp = 0;
elseif (!is_numeric($rowData[$options['data']['timestamp']]))
$timestamp = strtotime($rowData[$options['data']['timestamp']]);
else
$timestamp = (int) $rowData[$options['data']['timestamp']];

View File

@@ -67,14 +67,33 @@ class Image extends Asset
return EXIF::fromFile($this->getPath());
}
public function getPath()
public function getImageUrls($width = null, $height = null)
{
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
$image_urls = [];
if (isset($width) || isset($height))
{
$thumbnail = new Thumbnail($this);
$image_urls[1] = $this->getThumbnailUrl($width, $height, false);
// Can we afford to generate double-density thumbnails as well?
if ((!isset($width) || $this->image_width >= $width * 2) &&
(!isset($height) || $this->image_height >= $height * 2))
$image_urls[2] = $this->getThumbnailUrl($width * 2, $height * 2, false);
else
$image_urls[2] = $this->getThumbnailUrl($this->image_width, $this->image_height, true);
}
else
$image_urls[1] = $this->getUrl();
return $image_urls;
}
public function getUrl()
public function getInlineImage($width = null, $height = null, $className = 'inline-image')
{
return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
$image_urls = $this->getImageUrls($width, $height);
return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
}
/**
@@ -141,7 +160,8 @@ class Image extends Asset
}
return Registry::get('db')->query('
DELETE FROM assets_thumbs
UPDATE assets_thumbs
SET filename = NULL
WHERE id_asset = {int:id_asset}',
['id_asset' => $this->id_asset]);
}

View File

@@ -63,9 +63,9 @@ class PageIndex
lower current/cont. pgs. center upper
*/
$this->num_pages = ceil($this->recordCount / $this->items_per_page);
$this->num_pages = max(1, ceil($this->recordCount / $this->items_per_page));
$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
if ($this->num_pages == 0)
if ($this->num_pages <= 1)
{
$this->needsPageIndex = false;
return;

View File

@@ -9,6 +9,7 @@
class Thumbnail
{
private $image;
private $image_meta;
private $thumbnails;
private $properly_initialised;
@@ -23,7 +24,7 @@ class Thumbnail
const CROP_MODE_SLICE_CENTRE = 4;
const CROP_MODE_SLICE_BOTTOM = 5;
public function __construct($image)
public function __construct(Image $image)
{
$this->image = $image;
$this->image_meta = $image->getMeta();
@@ -45,51 +46,45 @@ class Thumbnail
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
if (!empty($this->thumbnails[$thumb_selector]))
{
$thumb_path = '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
if (file_exists(THUMBSDIR . $thumb_path))
return THUMBSURL . $thumb_path;
$thumb_filename = $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
if (file_exists(THUMBSDIR . '/' . $thumb_filename))
return THUMBSURL . '/' . $thumb_filename;
}
// Do we have a custom thumbnail on file?
$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
if (isset($this->image_meta[$custom_selector]))
{
$custom_thumb_path = '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
if (file_exists(ASSETSDIR . $custom_thumb_path))
$custom_filename = $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
if (file_exists(ASSETSDIR . '/' . $custom_filename))
{
// Ensure destination thumbnail directory exists.
if (!file_exists($this->image->getSubdir()))
@mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
// Copy the custom thumbail to the general thumbnail directory.
copy(ASSETSDIR . $custom_thumb_path, THUMBSDIR . $custom_thumb_path);
copy(ASSETSDIR . '/' . $custom_filename, THUMBSDIR . '/' . $custom_filename);
// Let's remember this for future reference.
$this->markAsGenerated($this->image_meta[$custom_selector]);
return THUMBSURL . $custom_thumb_path;
return THUMBSURL . '/' . $custom_filename;
}
else
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
}
// Is this the right moment to generate a thumbnail, then?
if ($generate && array_key_exists($thumb_selector, $this->thumbnails))
if ($generate)
{
return $this->generate();
if (array_key_exists($thumb_selector, $this->thumbnails))
return $this->generate();
else
throw new Exception("Trying to generate a thumbnail not previously queued by the system\n" .
print_r(func_get_args(), true));
}
// If not, queue it for generation at another time, and return a URL to generate it with.
elseif (!$generate)
{
$this->markAsQueued();
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $this->width . 'x' . $this->height . $this->filename_suffix . '/';
}
// Still here..? What are you up to? ..Sneaking?
else
{
throw new Exception("Trying to generate a thumbnail for selector " . $thumb_selector . ", which does not appear to have been requested by the system.\n" . print_r(func_get_args(), true));
$this->markAsQueued();
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $thumb_selector . '/';
}
}
@@ -260,14 +255,18 @@ class Thumbnail
'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
// Ensure the thumbnail subdirectory exists.
if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir()))
mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
$target_dir = THUMBSDIR . '/' . $this->image->getSubdir();
if (!is_dir($target_dir))
mkdir($target_dir, 0755, true);
if (!is_writable($target_dir))
throw new Exception('Thumbnail directory is not writable!');
// No need to preserve every detail.
$thumb->setImageCompressionQuality(80);
// Save it in a public spot.
$thumb->writeImage(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename);
$thumb->writeImage($target_dir . '/' . $thumb_filename);
// Let's remember this for future reference...
$this->markAsGenerated($thumb_filename);
@@ -278,7 +277,6 @@ class Thumbnail
// Finally, return the URL for the generated thumbnail image.
return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
}
// Blast! Curse your sudden but inevitable betrayal!
catch (ImagickException $e)
{
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
@@ -336,10 +334,16 @@ class Thumbnail
if ($success)
{
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : '';
}
$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : null;
return $success;
// For consistency, write new thumbnail filename to parent Image object.
// TODO: there could still be an inconsistency if multiple objects exists for the same image asset.
$this->image->getThumbnails()[$thumb_selector] = $this->thumbnails[$thumb_selector];
return $success;
}
else
throw new UnexpectedValueException('Thumbnail queuing query failed');
}
private function markAsQueued()

2
server
View File

@@ -1,2 +1,2 @@
#!/bin/bash
php -S 127.0.0.1:8080 -t public
php -S hashru.local:8080 -t public

View File

@@ -65,11 +65,9 @@ class PhotoPage extends SubTemplate
<a href="', $this->photo->getUrl(), '">';
if ($this->photo->isPortrait())
echo '
<img src="', $this->photo->getThumbnailUrl(null, 960), '" alt="">';
echo $this->photo->getInlineImage(null, 960);
else
echo '
<img src="', $this->photo->getThumbnailUrl(1280, null), '" alt="">';
echo $this->photo->getInlineImage(1280, null);
echo '
</a>

View File

@@ -90,8 +90,14 @@ class PhotosIndex extends SubTemplate
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
echo '
<a href="', $image->getPageUrl(), $this->url_suffix, '">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
<a href="', $image->getPageUrl(), $this->url_suffix, '#photo_frame">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"';
// Can we offer double-density thumbs?
if ($image->width() >= $width * 2 && $image->height() >= $height * 2)
echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"';
echo ' alt="" title="', $image->getTitle(), '">';
if ($this->show_labels)
echo '