Initial commit

This commit is contained in:
isUnknown 2026-02-12 15:22:46 +01:00
commit 65e0da7e11
1397 changed files with 596542 additions and 0 deletions

View file

@ -0,0 +1,46 @@
# ImageKit Changelog
- Recent changes (not part of an official release yet)
- Add redirect detection to Widget API, so the indexing feature works with templates, that only send a redirect to the browser instead of showing content.
- Re-Create placeholder files (`index.html` and `.gitkeep`), often used in Git repositories after thumbs cache has been cleaned.
- Discovery feature does now validate the `href` attribute of links, when indexing the whole site.
- `1.1.3` (2017/01/07)
- Add redirect detection to Widget API, so the indexing feature works with templates, that only send a redirect to the browser instead of showing content.
- `1.1.2` (2016/12/11)
- Remove Whoops handler to restore Widget compatibility with Kirby 2.4.1.
- `1.1.1` (2016/11/27)
- Fix path to panel JS
- `1.1.0` (2016/11/24)
- Fix error with overridden optimizer options.
- Make `imagekit.lazy` overridable for single thumbnails
- `1.1.0-beta2` (2016/10/29)
- Widget API now displays more helpful errors in most situations (only browsers, that support the `foreignContent` feature of SVG. Currently, most browsers will still only show the old error (but looks great in Firefox).
- Widget Errors are now recoverable. You dont have to reload the panel any more, if a server-side error occurs.
- Better integration with *Whoops* for error-handling for the panel widget.
- Confirm dialog for clearing thumbs folder is not displaying as overlay, rather than as system dialod. Supports ESC key for cancel and ENTER for confirming the action.
- `1.1.0-beta1` (2016/09/21)
- **Optimization:** ImageKit is now capable of applying several optimizations to your images, using popular command-line tools.
- **Better Error Handling:** The `ComplaingThumb` class now handles out-of-memory errors more reliable.
- **Compatibilitly:** Widget should now work with Kirby 2.4-beta1
- `1.0.0` (2016/08/19)
- **Release!** Initial version of the plugin is now final. Licenses are availabel at my [store](http://sites.fastspring.com/fabianmichael/product/imagekit).
- **Bugfix:** Fix handling of images that are located at the top-level of the `content` directory.
- `1.0.0-beta2` (2016/07/25)
- **Changed Job-File Suffix:** Pending thumbs aka placeholder files aka job files now have a suffix of `-imagekitjob.php` instead of `.imagekitjob.php`. This fixes errors with Apaches `MultiViews` feature (read [explanation](http://stackoverflow.com/questions/25423141/what-exactly-does-the-the-multiviews-options-in-htaccess)). You should clear your thumbs folder after upgrading.
- **Error Handling:** ImageKit now tries its best to show you if there was an error in the thumbnail creating process. The widget is now able to display errors and if thumbnail creation failed, an error image is returned instead of nothing.
- **Discovery Feature:** The widget now scans your whole site for thumbnails, so you dont have to open every page manually.
- **Widget Code:** The widget logic has been improved on both the server and the client side for better extensibility.
- **Widget UI:** Added text underneath the progress bar to give the user a better understanding of what the widget is currently doing. Added animation while the progress bar is visible. If an operation is cancelled, Widget UI is now blocked until another operation can be started.
- **Permissions:** The widget now shows an error message when the user has been logged out. The widget is now accessible for all logged-in panel users by default.
- **Refactoring:** The whole plugin has been refactored here and there …
- `1.0.0-beta1` (2016/06/04)
- First public release

View file

@ -0,0 +1,43 @@
/*jshint expr:true, -W083, esversion: 6, unused: false */
const gulp = require("gulp");
const rename = require("gulp-rename");
const csso = require("gulp-csso");
const uglify = require("gulp-uglify");
const sass = require("gulp-sass");
const toc = require("gulp-doctoc");
const postcss = require("gulp-postcss");
const assets = require("postcss-assets");
gulp.task("styles", function () {
return gulp.src([ "widgets/imagekit/assets/scss/*.scss" ])
.pipe(sass())
.pipe(postcss([
assets({ loadPaths: ['widgets/imagekit/assets/images/'] }),
]))
.pipe(csso())
.pipe(rename({ suffix: ".min" }))
.pipe(gulp.dest("widgets/imagekit/assets/css"));
});
gulp.task("scripts", function () {
return gulp.src([ "widgets/imagekit/assets/js/src/*.js" ])
.pipe(uglify())
.pipe(rename({ suffix: ".min" }))
.pipe(gulp.dest("widgets/imagekit/assets/js/dist"));
});
gulp.task( 'readme', function() {
return gulp.src(['readme.md'])
.pipe(toc({
mode: "github.com",
title: "**Table of Contents**",
}))
.pipe(gulp.dest('.'));
});
gulp.task("default", ['styles', 'scripts', 'readme']);
gulp.task("watch", ['default'], () => {
gulp.watch('widgets/imagekit/assets/scss/**/*.scss', [ 'styles' ]);
gulp.watch('widgets/imagekit/assets/js/src/**/*.js', [ 'scripts' ]);
});

View file

@ -0,0 +1,5 @@
<?php
function imagekit() {
return \Kirby\Plugins\ImageKit\ImageKit::instance();
}

View file

@ -0,0 +1,28 @@
<?php
namespace Kirby\Plugins\ImageKit;
load([
'kirby\\plugins\\imagekit\\imagekit' => 'lib' . DS . 'imagekit.php',
'kirby\\plugins\\imagekit\\component\\thumb' => 'lib' . DS . 'component' . DS . 'thumb.php',
'kirby\\plugins\\imagekit\\lazythumb' => 'lib' . DS . 'lazythumb.php',
'kirby\\plugins\\imagekit\\complainingthumb' => 'lib' . DS . 'complainingthumb.php',
'kirby\\plugins\\imagekit\\proxyasset' => 'lib' . DS . 'proxyasset.php',
'kirby\\plugins\\imagekit\\optimizer' => 'lib' . DS . 'optimizer.php',
// Only the base optimizer class is autoloaded, all other
// optimizers are loaded by scanning the directory.
'kirby\\plugins\\imagekit\\optimizer\\base' => 'lib' . DS . 'optimizer' . DS . 'base.php',
], __DIR__);
require_once __DIR__ . DS . 'helpers.php';
// Initialize the plugin
$kirby = kirby();
$kirby->set('component', 'thumb', '\\Kirby\\Plugins\\ImageKit\\Component\\Thumb');
if($kirby->option('imagekit.widget')) {
require_once __DIR__ . DS . 'widgets' . DS . 'imagekit' . DS . 'bootstrap.php';
}

View file

@ -0,0 +1,225 @@
<?php
namespace Kirby\Plugins\ImageKit;
use Response;
use Thumb;
use Str;
/**
* An extended version of Kirbys thumb class which is able
* to throw an error, if thumbs are resized with the
* GD Library and PHPs memory limit is exceeded or if
* thumbnail creation failed for another reason.
*/
class ComplainingThumb extends Thumb {
private static $_errorData = [];
private static $_errorFormat = 'image';
private static $_errorListening = false;
private static $_creating = false;
private static $_errorReporting;
private static $_displayErrors;
private static $_targetDimensions;
private static $_sendError = false;
private static $_reservedMemory;
public static function setErrorFormat($format = null) {
if (!is_null($format)) static::$_errorFormat = $format;
static::$_errorFormat;
}
public static function enableSendError() {
static::$_sendError = true;
}
public function create() {
if(!static::$_sendError) {
// Dont setup a error handlers, if complaining is
// not enabled and just return the result of create().
return parent::create();
}
$this->prepareErrorHandler();
$result = parent::create();
$this->restoreErrorHandler();
if(!file_exists($this->destination->root)) {
$message = str::template('Thumbnail creation for "{file}" failed. Please ensure, that the thumbs directory is writable and your driver configuration is correct.', [
'file' => static::$_errorData['file'],
]);
static::sendErrorResponse($message);
exit;
}
return $result;
}
private function prepareErrorHandler() {
// Reserve one additional megabyte of memory to make
// catching of out-of-memory errors more reliable.
// The variables content is deleted in the shutdown
// function to have more available memory to prepare an
// error.
static::$_reservedMemory = str_repeat('#', 1024 * 1024);
$dimensions = $this->source->dimensions();
$filename = str_replace(kirby()->roots()->index() . DS, '', $this->source->root());
$asset = new ProxyAsset($this->destination->root);
$asset->options($this->options);
$asset->original($this->source);
$targetDimensions = $asset->dimensions();
static::$_errorData = [
'file' => $filename,
'width' => $dimensions->width,
'height' => $dimensions->height,
'size' => $this->source->size(),
'targetWidth' => $targetDimensions->width,
'targetHeight' => $targetDimensions->height,
];
static::$_displayErrors = ini_get('display_errors');
static::$_errorReporting = error_reporting();
error_reporting(E_ALL);
ini_set('display_errors', 0);
if (!static::$_errorListening) {
// As of PHP 7.0 there is not way of de-registering a
// shutdown callback. So we only register it once at
// the first time, this method is called.
static::$_errorListening = true;
register_shutdown_function([__CLASS__, 'shutdownCallback']);
}
static::$_creating = true;
}
private function restoreErrorHandler() {
// Delete the reserved memory, because if thumbnail
// creation succeeded, it is not needed any more.
static::$_reservedMemory = null;
ini_set('display_errors', static::$_displayErrors);
error_reporting(static::$_errorReporting);
static::$_creating = false;
}
public static function shutdownCallback() {
// Delete the reserved memory to have more memory for
// preparing an error message.
static::$_reservedMemory = null;
$error = error_get_last();
if(!$error) return;
if($error['type'] == E_ERROR || $error['type'] === E_WARNING) {
if(str::contains($error['message'], 'Allowed Memory Size')) {
$message = str::template('Thumbnail creation for "{file}" failed, because source image is probably too large ({width} × {height} pixels / {size}) To fix this issue, increase the memory limit of PHP or upload a smaller version of this image.', [
'file' => static::$_errorData['file'],
'width' => number_format(static::$_errorData['width']),
'height' => number_format(static::$_errorData['height']),
'size' => number_format(static::$_errorData['size'] / (1024 * 1024), 2) . " MB",
]);
static::sendErrorResponse($message);
} else {
static::sendErrorResponse($error['message']);
}
}
}
public static function sendErrorResponse($message = '') {
// Make sure, that the error message is non-cachable
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
if(static::$_errorFormat === 'image') {
// The returned error image is an SVG file, because it
// can be created just by joined a couple of XML tags
// together and is supported by every modern browser,
// starting with IE 9 (which is of course not
// modern any more …).
header('Content-Type: image/svg+xml');
// Although this is technically not correct, we have
// to send status code 200, otherwise the image does
// not show up in Firefox and Safari, although it
// works with code 500 in Chrome and Edge. Also tested
// in IE 10, IE 11
http_response_code(200);
$width = static::$_errorData['targetWidth'];
$height = static::$_errorData['targetHeight'];
// Return an SVG File with the thumbs dimensions
// Icon Credit: fontawesome.io
?><svg version="1.1" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="<?= $width ?>" height="<?= $height ?>"
viewBox="0 0 <?= $width ?> <?= $height ?>"
preserveAspectRatio="none">
<symbol id="icon">
<svg viewBox="0 0 48 44.52" width="48" height="44.52" preserveAspectRatio="xMinYMax meet">
<path d="M27.425,36.789V31.705a0.854,0.854,0,0,0-.254-0.629,0.823,0.823,0,0,0-.6-0.254H21.431a0.823,0.823,0,0,0-.6.254,0.854,0.854,0,0,0-.254.629v5.084a0.854,0.854,0,0,0,.254.629,0.823,0.823,0,0,0,.6.254h5.137a0.823,0.823,0,0,0,.6-0.254A0.854,0.854,0,0,0,27.425,36.789ZM27.371,26.782L27.853,14.5a0.589,0.589,0,0,0-.268-0.508,1.034,1.034,0,0,0-.642-0.294H21.057a1.034,1.034,0,0,0-.642.294,0.64,0.64,0,0,0-.268.562L20.6,26.782a0.514,0.514,0,0,0,.268.441,1.152,1.152,0,0,0,.642.174h4.95a1.088,1.088,0,0,0,.629-0.174A0.6,0.6,0,0,0,27.371,26.782ZM27,1.793L47.545,39.464a3.192,3.192,0,0,1-.054,3.371,3.422,3.422,0,0,1-2.943,1.686H3.452A3.422,3.422,0,0,1,.509,42.835a3.192,3.192,0,0,1-.054-3.371L21,1.793A3.417,3.417,0,0,1,22.261.482a3.381,3.381,0,0,1,3.478,0A3.417,3.417,0,0,1,27,1.793Z" fill="#ffffff"/>
</svg>
</symbol>
<rect width="100%" height="100%" fill="#F4999D" />
<switch>
<foreignObject width="100%" height="100%" requiredExtensions="http://www.w3.org/1999/xhtml">
<body xmlns="http://www.w3.org/1999/xhtml" style="background:#F4999D;text-align:center;color:#fff;box-sizing:border-box;margin:0;padding:0;font-size:12px;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif">
<p style="margin:0;word-wrap:break-word;position:absolute;top:50%;transform:translateY(-50%);width: 100%;box-sizing:border-box;padding: 0 1em;">
<svg viewBox="0 0 48 44.52" width="24" height="22.125" xmlns="http://www.w3.org/2000/svg" style="display: block; margin: 0 auto 12px;"><path d="M27.425,36.789V31.705a0.854,0.854,0,0,0-.254-0.629,0.823,0.823,0,0,0-.6-0.254H21.431a0.823,0.823,0,0,0-.6.254,0.854,0.854,0,0,0-.254.629v5.084a0.854,0.854,0,0,0,.254.629,0.823,0.823,0,0,0,.6.254h5.137a0.823,0.823,0,0,0,.6-0.254A0.854,0.854,0,0,0,27.425,36.789ZM27.371,26.782L27.853,14.5a0.589,0.589,0,0,0-.268-0.508,1.034,1.034,0,0,0-.642-0.294H21.057a1.034,1.034,0,0,0-.642.294,0.64,0.64,0,0,0-.268.562L20.6,26.782a0.514,0.514,0,0,0,.268.441,1.152,1.152,0,0,0,.642.174h4.95a1.088,1.088,0,0,0,.629-0.174A0.6,0.6,0,0,0,27.371,26.782ZM27,1.793L47.545,39.464a3.192,3.192,0,0,1-.054,3.371,3.422,3.422,0,0,1-2.943,1.686H3.452A3.422,3.422,0,0,1,.509,42.835a3.192,3.192,0,0,1-.054-3.371L21,1.793A3.417,3.417,0,0,1,22.261.482a3.381,3.381,0,0,1,3.478,0A3.417,3.417,0,0,1,27,1.793Z" fill="#ffffff"/></svg>
<?= $message ?>
</p>
</body>
</foreignObject>
<!-- Fallback -->
<use xlink:href="#icon" transform="translate(<?= number_format($width / 2 - 48 / 2,1,'.','') ?>, <?= number_format($height / 2 - 44.52 / 2,1,'.','') ?>)" />
</switch>
</svg><?php
} else {
// If error format has been set to JSON, return JSON ;-)
header('Content-Type: application/json; charset=utf-8');
http_response_code(500);
echo json_encode([
'status' => '',
'code' => 500,
'message' => $message,
'data' => [
'file' => static::$_errorData,
],
]);
}
exit;
}
}

View file

@ -0,0 +1,128 @@
<?php
namespace Kirby\Plugins\ImageKit\Component;
use Asset;
use F;
use Header;
use Kirby\Component\Thumb as ThumbComponent;
use Kirby\Plugins\ImageKit\LazyThumb;
use Kirby\Plugins\ImageKit\ComplainingThumb;
use Kirby\Plugins\ImageKit\ProxyAsset;
use Kirby\Plugins\ImageKit\Optimizer;
/**
* Replacement for Kirbys built-in `thumb` component with
* asynchronous thumb creation and image optimization
* capabilities.
*/
class Thumb extends ThumbComponent {
public function defaults() {
return array_merge(parent::defaults(), [
'imagekit.lazy' => true,
'imagekit.complain' => true,
'imagekit.widget' => true,
'imagekit.widget.step' => 5,
'imagekit.widget.discover' => true,
'imagekit.optimize' => false,
'imagekit.engine' => null,
'imagekit.license' => '',
// Used for the development of the plugin, currently
// not officialy documented.
'imagekit.debug' => false,
]);
}
public function configure() {
parent::configure();
// Register route to catch non-existing files within the
// thumbs directory.
$base = ltrim(substr($this->kirby->roots->thumbs(), strlen($this->kirby->roots->index())), DS);
// Setup optimizer if enabled.
if($this->kirby->option('imagekit.optimize')) {
optimizer::register();
}
$this->kirby->set('route', [
'pattern' => "{$base}/(:all)", // $base = 'thumbs' by default
'action' => function ($path) {
if($this->kirby->option('imagekit.complain')) {
complainingthumb::enableSendError();
complainingthumb::setErrorFormat('image');
}
// Try to load a jobfile for given thumb url and
// execute if exists
$thumb = lazythumb::process($path);
if($thumb) {
// Serve the image, if everything went fine :-D
$root = $thumb->result->root();
// Make sure, were sending a 200 status, telling
// the browser that everythings okay.
header::status(200);
// Dont tell anyone that this image was just
// created by PHP ;-)
header_remove('X-Powered-By');
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', f::modified($root)) . ' GMT');
header('Content-Type: ' . f::mime($root));
header('Content-Length: ' . f::size($root));
// Send file and stop script execution
readfile($root);
exit;
} else {
// Show a 404 error, if the job could not be
// found or executed
return site()->errorPage();
}
},
]);
}
public function create($file, $params) {
if (!$this->kirby->option('imagekit.lazy') || (isset($params['imagekit.lazy']) && !$params['imagekit.lazy'])) {
return parent::create($file, $params);
}
if(!$file->isWebsafe()) return $file;
// Instead of a Thumb, a Job will be created for later
// execution
$thumb = new LazyThumb($file, $params);
if($thumb->result instanceof ProxyAsset) {
// If the thumb is yet to be generated, use the
// virtual asset, created by the LazyThumb class
$asset = $thumb->result;
} else {
// Otherwise, create a new asset from the returned
// media object.
$asset = new Asset($thumb->result);
}
// store a reference to the original file
$asset->original($file);
return $asset;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace Kirby\Plugins\ImageKit;
use F;
use Obj;
use Str;
/**
* Utility class for retrieving information about the plugin
* version and its license.
*/
class ImageKit {
protected $version;
protected function __construct() {
// Just declared to prevent direct instantiation of this
// class (singleton pattern).
}
public static function instance() {
static $instance;
return ($instance ?: $instance = new static());
}
public function version() {
if(is_null($this->version)) {
$package = json_decode(f::read(dirname(__DIR__) . DS . 'package.json'));
$this->version = $package->version;
}
return $this->version;
}
public function root() {
return dirname(__DIR__);
}
public function license() {
$key = kirby()->option('imagekit.license');
$type = 'trial';
/**
* Hey there,
*
* if you have digged deep into Kirbys source code,
* than youve probably stumbled across a similiar
* message, asking you to be honest when using the
* software. I ask you the same, if your intention is to
* use ImageKit. Writing this plugin took a lot of time
* and it hopefully saves you a lot of headaches. If you
* would use a cloud-provider instead of rolling your own
* thumb engine, then your would also have to pay them.
*
* Anyway, have a nice day!
*
* Fabian
*/
if (str::startsWith($key, 'IMGKT1') && str::length($key) === 39) {
$type = 'ImageKit 1';
} else {
$key = null;
}
return new Obj(array(
'key' => $key,
'type' => $type,
));
}
}

View file

@ -0,0 +1,293 @@
<?php
namespace Kirby\Plugins\ImageKit;
use Asset;
use Dir;
use Error;
use Exception;
use F;
use File;
use Media;
use Str;
use Thumb;
// Honestly, who in the PHP team is responsible for naming things?!?
use RecursiveIteratorIterator as Walker;
use RecursiveDirectoryIterator as DirWalker;
/**
* Extended version of Kirbys thumb class for
* creating “lazy” thumbnails.
*/
class LazyThumb extends Thumb {
const JOBFILE_SUFFIX = '-imagekitjob.php';
public function __construct($source, $params = []) {
$this->source = $this->result = is_a($source, 'Media') ? $source : new Media($source);
$this->options = array_merge(static::$defaults, $this->params($params));
$this->destination = $this->destination();
// don't create the thumbnail if it's not necessary
if($this->isObsolete()) return;
// don't create the thumbnail if it exists
if(!$this->isThere()) {
// try to create the thumb folder if it is not there yet
dir::make(dirname($this->destination->root));
// check for a valid image
if(!$this->source->exists() || $this->source->type() != 'image') {
throw new Error('The given image is invalid', static::ERROR_INVALID_IMAGE);
}
// check for a valid driver
if(!array_key_exists($this->options['driver'], static::$drivers)) {
throw new Error('Invalid thumbnail driver', static::ERROR_INVALID_DRIVER);
}
// create a jobfile for the thumbnail
$this->create();
// create a virtual asset, on which methods like width()
// and height() can be called on.
$this->result = new ProxyAsset(new Media($this->destination->root, $this->destination->url));
$this->result->original($this->source);
$this->result->options($this->options);
} else {
// create the result object
$this->result = new Media($this->destination->root, $this->destination->url);
}
return $this;
}
protected function create() {
$root = static::jobfile($this->destination->root);
if(f::exists($root)) return;
if(is_a($this->source, 'File')) {
// Source file belongs to a page
$pageid = $this->source->page()->id();
$dir = null;
} else {
// Source file is an outlaw, hiding somewhere else in
// the file tree
$pageid = null;
$dir = substr($this->source->root(), str::length(kirby()->roots->index));
$dir = pathinfo(ltrim($dir , DS), PATHINFO_DIRNAME);
}
$options = [
'imagekit.version' => imagekit()->version(),
'source' => [
'filename' => $this->source->filename(),
'dir' => $dir,
'page' => $pageid,
],
'options' => $this->options,
];
// Remove `destination` option before export, because
// closures cannot be exported and this option is a
// closure by default.
unset($options['options']['destination']);
$export = "<?php\nreturn " . var_export($options, true) . ';';
f::write($root, $export);
}
// ===== API Methods ========================================================
public static function process($path) {
$thumbs = kirby()->roots->thumbs();
if(!str::startsWith($path, $thumbs)) {
$path = $thumbs . DS . $path;
}
$jobfile = static::jobfile($path);
if(!$thumbinfo = @include($jobfile)) {
// Abort, if there is no matching jobfile for the
// requested thumb.
return false;
}
// This option is a closure by default, which cannot be
// restored. Currently, we can only restore it by
// overriding it the the default option of the thumb
// class. So this does not work, when a custom
// 'destination' option was set for this particular
// thumbnail or the option has been changed between
// jobfile creation and execution of this method.
$thumbinfo['options']['destination'] = thumb::$defaults['destination'];
if (!is_null($thumbinfo['source']['page'])) {
// Try to relocate the image and get its association
// with the original parent page or site
if ($thumbinfo['source']['page'] === '') {
// Image was uploaded to the "content" directory
$image = site()->image($thumbinfo['source']['filename']);
} else {
// Image belongs to a specific page
$image = page($thumbinfo['source']['page'])->image($thumbinfo['source']['filename']);
}
if(!$image) {
// If source image does not exist any more, remove
// the jobfile.
f::remove($jobfile);
return false;
}
} else {
// If the image does not belong to a specific page,
// just use an `Asset` as source.
$image = new Asset($thumbinfo['source']['dir'] . DS . $thumbinfo['source']['filename']);
if(!$image->exists()) {
f::remove($jobfile);
return false;
}
}
// override url and root of the thumbs directory to the
// current values. This prevents ImageKit from failing
// after your Kirby installation has been moved.
$thumbinfo['options']['ul'] = kirby()->urls->thumbs();
$thumbinfo['options']['root'] = kirby()->roots->thumbs();
// Finally execute job file by creating a thumb
$thumb = new ComplainingThumb($image, $thumbinfo['options']);
if(!kirby()->option('imagekit.debug') && f::exists($thumb->destination()->root)) {
// Delete job file if thumbnail has been generated
// successfully and were not in debug mode.
f::remove($jobfile);
}
return $thumb;
}
/**
* Returns the path of a thumbnails jobfile. The jobfile
* contains instructions about how to create the actual
* thumbnail.
*
* @return string A thumbnails jobfile.
*/
public static function jobfile($path) {
return !str::endsWith($path, self::JOBFILE_SUFFIX) ? $path . self::JOBFILE_SUFFIX : $path;
}
/**
* Returns all pending thumbnails, i.e. thumbnails that
* have not been created yet. This works by looking for
* jobfiles in the thumbs directory.
*
* @return array A list of all pending thumbnails
*/
public static function pending() {
$pending = [];
$iterator = new Walker(new DirWalker(kirby()->roots()->thumbs()), Walker::SELF_FIRST);
foreach($iterator as $file) {
$pathname = $file->getPathname();
if(str::endsWith($pathname, self::JOBFILE_SUFFIX)) {
$thumb = str::substr($pathname, 0, -str::length(self::JOBFILE_SUFFIX));
if(!file_exists($thumb)) {
$pending[] = $pathname;
}
}
}
return $pending;
}
/**
* Walks the thumbs directory, searches for image files
* and returns a list of those.
*
* @return array A list of all websafe image files within
* the thumbs directory.
*/
public static function created() {
$created = [];
$iterator = new Walker(new DirWalker(kirby()->roots()->thumbs()), Walker::SELF_FIRST);
foreach($iterator as $file) {
$pathname = $file->getPathname();
if(in_array(pathinfo($pathname, PATHINFO_EXTENSION), ['jpg', 'jpeg', 'png', 'gif'])) {
$created[] = $pathname;
}
}
return $created;
}
/**
* Get the actual status of generated and pending thumbs
* on your site.
*
* @return array An associative array containing stats
* about your thumbs folder.
*/
public static function status() {
return [
'pending' => sizeof(static::pending()),
'created' => sizeof(static::created()),
];
}
/**
* Clears the entire thumbs directory.
*
* @return boolen `true` if cleaning was successful,
* otherwise `false`.
*/
public static function clear() {
$root = kirby()->roots()->thumbs();
// Look for placeholder files used by many projects
// when working with git. these files are used to add
// empty directories to repositories. Files will be
// re-created after zhe cache has been flushed. Although
// these files are usually empty, its more secure to
// read their contents before deleting them, just in case …
$indexFile = $root . DS . 'index.html';
$index = f::exists($indexFile) ? f::read($indexFile) : false;
$gitkeepFile = $root . DS . '.gitkeep';
$gitkeep = f::exists($gitkeepFile) ? f::read($gitkeepFile) : false;
$result = dir::clean($root);
if($result) {
// Only re-create if thumbs dir cleanup was successful
if($index !== false) {
// Re-create index.html if it existed before
f::write($indexFile, $index);
}
if($gitkeep !== false) {
// Re-create .gitkeep file, if it existed before
f::write($gitkeepFile, $gitkeep);
}
}
return $result;
}
}

View file

@ -0,0 +1,169 @@
<?php
namespace Kirby\Plugins\ImageKit;
use A;
use Dir;
use F;
use Thumb;
class Optimizer {
protected static $kirby;
protected static $optimizers = [];
// These variables store all loaded optimizers of an
// actual instance of the Optimizer object, sorted by
// their priority.
protected $pre;
protected $post;
/**
* Creates an optimmizer for given Thumb
*
* @param Thumb $thumb
* @param array $pre Optimizers to apply prior to
* thumbnail creation.
* @param array $post Optimizers to apply after
* thumbnail creation.
*/
protected function __construct($thumb, $pre, $post) {
static::init();
$this->thumb = $thumb;
$this->pre = $pre;
$this->post = $post;
}
/**
* Creates a new instance of this class for given thumb.
*
* @param Thumb $thumb
* @return Optimizer
*/
public static function create(Thumb $thumb) {
static::init();
$pre = [];
$post = [];
// Get optimizers parameter
$optimizers = a::get($thumb->options, 'imagekit.optimize', kirby()->option('imagekit.optimize'), true);
foreach(static::$optimizers as $optimizerClass) {
if($optimizers === true || (is_array($optimizers) && in_array($optimizerClass::name(), $optimizers))) {
if($optimizer = $optimizerClass::create($thumb)) {
if($optimizer->priority('pre') !== false) {
$pre[] = $optimizer;
}
if($optimizer->priority('post') !== false) {
$post[] = $optimizer;
}
}
}
}
// Sort all applicable optimization operations.
usort($pre, function($a, $b) {
if($a === $b) return 0;
return ($a->priority('pre') < $b->priority('pre')) ? -1 : 1;
});
usort($post, function($a, $b) {
if($a === $b) return 0;
return ($a->priority('post') < $b->priority('post')) ? -1 : 1;
});
return new static($thumb, $pre, $post);
}
/**
* Runs all operations that should happen before thumbnail
* creation.
*/
public function pre() {
foreach($this->pre as $optimizer) {
$optimizer->pre();
}
}
/**
* Runs all operations that should happen after thumbnail
* creation.
*/
public function post() {
foreach($this->post as $optimizer) {
$optimizer->post();
}
}
/**
* Registers the optimizer by extending all thumbnail
* drivers in Kirbys toolkit.
*/
public static function register() {
static $registred;
if($registred) return;
foreach(thumb::$drivers as $name => $driver) {
thumb::$drivers[$name] = function($thumb) use ($driver) {
if(a::get($thumb->options, 'imagekit.optimize', kirby()->option('imagekit.optimize')) !== false) {
$optimizer = static::create($thumb);
$optimizer->pre();
$driver($thumb);
$optimizer->post();
} else {
$driver($thumb);
}
};
}
$registred = true;
}
/**
* Scans `optimizer` subdir for available optimizers and
* loads them, if theyre available.
*
* @param Kirby $kirby
*/
public static function init($kirby = null) {
static $initialized;
if ($initialized) return;
static::$kirby = $kirby ?: kirby();
$lib = imagekit()->root() . DS . 'lib' . DS . 'optimizer';
// Load and initialize all optimizers
foreach(dir::read($lib, ['base.php']) as $basename) {
require_once($lib . DS . $basename);
$optimizerClass = __NAMESPACE__ . '\\Optimizer\\' . f::name($basename);
// Setup defaults
static::$kirby->options = array_merge($optimizerClass::defaults(), static::$kirby->options);
$optimizerClass::configure(static::$kirby);
if ($optimizerClass::available()) {
static::$optimizers[] = $optimizerClass;
}
}
$initialized = true;
}
public static function available($name) {
static::init();
$name = strtolower($name);
foreach(static::$optimizers as $optimizer) {
if($optimizer::name() === $name) return true;
}
return false;
}
}

View file

@ -0,0 +1,256 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
use Exception;
use F;
use ReflectionClass;
/**
* Interface for the optimizer class. An optimizer is
* created for a specific thumbnail by calling the
* `create($thumb)` method.
*/
interface BaseInterface {
/**
* Should implement at least some basic checks to make
* sure, that the optimizer is working. This method should
* check for executables being in place,
* for PHP extensions, operating system etc.
*
* @return boolean Return `true`, if the optimizer is
* available, otherwise `false`.
*/
public static function available();
}
/**
* The base class for ImageKits optimizers.
*/
abstract class Base implements BaseInterface {
/**
* Defines the file types, this optimizer can handle.
*
* @var string|array Either string with a value of '*' or
* an array containing a list of mime types.
*/
public static $selector = '*';
/**
* Defines the priority of this optimizers operations.
* @var array An array of two elements, where each can be
* either a positive integer, zero or false.
* The first value is the priority of
* pre-operations, the second one of
* post-operations. Priority is not set static
* to make synamic changed possible.
*/
public $priority = [10, 10];
/**
* Kirbys instance.
*
* @var Kirby
*/
public static $kirby;
/**
* Thumbnail will be set by create method.
*
* @var Thumb
*/
protected $thumb;
/**
* Constructor of this optimizer
*
* @param Thumb An instance of the Thumb class.
*/
protected function __construct($thumb) {
$this->thumb = $thumb;
}
/**
* Returns an array of all available setting variables of
* this optimizer.
*
* @return array
*/
public static function defaults() {
return [];
}
/**
* Called after defaults have been added to the global
* Kirby instance. Can be used for further operations
* that need all options to be in place.
*/
public static function configure($kirby) {
static::$kirby = $kirby;
}
/**
* Use this method to create an instance of given
* optimizer.
*/
public static function create($thumb) {
if(!static::matches($thumb)) {
// Dont create optimizer for given thumb, if it
// cannot handle its file type.
return null;
} else {
return new static($thumb);
}
}
/**
* Operations to performed before thumbnail creation. This
* can be used to modify parameters on the passed $thumb
* object.
*
* @param Thumb $thumb
*/
public function pre() {
// Do some crazy stuff here in a subclass …
}
/**
* Operations to be performed after the thumbnai has been
* created by the thumbs driver.
*
* @param Thumb $thumb
*/
public function post() {
// Do some crazy stuff here in a subclass …
}
/**
* Returns the priority of this optimizer.
*
* @param string $which Must be either 'pre' or 'post'.
* @return int|boolean The priority of either 'pre' or
* 'post' operations or false, if this
* optimizer does not have a pre/post
* operation defined.
*/
public function priority($which) {
switch($which) {
case 'pre':
return $this->priority[0];
case 'post':
return $this->priority[1];
default:
throw new Exception('`$which` parameter must have a value of either `"pre"` or `"post"`.');
}
}
/**
* Returns true, checks the extension of a thumb
* destination file against the mime types, this optimizer
* can handle. Additional checks are not performed.
*
* @param Thumb $thumb
* @return bool `true`, if Optimizer can handle given
* thumb, otherwise `false`.
*/
public static function matches($thumb) {
$mime = f::extensionToMime(f::extension($thumb->destination->root));
return in_array($mime, static::$selector);
}
/**
* Returns the name of the optimizer class in lowercase
* without namespace.
*
* @return string The optimizers class name without
* namespace.
*/
public static function name() {
return strtolower(str_replace(__NAMESPACE__ . '\\', '', get_called_class()));
}
/* ===== Utility Functions ============================================== */
/**
* Returns a temporary filename, based on the original
* filename of the thumbnail destination.
*
* @param string $extension Optionally change the
* extension of the temporary
* file by providing this
* parameter.
* @return string The full path to the
* temporary file.
*/
protected function getTemporaryFilename($extension = null) {
$parts = pathinfo($this->thumb->destination->root);
if (!$extension) {
$extension = $parts['extension'];
}
// Add a unique suffix
$suffix = '-' . uniqid();
return $parts['dirname'] . DS . $parts['filename'] . $suffix . '.' . $extension;
}
/**
* Compares the filesize of $target and $alternative and
* only keeps the smallest of both files. If $alternative
* is smaller than $target, $target will be replaced by
* $alternative.
*
* @param string $target Full path to target file.
* @param string $alternative Full path to alternative file.
*
*/
protected function keepSmallestFile($target, $alternative) {
if(f::size($alternative) <= f::size($target)) {
f::remove($target);
f::move($alternative, $target);
} else {
f::remove($alternative);
}
}
/**
* Checks the driver of a given thumb is given driver id.
*
* @param string $driver Name of the thumbnail engine,
* (i.e. 'im' or 'gd').
* @return boolean
*/
protected function isDriver($driver) {
return (
(isset($this->thumb->options['driver']) && $this->thumb->options['driver'] === $driver) ||
(static::$kirby->option('imagekit.driver') === $driver)
);
}
/**
* Tries to get the value of an option from given Thumb
* object. If not set, returns the global value of this
* option.
*
* @param Thumb $thumb An instance of the Thumb class
* from Kirbys toolkit.
* @param string $key The option key.
* @return mixed Either local or global value of
* the option.
*/
protected function option($key, $default = null) {
if(isset($this->thumb->options[$key])) {
return $this->thumb->options[$key];
} else {
return static::$kirby->option($key, $default);
}
}
}

View file

@ -0,0 +1,74 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
use F;
/**
* Lossless optimization of GIF files by using `gifsicle`.
* Supports animated GIFs.
*
* See: https://www.lcdf.org/gifsicle/
*/
class Gifsicle extends Base {
public static $selector = ['image/gif'];
public $priority = [false, 50];
protected $targetFile;
protected $tempFile;
public static function defaults() {
return [
'imagekit.gifsicle.bin' => null,
'imagekit.gifsicle.level' => 3,
'imagekit.gifsicle.colors' => false,
'imagekit.gifsicle.flags' => '',
];
}
public static function available() {
return !empty(static::$kirby->option('imagekit.gifsicle.bin'));
}
public function post() {
$tmpFile = $this->getTemporaryFilename();
$command = [];
$command[] = static::$kirby->option('imagekit.gifsicle.bin');
if($this->thumb->options['interlace']) {
$command[] = '--interlace';
}
// Set colors
$colors = $this->option('imagekit.gifsicle.colors');
if($colors !== false) {
$command[] = "--colors $colors";
}
// Set optimization level.
$command[] = '--optimize=' . $this->option('imagekit.gifsicle.level');
$flags = $this->option('imagekit.gifsicle.flags');
if(!empty($flags)) {
// Add exra flags, if defined by user.
$command[] = $flags;
}
// Set output file
$command[] = '--output "' . $tmpFile . '"';
// Set input file
$command[] = '"' . $this->thumb->destination->root . '"';
exec(implode(' ', $command));
$this->keepSmallestFile($this->thumb->destination->root, $tmpFile);
}
}

View file

@ -0,0 +1,79 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
use F;
/**
* Lossless optimization of JPEG files by using `jpegtran`.
*
* See: http://jpegclub.org/jpegtran/
* and: http://linux.die.net/man/1/jpegtran
*/
class JPEGTran extends Base {
public static $selector = ['image/jpeg'];
public $priority = [false, 50];
protected $targetFile;
protected $tempFile;
public static function defaults() {
return [
'imagekit.jpegtran.bin' => null,
'imagekit.jpegtran.optimize' => true,
'imagekit.jpegtran.copy' => 'none',
'imagekit.jpegtran.flags' => '',
];
}
public static function available() {
return !empty(static::$kirby->option('imagekit.jpegtran.bin'));
}
public function post() {
$tmpFile = $this->getTemporaryFilename();
$command = [];
$command[] = static::$kirby->option('imagekit.jpegtran.bin');
if($this->thumb->options['interlace']) {
$command[] = '-progressive';
}
if($this->thumb->options['grayscale']) {
$command[] = '-grayscale';
}
// Copy metadata (or not)?
if($copy = $this->option('imagekit.jpegtran.copy')) {
$command[] = "-copy $copy";
}
if($this->option('imagekit.jpegtran.optimize')) {
$command[] = '-optimize';
}
// Write to a temporary file, so we can compare filesizes
// after optimization and keep only the smaller file as
// jpegtran does not always create smaller files.
$command[] = '-outfile "' . $tmpFile . '"';
$flags = $this->option('imagekit.jpegtran.flags');
if(!empty($flags)) {
// Add exra flags, if defined by user.
$command[] = $flags;
}
$command[] = '"' . $this->thumb->destination->root . '"';
exec(implode(' ', $command));
$this->keepSmallestFile($this->thumb->destination->root, $tmpFile);
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
/**
* Uses `mozjpeg` for encoding JPEG files. This often creates
* much smaller JPEG files than most other encoders at a
* comparable quality.
*
* See: https://github.com/mozilla/mozjpeg
*/
class MozJPEG extends Base {
public static $selector = ['image/jpeg'];
public $priority = [100, 5];
protected $targetFile;
protected $tmpFile;
public static function defaults() {
return [
'imagekit.mozjpeg.bin' => null,
'imagekit.mozjpeg.quality' => 85,
'imagekit.mozjpeg.flags' => '',
];
}
public static function available() {
return !empty(static::$kirby->option('imagekit.mozjpeg.bin'));
}
public function pre() {
$this->targetFile = $this->thumb->destination->root;
if($this->isDriver('im')) {
// Instruct imagemagick driver to write out a temporary,
// uncrompressed TGA file, so our JPEG will not be
// compressed twice. I played around with PNM too,
// because its much faster as an intermediate format,
// but some images got corrupted by mozjpeg.
$this->tmpFile = $this->getTemporaryFilename('tga');
$this->thumb->destination->root = $this->tmpFile;
} else {
// If GD driver (or an unknown driver) is active, we
// need to encode twice, because SimpleImage can only
// save to JPEG, PNG or GIF. As saving a 24-bit
// lossless PNG as an intermediate step is too
// expensive for large images, we need to encode
// as JPEG :-(
// This also needs a temporary file, because it seems
// that mozjpeg cannot overwrite the its file.
$this->tmpFile = $this->getTemporaryFilename();
$this->thumb->destination->root = $this->tmpFile;
$this->thumb->options['quality'] = 99;
}
}
public function post() {
$command = [];
$command[] = static::$kirby->option('imagekit.mozjpeg.bin');
// Quality
$command[] = '-quality ' . $this->option('imagekit.mozjpeg.quality');
// Interlace
if($this->thumb->options['interlace']) {
$command[] = '-progressive';
}
// Grayscale
if($this->thumb->options['grayscale']) {
$command[] = '-grayscale';
}
// Set output file.
$command[] = '-outfile "' . $this->targetFile . '"';
$flags = $this->option('imagekit.mozjpeg.flags');
if(!empty($flags)) {
// Add exra flags, if defined by user.
$command[] = $flags;
}
// Use tmp file as input.
$command[] = '"' . $this->tmpFile . '"';
exec(implode(' ', $command));
// Delete temporary file and restore destination path
// on thumb object. This only needs to be done, if
// ImageMagick driver is used and the input file was
// in PNM format.
@unlink($this->tmpFile);
$this->thumb->destination->root = $this->targetFile;
}
}

View file

@ -0,0 +1,64 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
/**
* Lossless optimization of PNG images using `optipng`.
*
* See: http://optipng.sourceforge.net/
* and: http://optipng.sourceforge.net/pngtech/optipng.html
*/
class OptiPNG extends Base {
public static $selector = ['image/png'];
public $priority = [false, 50];
protected $targetFile;
protected $tmpFile;
public static function defaults() {
return [
'imagekit.optipng.bin' => null,
'imagekit.optipng.level' => 2,
'imagekit.optipng.strip' => 'all',
'imagekit.optipng.flags' => '',
];
}
public static function available() {
return !empty(static::$kirby->option('imagekit.optipng.bin'));
}
public function post() {
$command = [];
$command[] = static::$kirby->option('imagekit.optipng.bin');
// Optimization Level
$level = $this->option('imagekit.optipng.level');
if($level !== false) { // Level can be 0, so strict comparison is neccessary
$command[] = "-o$level";
}
// Should we strip Metadata?
if($strip = $this->option('imagekit.optipng.strip')) {
$command[] = "-strip $strip";
}
$flags = $this->option('imagekit.optipng.flags');
if(!empty($flags)) {
// Add exra flags, if defined by user.
$command[] = $flags;
}
// Set file to optimize
$command[] = '"' . $this->thumb->destination->root . '"';
exec(implode(' ', $command));
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace Kirby\Plugins\ImageKit\Optimizer;
/**
* Lossy optimization by using `pngquant` for converting
* 24-bit PNGs to an 8-bit palette while preserving the
* alpha-channel.
*
* See: https://pngquant.org/
*/
class PNGQuant extends Base {
public static $selector = ['image/png'];
public $priority = [100, 5];
protected $targetFile;
protected $tmpFile;
public static function defaults() {
return [
'imagekit.pngquant.bin' => null,
'imagekit.pngquant.quality' => null,
'imagekit.pngquant.speed' => 3,
'imagekit.pngquant.posterize' => false,
'imagekit.pngquant.colors' => false,
'imagekit.pngquant.flags' => '',
];
}
public static function available() {
return !empty(static::$kirby->option('imagekit.pngquant.bin'));
}
public function pre() {
$this->targetFile = $this->thumb->destination->root;
if($this->isDriver('im')) {
// Instruct imagemagick driver to write out a
// temporary, uncrompressed PNM file, so our PNG will
// not need to be encoded twice.
$this->tmpFile = $this->getTemporaryFilename('pnm');
$this->thumb->destination->root = $this->tmpFile;
} else {
// If driver is anything else but IM, we do not create
// a temporary file.
$this->tmpFile = null;
}
}
public function post() {
$command = [];
$command[] = static::$kirby->option('imagekit.pngquant.bin');
// Quality
$quality = $this->option('imagekit.pngquant.quality');
if($quality !== null) {
$command[] = "--quality $quality";
}
// Speed
if($speed = $this->option('imagekit.pngquant.speed')) {
$command[] = "--speed $speed";
}
// Posterize
$posterize = $this->option('imagekit.pngquant.posterize');
if($posterize !== false) { // need verbose check,
// because posterize can have
// value of 0
$command[] = "--posterize $posterize";
}
$copy = $this->option('imagekit.pngquant.copy');
if($copy !== null) {
$command[] = "--copy $copy";
}
if(is_null($this->tmpFile)) {
// Only save optimized file, if it is smaller than the
// original. This only makes sense, if input file a
// PNG image. If input is PNM, the result should
// always be saved.
$command[] = '--skip-if-larger';
}
$flags = $this->option('imagekit.pngquant.flags');
if(!empty($flags)) {
$command[] = $flags;
}
// Force pngquant to override original file, if it was used
// as input (i.e. when no temporary file was created).
$command[] = '--force --output "' . $this->targetFile . '"';
// Colors
$colors = $this->option('imagekit.pngquant.colors');
if($colors != false) {
$command[] = $colors;
}
// Separator between options and input file as
// recommended according by the pngquant docs.
$command[] = '--';
if(!is_null($this->tmpFile)) {
// Use tmp file as input.
$command[] = '"' . $this->tmpFile . '"';
} else {
// Use target file as input
$command[] = '"' . $this->targetFile . '"';
}
exec(implode(' ', $command));
if(!is_null($this->tmpFile)) {
// Delete temporary file and restore destination path on thumb object.
@unlink($this->tmpFile);
$this->thumb->destination->root = $this->targetFile;
}
}
}

View file

@ -0,0 +1,268 @@
<?php
namespace Kirby\Plugins\ImageKit;
use A;
use Asset;
use Dimensions;
use Exception;
use F;
use Kirby;
use Media;
use Str;
use Thumb;
use Url;
/**
* An extended version of Kirbys Asset class, which also
* works with files that do not exist yet.
*/
class ProxyAsset extends Asset {
// Thumb parameters like you would path to the `Thumb`
// class constructor
protected $options = [];
// Will be changed to true if thumbnail has been generated
protected $generated = false;
/**
* Constructor
*
* @param Media|string $path
*/
public function __construct($path) {
// This constructor function mimics the behavior of both
// Assets and Medias constructors. Because `realpath()`
// (used in Medias constructor) does not work with
// non-existing files, the complete constructors of both
// classes are redeclared here, using a different function
// for resolving paths of
// non-existing files. This constructor should always try
// to match the behavior of Kirbys original Asset class.
// Assets constructor
$this->kirby = kirby::instance();
if($path instanceof Media) {
// Because “root” of non-existing files does not work,
// when theyre initialized as Media objects, well
// reconstruct the files path from its URL.
$root = $this->kirby->roots()->index() . str_replace(kirby()->urls->index, '', $path->url());
$url = $path->url();
} else {
$root = url::isAbsolute($path) ? null : $this->kirby->roots()->index() . DS . ltrim($path, DS);
$url = url::makeAbsolute($path);
}
// Medias constructor
$this->url = $url;
$this->root = $root === null ? $root : static::normalizePath($root);
$this->filename = basename($root);
$this->name = pathinfo($root, PATHINFO_FILENAME);
$this->extension = strtolower(pathinfo($root, PATHINFO_EXTENSION));
}
/**
* Tries to normalize a given path by resolving `..`
* and `.`. Tries to mimick the behavoir of `realpath()`
* which does not work on non-existing files.
* Source: http://php.net/manual/de/function.realpath.php#84012
*
* @param string $path
* @return string
*/
protected static function normalizePath($path) {
$path = str_replace(['/', '\\'], DS, $path);
$parts = explode(DS, $path);
$absolutes = [];
foreach($parts as $part) {
if('.' === $part) continue;
if('..' === $part)
array_pop($absolutes);
else
$absolutes[] = $part;
}
return implode(DS, $absolutes);
}
/**
* Setter and getter for transformation parameters
*
* @param array $options
* @return string
*/
public function options(array $options = null) {
if($options === null) {
return $this->options;
} else {
$this->options = $options;
return $this;
}
}
/**
* Returns the dimensions of the file if possible. If the
* proxy asset has not been generated yet, dimensions are
* read from source file and then calculated according to
* given thumb options.
*
* @return Dimensions
*/
public function dimensions() {
if($this->generated) {
// If the thumbnail has been generated, get dimensions
// from thumb file.
return parent::dimensions();
}
if(isset($this->cache['dimensions'])) {
return $this->cache['dimensions'];
}
if(!$this->original) {
throw new Exception('Cannot calculate dimensions of ProxyAsset without a valid original.');
}
if(!is_array($this->options)) {
throw new Exception('You have to set transformation options on ProxyAsset before calling `dimensions()`');
}
if(in_array($this->original->mime(), ['image/jpeg', 'image/png', 'image/gif'])) {
$size = (array)getimagesize($this->original->root());
$width = a::get($size, 0, 0);
$height = a::get($size, 1, 0);
} else {
$width = 0;
$height = 0;
}
// Create dimensions object and resize according to thumb options.
$dimensions = new Dimensions($width, $height);
if($this->options['crop']) {
$dimensions->crop($this->options['width'], $this->options['height']);
} else {
$dimensions->fitWidthAndHeight($this->options['width'], $this->options['height'], $this->options['upscale']);
}
return $this->cache['dimensions'] = $dimensions;
}
/**
* Generate the actual thumbnail. This function is triggered
* by certain methods, which need the final thumbnail to be
* there for returning reasonable results.
*/
public function generate() {
if($this->generated) return;
$thumb = new Thumb($this->original, $this->options);
// Just to be sure to have all corrent data of the
// resulting object in place, we override this objects
// properties with those of thumbs result.
$this->reset();
foreach(['url', 'root', 'filename', 'name', 'extension', 'content'] as $prop) {
$this->$prop = $thumb->result->$prop;
}
$this->generated = true;
}
public function mime() {
return f::mime($this->generated ? $this->root : $this->original->root());
}
public function type() {
return f::type($this->generated ? $this->root : $this->original->root());
}
public function is($value) {
return f::is($this->generated ? $this->root : $this->original->root(), $value);
}
public function read($format = null) {
$this->generate();
return parent::read($format);
}
public function content($content = null, $format = null) {
if(is_null($content)) $this->generate();
return parent::content($content, $format);
}
public function move($to) {
$this->generate();
return parent::move($to);
}
public function copy($to) {
$this->generate();
return parent::copy($to);
}
public function size() {
$this->generate();
return parent::size();
}
public function modified($format = null, $handler = 'date') {
$this->generate();
return parent::modified($format, $handler);
}
public function base64() {
$this->generate();
return parent::base64();
}
public function exists() {
$this->generate();
return parent::exists();
}
public function isWritable() {
$this->generate();
return parent::isWritable();
}
public function isReadable() {
$this->generate();
return parent::isReadable();
}
public function load($data = array()) {
$this->generate();
return parent::load($data);
}
public function show() {
$this->generate();
return parent::show();
}
public function download($filename = null) {
$this->generate();
return parent::download($filename);
}
public function exif() {
$this->generate();
return parent::exif();
}
public function imagesize() {
$this->generate();
return parent::imagesize();
}
// Methods that dont need to be overloaded:
// thumb, resize, crop, width, height, ratio, scale, bw, blur
// => thumb() fails automatically, if called on a (Proxy)Asset with original set.
// isThumb, isWebsafe
// => Dont need the result file to be in place and work without any further help.
}

View file

@ -0,0 +1,73 @@
# ImageKit End User License Agreement
This End User License Agreement (the "Agreement") is a binding legal agreement between you and the Fabian Michael (the "Author"). By installing or using the ImageKit Plugin (the "Software"), you agree to be bound by the terms of this Agreement. If you do not agree to the Agreement, do not download, install, or use the Software. Installation or use of the Software signifies that you have read, understood, and agreed to be bound by the Agreement.
Revised on: 20 October, 2016
## Usage
This Agreement grants a non-exclusive, non-transferable license to install and use a single instance of the Software on a specific Website. Additional Software licenses must be purchased in order to install and use the Software on additional Websites. The Author reserves the right to determine whether use of the Software qualifies under this Agreement. The Author owns all rights, title and interest to the Software (including all intellectual property rights) and reserves all rights to the Software that are not expressly granted in this Agreement.
## Backups
You may make copies of the Software in any machine readable form solely for back-up purposes, provided that you reproduce the Software in its original form and with all proprietary notices on the back-up copy. All rights to the Software not expressly granted herein are reserved by the Author.
## Technical Support
Technical support is provided as described on <https://github.com/fabianmichael/kirby-imagekit>. No representations or guarantees are made regarding the response time in which support questions are answered.
## Refund Policy
We offer a 14-day, money back refund policy.
## Restrictions
You understand and agree that you shall only use the Software in a manner that complies with any and all applicable laws in the jurisdictions in which you use the Software. Your use shall be in accordance with applicable restrictions concerning privacy and intellectual property rights.
### You may not:
Distribute derivative works based on the Software;
Reproduce the Software except as described in this Agreement;
Sell, assign, license, disclose, distribute, or otherwise transfer or make available the Software or its Source Code, in whole or in part, in any form to any third parties;
Use the Software to provide services to others;
Remove or alter any proprietary notices on the Software.
## No Warranty
THE SOFTWARE IS OFFERED ON AN "AS-IS" BASIS AND NO WARRANTY, EITHER EXPRESSED OR IMPLIED, IS GIVEN. THE AUTHOR EXPRESSLY DISCLAIMS ALL WARRANTIES OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. YOU ASSUME ALL RISK ASSOCIATED WITH THE QUALITY, PERFORMANCE, INSTALLATION AND USE OF THE SOFTWARE INCLUDING, BUT NOT LIMITED TO, THE RISKS OF PROGRAM ERRORS, DAMAGE TO EQUIPMENT, LOSS OF DATA OR SOFTWARE PROGRAMS, OR UNAVAILABILITY OR INTERRUPTION OF OPERATIONS. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USE THE SOFTWARE AND ASSUME ALL RISKS ASSOCIATED WITH ITS USE.
## Term, Termination, and Modification.
You may use the Software under this Agreement until either party terminates this Agreement as set forth in this paragraph. Either party may terminate the Agreement at any time, upon written notice to the other party. Upon termination, all licenses granted to you will terminate, and you will immediately uninstall and cease all use of the Software. The Sections entitled "No Warranty," "Indemnification," and "Limitation of Liability" will survive any termination of this Agreement.
The Author may modify the Software and this Agreement with notice to you either in email or by publishing content on the Software website, including but not limited to changing the functionality or appearance of the Software, and such modification will become binding on you unless you terminate this Agreement.
## Indemnification.
By accepting the Agreement, you agree to indemnify and otherwise hold harmless the Author, its officers, employers, agents, subsidiaries, affiliates and other partners from any direct, indirect, incidental, special, consequential or exemplary damages arising out of, relating to, or resulting from your use of the Software or any other matter relating to the Software.
## Limitation of Liability.
YOU EXPRESSLY UNDERSTAND AND AGREE THAT THE AUTHOR SHALL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER INTANGIBLE LOSSES (EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES. ACCORDINGLY, SOME OF THE ABOVE LIMITATIONS MAY NOT APPLY TO YOU. IN NO EVENT WILL THE AUTHOR'S TOTAL CUMULATIVE DAMAGES EXCEED THE FEES YOU PAID TO THE AUTHOR UNDER THIS AGREEMENT IN THE MOST RECENT TWELVE-MONTH PERIOD.
# Definitions
## Definition of Website
A "Website" is defined as a single domain including sub-domains that operate as a single entity. What constitutes a single entity shall be at the sole discretion of the Author.
## Definition of Source Code
The "Source Code" is defined as the contents of all HTML, CSS, JavaScript, and PHP files provided with the Software and includes all related image files and database schemas.
## Definition of an Update
An "Update" of the Software is defined as that which adds minor functionality enhancements or any bug fix to the current version. This class of release is identified by the change of the revision to the right of the decimal point, i.e. X.1 to X.2
The assignment to the category of Update or Upgrade shall be at the sole discretion of the Author.
## Definition of an Upgrade
An "Upgrade" is a major release of the Software and is defined as that which incorporates major new features or enhancement that increase the core functionality of the software. This class of release is identified by the change of the revision to the left of the decimal point, i.e. 4.X to 5.X
The assignment to the category of Update or Upgrade shall be at the sole discretion of the Author.

View file

@ -0,0 +1,29 @@
{
"name": "imagekit",
"description": "Asynchronous thumbs API and image optimization for Kirby CMS.",
"author": "Fabian Michael <hallo@fabianmichael.de>",
"license": "SEE LICENSE IN license.md",
"version": "1.1.3",
"type": "kirby-plugin",
"repository": {
"type": "git",
"url": "https://github.com/fabianmichael/kirby-imagekit"
},
"bugs": {
"url": "https://github.com/fabianmichael/kirby-imagekit/issues"
},
"devDependencies": {
"gulp": "^3.9.1",
"gulp-csso": "^2.0.0",
"gulp-doctoc": "^0.1.4",
"gulp-postcss": "^6.1.1",
"gulp-rename": "^1.2.2",
"gulp-sass": "^2.3.2",
"gulp-uglify": "^1.5.4",
"postcss-assets": "^4.1.0"
}
}

View file

@ -0,0 +1,282 @@
# ImageKit
![GitHub release](https://img.shields.io/github/release/fabianmichael/kirby-imagekit.svg?maxAge=1800) ![License](https://img.shields.io/badge/license-commercial-green.svg) ![Kirby Version](https://img.shields.io/badge/Kirby-2.3%2B-red.svg)
ImageKit provides an asynchronous thumbnail API and advanced image optimization for [Kirby CMS](http://getkirby.com).
**NOTE:** This is not be a free plugin. In order to use it on a production server, you need to buy a license. For details on ImageKits license model, scroll down to the [License](#8-license) section of this document.
<img src="https://shared.fabianmichael.de/imagekit-widget-v2.gif" alt="ImageKits Dashboard Widget" width="460" height="231" />
***
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
**Table of Contents**
- [1 Key Features](#1-key-features)
- [2 Download and Installation](#2-download-and-installation)
- [2.1 Requirements](#21-requirements)
- [2.2 Kirby CLI](#22-kirby-cli)
- [2.3 Git Submodule](#23-git-submodule)
- [2.4 Copy and Paste](#24-copy-and-paste)
- [3 Usage](#3-usage)
- [4 How it works](#4-how-it-works)
- [4.1 Discovery mode](#41-discovery-mode)
- [5 Basic Configuration](#5-basic-configuration)
- [6 Image Optimization](#6-image-optimization)
- [6.1 Setup](#61-setup)
- [6.2 Overriding Global Settings](#62-overriding-global-settings)
- [6.3 Available Optimizers](#63-available-optimizers)
- [6.3.1 mozjpeg](#631-mozjpeg)
- [6.3.2 jpegtran](#632-jpegtran)
- [6.3.3 pngquant](#633-pngquant)
- [6.3.4 optipng](#634-optipng)
- [6.3.5 gifsicle](#635-gifsicle)
- [7 Troubleshooting](#7-troubleshooting)
- [8 License](#8-license)
- [9 Technical Support](#9-technical-support)
- [10 Credits](#10-credits)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
***
## 1 Key Features
- **Image-resizing on demand:** Kirbys built-in thumbnail engine resizes images on-the-fly while executing the code in your template files. On image-heavy pages, the first page-load can take very long or even exceed the maximum execution time of PHP. ImageKit resizes images only on-demand as soon as they are requested by the client.
- **Optimization:** ImageKit can utilize several command-line utilities to apply superior compression to your images. Both lossless and lossy optimizers are available. Your images look as shiny as before, but your pages will load much faster.*
- **Security:** A lot of thumbnail libraries for PHP still offer the generation of resized images through URL parameters (e.g. `thumbnail.php?file=ambrosia.jpg&width=500`), which is a potential vector for DoS attacks. ImageKit only generates the thumbnails whose are defined in your page templates.
- **Widget:** Pre-Generate your thumbnails right from the panel with a single click.
- **Discovery Feature:** The widget scans you whole site for new thumbnails, before creating them.
- **Error-Handling:** ImageKit lets you know, when errors occur during thumbnail creation *(experimental)*.
- **Self-Hosted:** Unlike many other image-resizing-services, ImageKit just sits in Kirbys plugin directory, so you have everything under control without depending on external providers. No monthly fees. No visitor data is exposed to external companies. tl;dr: No bullshit!
*) To use optimization features, your need to have the corresponding command-line utilities installed on your server or have sufficient permissions to install them. The effectiveness of compression also depends heavily on your source images.
***
## 2 Download and Installation
### 2.1 Requirements
- PHP 5.4.0+ (With *libxml* if youre using discovery mode. This extension is usually installed on most hosting providers.)
- Kirby 2.3.0+
- GD Library for PHP or ImageMagick command-line tools to resize images.
- Tested on Apache 2 with mod_rewrite (but it should also work with other servers like nginx)
- Permission to install and execute several command-line utilities on your server, if your want to use the optimization feature.
### 2.2 Kirby CLI
If youre using the [Kirby CLI](https://github.com/getkirby/cli), you need to `cd` to the root directory of your Kirby installation and run the following command:
```
kirby plugin:install fabianmichael/kirby-imagekit
```
This will download and copy *ImageKit* into `site/plugins/imagekit`.
### 2.3 Git Submodule
To install this plugin as a git submodule, execute the following command from the root of your kirby project:
```
$ git submodule add https://github.com/fabianmichael/kirby-imagekit.git site/plugins/imagekit
```
### 2.4 Copy and Paste
1. [Download](https://github.com/fabianmichael/kirby-imagekit/archive/master.zip) the contents of this repository as ZIP-file.
2. Rename the extracted folder to `imagekit` and copy it into the `site/plugins/` directory in your Kirby project.
## 3 Usage
Just use it like the built-in thumbnail API of Kirby. You can learn more about Kirbys image processing capabilities in the [Kirby Docs](https://getkirby.com/docs/templates/thumbnails).
Due to the fact that thumbs created by ImageKit remain *virtual* until the the actual thumb file has been requested by a visitor of your website, some API methods will trigger instant creation of a thumbnail. You should avoid to call methods like `size()`, `base64()` or `modified()` on your thumb, whenever possible, because they only work after the actual thumbnail has been created. However, you can safely use methods like `width()`, `height()` and `ratio()` because dimensions are calculated prior to thumbnail creation.
If you dont want to let the first visitors of your site need to wait for images to appear, all thumbnails on your site can be generated from ImageKits dashboard widget in advance.
## 4 How it works
Rather than doing the expensive task of image conversion on page load (default behavior of Kirbys built-in thumbs API), thumbnails are stored as a »job« instead as the API is called by your template code. So they will only be generated, when a particular image size is requested by the browser. ImageKit also comes with a widget, so you can trigger creation of all thumbnails right from the panel.
### 4.1 Discovery mode
If the `imagekit.widget.discover` *(automatic indexing)* option is active, the widget will not only scan your thumbs folder for pending thumbnails, but will also make a HTTP request to every single page of your Kirby installation to execute every pages template code once. This feature also works with pagination and/or prev- and next links. Just make sure, that the pagination links have `rel` attributes of either `'next'` or `'prev'`. This way, ImageKit can even scan through paginated pages.
```
<link href="<?= $pagination->prevPageURL() ?>" rel="prev">
<link href="<?= $pagination->nextPageURL() ?>" rel="next">
<a href="<?= $pagination->prevPageURL() ?>" rel="prev">Previous page</a>
<a href="<?= $pagination->nextPageURL() ?>" rel="next">Next page</a>
```
This currently works by using PHPs DOM interface (`DOMDocument`), so if your HTML contains a lot of errors, this might fail. If you are experiencing any trouble with this feature, please report a bug so I can make it work with your project.
## 5 Basic Configuration
| Option | Default value | Description |
|:--------------------|:--------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `imagekit.license` | `''` | Enter your license code here, once your site goes live.<br>*See the [License](#8-license) section of this document for more information.* |
| `imagekit.lazy` | `true` | Set to `false` to temporary disable asynchronous thumbnail generation. This will restore the default behavior of Kirby. |
| `imagekit.complain` | `true` | If enabled, ImageKit will try to return a placeholder showing an error symbol whenever thumbnail creation fails. If you dont like this behavior, you can turn this feature off and ImageKit will fail silently.
| `imagekit.widget` | `true` | Enables the dashboard widget. |
| `imagekit.widget.step` | `5` | Sets how many pending thumbnails will be generated by the widget in one step. If thumbnail generation exceeds the max execution time on your server, you should set this to a lower value. If your server is blazingly fast, you can safely increase the value. |
| `imagekit.widget.discover` | `true` | If enabled, the widget scans your whole site before creating thumbnails. If this feature is not compatible with your setup, disable it. It can also take very long on large site, every single page has to be rendered in order to get all pending thumbnails. In order to do this, the plugin will flush your site cache before running. |
## 6 Image Optimization
### 6.1 Setup
As of *version 1.1*, ImageKit is also capable of optimizing thumbnails. There are different optimizers, providing both lossless and lossy optimization. It works similar to third-party services (e.g. [TinyPNG](https://tinypng.com/), [Kraken.io](https://kraken.io/)), but on your own server. If you want to use this feature, you have to install the corresponding command-line utilities first—if theyre not already installed on your server.
If your hosting provider doesnt let you compile software on your webspace (most likely for shared hosting), you can get binaries for most operation systems from a WordPress plugin called [EWWW-Image-Optimizer](https://wordpress.org/plugins/ewww-image-optimizer/). Just download the plugin and upload its the pre-compiled utilities to your server (look into the folder `binaries` within the ZIP archive). There are many other places, where you can get pre-compiled versions of the image optimation tools, but please be careful and do not download any of these tools from some strange russian server. The only tool I couldnt find as a pre-compiled binary for Linux / OS X hosts is `mozjpeg`.
| Option | Default value | Description |
|:--------------------|:--------------|:------------|
| `imagekit.optimize` | `false` | Set to `true` to enable optimization. In addition, you have to configure at least one of the optimizers listed below. |
| `imagekit.driver` | `null` | This option only needs to be set, if your Kirby installation uses a custom ImageMagick driver, that has a different name than `'im'`. If this is the case, set this value to `'im'` to tell ImageKit, that youre using ImageMagick for thumbnail processing, as there is no other way to detect this reliably. In most cases, you can safely ignore this setting.<br>*This settings does not change the thumbs driver to ImageMagick! If you want to use ImageMagick as your image processing backend, please refer to the corresponding pages in the Kirby documentation or have a look into the troubleshooting section of this document.* |
By default, all optimizers will be loaded and ImageKit checks if you have set the path to a valid binary (e.g. `imagekit.mozjpeg.bin`). If the binary is set and executable, ImageKit will then activate the optimizer automatically for supported image types. All optimizers come with a sane default configuration, but you can tweak them according to your needs.
### 6.2 Overriding Global Settings
If you need different optimization configuration settings for different images, you can override any of the settings (except for the path to an optimizerss binary) you defined in `config.php` by passing them to the `thumb()` method. The parameter `imagekit.optimize` can also take an array of optimizers. Note, that optimizers will only become active, if the input image is in a format supported by them (e.g. If you provide a JPEG to the example below, `pngquant` will be skipped, because it can only handle PNG images.)
```php
$page->image()->thumb([
'width' => 600,
'imagekit.optimize' => ['mozjpeg', 'pngquant'],
'imagekit.mozjpeg.quality' => 60,
]);
```
Overriding global settings might become useful, if you want to apply lossless optimization for some images and lossy optimization for others. A typical use-case would be a photo gallery with lots of small preview images on an index page, where you want to squeeze the last byte out of your thumbnails using `mozjpeg`. For the enlarged view of a photo, image quality might be more important than filesize, so you might prefer `jpegtran` over `mozjpeg` for lossless optimization.
### 6.3 Available Optimizers
#### 6.3.1 mozjpeg
Mozjpeg is an improved JPEG encoder that produces much smaller images at a similar perceived quality as those created by GD Library, ImageMagick, or Photoshop. I really recommend to try out this optimizer, because it can significantly reduce the size of your thumbnails.
[<img src="https://img.shields.io/badge/%E2%80%BA-Download%20mozjpeg-lightgrey.svg" alt="Download mozjpeg">](https://github.com/mozilla/mozjpeg)
| Option | Default value | Possible Values | Description |
|:--------------------|:--------------|:------------|:------------|
| `imagekit.mozjpeg.bin` | `null` | — | Enter the path to mozjpegs encoder executable (`cjpeg`) to activate this optimizer.<br>*(tested with mozjpeg 3.1)* |
| `imagekit.mozjpeg.quality` | `85` | `0-100` | Sets the quality level of the generated image. Choose from a scale between 0-100, where 100 is the highest quality level. The scale is not identical to that of other JPEG encoders, so you should try different settings and compare the results if you want to get the optimal results for your project.
| `imagekit.mozjpeg.flags` | `''` | — | Use this parameter to pass additional options to the optimizer. Have a look at mozjpegs documentation for available flags. |
I recommend that you dont upscale images that have been compressed by `mozjpeg`, bacause it will add a lot of artifacts to thumbnails. Those are mostly invisible when the image is viewed at full size or downscaled. But they can give your images an unpleasant look, if theyre upscaled.
#### 6.3.2 jpegtran
Jpegtran applies lossless compression to your thumbnails by optimizing the JPEG data and stripping out metadata like EXIF. If you use mozjpeg, there is no reason to also use jpegtran, as my tests did not show any benefit in thumbnail size, when both are used together.
[<img src="https://img.shields.io/badge/%E2%80%BA-Download%20jpegtran-lightgrey.svg" alt="Download jpegtran">](http://jpegclub.org/jpegtran/)
| Option | Default value | Possible Values | Description |
|:--------------------|:--------------|:------------|:------------|
| `imagekit.jpegtran.bin` | `null` | — | Enter the path to the optipng executable to activate this optimizer.<br>*(tested with jpegtran 0.7.6)* |
| `imagekit.jpegtran.optimize` | `true` | `true`, `false` | Enables lossless optimization of image data. |
| `imagekit.jpegtran.copy` | `'none'` | `'all'`, `'comments'`, `'none'` | Sets which metadata should be copied from source file. |
| `imagekit.jpegtran.flags` | `''` | — | Use this parameter to pass additional options to the optimizer. Have a look at jpegtrans documentation for available flags. |
#### 6.3.3 pngquant
Pngquant performs lossy optimization on PNG images by converting 24-bit images to indexed color (8-bit), while alpha-transparency is kept. The files can be displayed in all modern browsers and this kind of lossy optimization works great for most non-photographic images and screenshots. You may notice some color shifts on photographic images with a lot of different colors (you usually should not use PNG for displaying photos on the web anyway …).
[<img src="https://img.shields.io/badge/%E2%80%BA-Download%20pngquant-lightgrey.svg" alt="Download pngquant">](https://pngquant.org/)
| Option | Default value | Possible Values | Description |
|:--------------------|:--------------|:------------|:------------|
| `imagekit.pngquant.bin` | `null` | — | Enter the path to the `pngquant` executable to activate this optimizer.<br>*(tested with optipng 2.7.2)* |
| `imagekit.pngquant.quality` | `null` | `null`, `'min-max'` (e.g. `'0-100'`) | Sets minimum and maximum quality of the resulting image. Has to be a string. |
| `imagekit.pngquant.speed` | `3` | `1` = slow,<br>`3` = default,<br>`11` = fast & rough |Slower speed means a better quality, but optimization takes longer *(for large images from a few megapixels and above, were talking about tens of seconds or even minutes, when using a speed setting of `1`)*. |
| `imagekit.pngquant.posterize` | `false` | `false`,<br>`0-4` | Output lower-precision color if set to further reduce filesize. |
| `imagekit.pngquant.colors` | `false` | `false`, `2`-`256`| Sets the number of colors for optimized images. Less colors mean smaller images, but also reduction of quality. |
| `imagekit.pngquant.flags` | `''` | — | Use this parameter to pass additional options to the optimizer. Have a look at pngquants documentation for available flags. |
#### 6.3.4 optipng
Optipng performs lossless optimizations on PNG images by stripping meta data and optimizing the PNG data itself.
[<img src="https://img.shields.io/badge/%E2%80%BA-Download%20optipng-lightgrey.svg" alt="Download optipng">](http://optipng.sourceforge.net/)
| Option | Default value | Possible Values | Description |
|:--------------------|:--------------|:------------|:------------|
| `imagekit.optipng.bin` | `null` | — | Enter the path to the `optipng` executable to activate this optimizer.<br>*(tested with optipng 0.7.6)* |
| `imagekit.optipng.level` | `2` | `0`-`7` | Sets the optimization level. Note, that a high optimization level can make processing of large image files very slow, while having only little impact on filesize. |
| `imagekit.optipng.strip` | `'all'` | `'all'`, `false` | Strips all metadata from the PNG file. |
| `imagekit.optipng.flags` | `''` | — | Use this parameter to pass additional options to the optimizer. Have a look at optipngs documentation for available flags. |
#### 6.3.5 gifsicle
Gifsicle optimizes the data of GIF images. Especially for animations, using this optimizer can lead to a great improvement in file size, but can also take very long for large animations. Static GIF images will also benefit from using Gifsicle.
[<img src="https://img.shields.io/badge/%E2%80%BA-Download%20gifsicle-lightgrey.svg" alt="Download gifsicle">](https://www.lcdf.org/gifsicle/)
| Option | Default value | Possible Values | Description |
|:--------------------|:--------------|:------------|:------------|
| `imagekit.gifsicle.bin` | `null` | — | Enter the path to the `optipng` executable to activate this optimizer.<br>*(tested with optipng 1.88)* |
| `imagekit.gifsicle.level` | `3` | `false`, `1`-`3` | Sets the level of optimization, where `3` is the highest possible value. |
| `imagekit.gifsicle.colors` | `false` | `false`, `2`-`256` | Sets the amount of colors in the resulting thumbnail. By default, color palettes are not reduced. |
| `imagekit.gifsicle.flags` | `''` | — | Use this parameter to pass additional options to the optimizer. Have a look at gifsicles documentation for available flags. |
## 7 Troubleshooting
<details>
<summary><strong>How can I activate ImageMagick?</strong></summary>
As ImageKit acts as a proxy for Kirbys built-in thumbnail engine, you have to activate it on your `config.php` file, just as you would do without ImageKit like below:
```
c::set('thumbs.driver','gd');
c::set('thumbs.bin', '/usr/local/bin/convert');
```
&rarr; Kirby documentation for [`thumbs.driver`](https://getkirby.com/docs/cheatsheet/options/thumbs-driver) and [`thumbs.bin`](https://getkirby.com/docs/cheatsheet/options/thumbs-bin)
Please note, that Kirby uses the command-line version of ImageMagick, rather than its PHP extension. In order to use ImageMagick as your processing backend, the ImageMagick executable (`convert`) has to be installed on your server.*
</details>
<details>
<summary><strong>Thumbnail creation always fails …</strong></summary>
This may happen because of several reasons. First, make sure that your thumbs folder is writable for Kirby. If youre using the GD Library driver, make sure that PHPs memory limit is set to a high-enough value. Increasing the memory limit allows GD to process larger source files. Or if you favor ImageMagick (I do), make sure that the path to the `convert` executable is correctly configured.
</details>
<details>
<summary><strong>The Discovery Feature does not work with my site:</strong></summary>
Discovery works by creating a sitemap of your entire site and then sends an HTTP request to every of those URLs to trigger rendering of every single page. When doing so, ImageKit sees everything from a logged-in users perspective. It tries its best to find pagination on pages, but it cannot create thumbnails whose are for example only available on a search results page, where entries are only displayed when a certain keyword was entered into a form. Also make sure, that your Servers PHP installation comes with `libxml`, which is used by PHPs DOM interface.
</details>
<details>
<summary><strong>Can I also optimize the images in my content folder?</strong></summary>
This is currently not possible, because it would need a whole UI for the admin panel and would also be very risky to apply some bulk processing on your source images without knowing the actual results of optimization. If you need optimized images in your content folder, I really recommend that you use tools like <a href="https://imageoptim.com/mac">ImageOptim</a> and <a href="https://pngmini.com/">ImageAlpha</a> to optimize your images prior to uploading them. This saves space on your server and also speeds up your backups.
</details>
<details>
<summary><strong>404 Errors with nginx</strong></summary>
ImageKit may have problems with certain nginx configurations, resulting 404 errors, when a thumbnail is requested for the first time. See <a href="https://github.com/fabianmichael/kirby-imagekit/issues/9">this issue</a> to learn, how you have to configure nginx to solve this issue.
</details>
## 8 License
ImageKit can be evaluated as long as you want on how many private servers you want. To deploy ImageKit on any public server, you need to buy a license. See `license.md` for terms and conditions.
*The plugin is also available as a bundle with [ImageSet](https://github.com/fabianmichael/kirby-imageset), a plugin for bringing responsive images with superpowers to you Kirby-driven site.*
→ [Buy ImageKit](http://sites.fastspring.com/fabianmichael/product/imagekit)
→ [Buy the ImageKit + ImageSet Bundle](http://sites.fastspring.com/fabianmichael/product/imgbundle1)
However, even with a valid license code, it is discouraged to use it in any project, that promotes racism, sexism, homophobia, animal abuse or any other form of hate-speech.
## 9 Technical Support
Technical support is provided via Email and on GitHub. If youre facing any problems with running or setting up ImageKit, please send your request to [support@fabianmichael.de](mailto:support@fabianmichael.de) or [create a new issue](https://github.com/fabianmichael/kirby-imagekit/issues/new) in this GitHub repository. No representations or guarantees are made regarding the response time in which support questions are answered.
## 10 Credits
ImageKit is developed and maintained by [Fabian Michael](https://fabianmichael.de), a graphic designer & web developer from Germany.

View file

@ -0,0 +1 @@
.imagekit-stats{width:100%}.imagekit-stats th{font-weight:400;text-align:left;white-space:nowrap;overflow:hidden}.imagekit-stats th::after{content:"";display:inline-block;width:100%;height:1px;background:currentColor;position:relative;bottom:.25em;margin-left:.35em;opacity:.2}.imagekit-stats td{padding-left:.35em;width:25%}.imagekit-action--disabled{opacity:.5;color:inherit!important;cursor:default!important}.imagekit-modal{background:rgba(255,255,255,.9);position:absolute;top:0;left:0;width:100%;height:100%;display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center;box-sizing:border-box;padding:1em 4em}.imagekit-modal p{margin-bottom:1em}.imagekit-modal p:last-child{margin-bottom:0}.imagekit-modal-buttons{padding-top:1em}.imagekit-error-icon{display:inline-block;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADgAAABACAQAAADl/nw5AAAAuElEQVR4Ae3St7nCUBQE4dMM7bxqpA5wFdCQ+iCjDfzc5PDtc3BNsDuZ3C9MxOWzbUhdUA80+KltScH1QYPLmxUwv4DB/uBEMy2iDeVtqDpoUEJHmihoSYWIDHYAZwLSoIgZ7AiWghQ0PmhQ9SUKyqLBduCefgueyOA44IF2JMHUigpQMtgPJPknCqJXiAUZ7AcqOAOlct5gPzBE8oTB4UD6M1Ay2AHMi1/2y9UHDZKAdL+/1GAl8Aq7a0QNCPzaUwAAAABJRU5ErkJggg==) 0 0/28px 32px no-repeat;width:28px;height:32px;margin-bottom:.75em}@keyframes imagekit-progress-indeterminate{0%{background-position:0 0}to{background-position:-40px 0}}.imagekit-progress{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:12px;background:#efefef;border:2px solid #000;animation:1s imagekit-progress-indeterminate linear infinite;background-size:40px 40px}.imagekit-progress::-webkit-progress-value{background:0 0}.imagekit-progress::-moz-progress-bar{background:0 0}.imagekit-progress[value]::-moz-progress-bar{background:#000}.imagekit-progress[value]::-webkit-progress-value{background:#000}.imagekit-progress:not([value]){background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQAQAAAACmas8AAAAAiUlEQVR4Ae3MN2ECABQAUdLHSIgUpIE0pCCBjd7+Ow1suemmt9B9WqxnV3Yzu7Tb2T+7m/21h9kfe5n9sLfZN3sHt8E22AbbYBtsg22wDW6DbbANtsE22AbbYBvcBttgG2yDbbANtsE2uA22wTbYBttgG2yD7cDDBeOCccG4YFwwLhgHtv/wS+EH/ASOgIJwaM8AAAAASUVORK5CYII=);opacity:.5}.imagekit-progress[value]{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAMAAAC5zwKfAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAZQTFRF////5eXlCYLQawAAAJZJREFUeNrs1rENwEAMw0By/6UzwtsAixTWANeKOBzjnXjiiSeeeOKJJ5544okn/kw0FjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWMRYxFjEWXw+xFp8nthXfP7sUBymwEye1shJHQbURZ823EIdZOhen5TwWx3E/FT8BBgBWkgyBWPq8ewAAAABJRU5ErkJggg==)}.imagekit-progress.is-hidden,.imagekit-progress.is-hidden+.imagekit-progress-text{visibility:hidden}.imagekit-progress.is-disabled{animation-play-state:paused}.imagekit-progress-text{font-size:14px;font-style:italic}

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,543 @@
(function(window, document, $) {
/* ===== Utility Functions ================================================ */
function i18n(key) {
var str = window.ImageKitSettings.translations[key];
if(str !== undefined) {
return str;
} else {
return '[' + key + ']';
}
}
function arrayUnique(array) {
var a = array.concat();
for(var i=0; i<a.length; ++i) {
for(var j=i+1; j<a.length; ++j) {
if(a[i] === a[j])
a.splice(j--, 1);
}
}
return a;
}
/* ===== ImageKit API ===================================================== */
function ImageKitAPI(options) {
var self = this,
settings = $.extend({
error: function(response) {
console.error(response.message);
},
}, options),
_running = false,
_cancelled = false;
var ACTION_CREATE = "create",
ACTION_CLEAR = "clear",
ACTION_INDEX = "index";
function api(command, callback, data, method) {
method = method || 'GET';
data = data || {};
callback = callback || function(){};
var url = settings.api + command;
return $.ajax({
url : url,
dataType : 'json',
method : method,
data : data,
success : function(response) {
callback(response);
},
error : function(xhr, status, error) {
settings.error(xhr.responseJSON);
}
});
}
function start(action) {
_running = action;
}
function stop() {
_running = false;
}
function running(running) {
if(running === undefined) {
return _running;
} else {
return (_running === running);
}
}
function status(callback) {
return api("status", callback);
}
function clear(callback) {
reset();
start(ACTION_CLEAR);
return api(ACTION_CLEAR, function(response) {
stop(ACTION_CLEAR);
callback(response.data);
});
}
function create(step, complete) {
reset();
start(ACTION_CREATE);
step = step || function(){};
complete = complete || function(){};
function doCreate() {
api(ACTION_CREATE, function (response) {
if (response.data.pending > 0 && !cancelled()) {
step(response.data);
doCreate();
} else {
stop(ACTION_CREATE);
complete(response.data);
}
});
}
doCreate();
}
function index(step, complete, error) {
reset();
start(ACTION_INDEX);
step = step || function(){};
complete = complete || function(){};
error = error || function(){};
api(ACTION_INDEX, function (response) {
var i = 0;
var pageUrls = response.data;
var triggerPageLoad = function() {
$.ajax({
url: pageUrls[i],
headers: {
"X-ImageKit-Indexing": 1,
},
success: function(response) {
if (response.data.links.length > 0) {
pageUrls = arrayUnique(pageUrls.concat(response.data.links));
}
if (++i >= pageUrls.length || cancelled()) {
stop(ACTION_INDEX);
complete({ total: pageUrls.length, status: response.data.status });
} else {
step({ current: i, total: pageUrls.length, url: pageUrls[i], status: response.data.status });
triggerPageLoad();
}
},
error: function(response) {
error(response.responseJSON);
}
});
};
triggerPageLoad();
});
}
function cancel() {
_cancelled = true;
}
function cancelled() {
return _cancelled;
}
function reset() {
_running = false;
_cancelled = false;
}
return {
status : status,
clear : clear,
create : create,
index : index,
cancel : cancel,
cancelled : cancelled,
running : running,
reset : reset,
};
}
/* ===== Progress Bar ===================================================== */
function ProgressBar() {
var progressElm = document.querySelector(".js-imagekit-progress"),
progressTextElm = document.querySelector(".js-imagekit-progress-text"),
_visible = false,
self = this,
_public = {};
function toggle(show) {
if (show === _visible) return self;
progressElm.classList[show ? "remove" : "add"]("is-hidden");
_visible = show;
return _public;
}
function disable() {
progressElm.classList.add("is-disabled");
return _public;
}
function enable() {
progressElm.classList.remove("is-disabled");
return _public;
}
function show() {
return toggle(true);
}
function hide() {
return toggle(false);
}
function value(value) {
if (value !== null && value !== false) {
progressElm.setAttribute("value", value);
} else {
progressElm.removeAttribute("value");
}
return _public;
}
function text(msg) {
if (msg) {
progressTextElm.innerHTML = msg;
return _public;
} else {
return progressTextElm.innerHTML;
}
}
_public = {
show : show,
hide : hide,
value : value,
text : text,
enable : enable,
disable : disable,
};
return _public;
}
/* ===== Actions ========================================================== */
function Actions() {
var _public = {},
actions = {
clear: {
element: $('[href="#imagekit-action-clear"]'),
icon: $('[href="#imagekit-action-clear"] i'),
},
create: {
element: $('[href="#imagekit-action-create"]'),
icon: $('[href="#imagekit-action-create"] i'),
}
};
function disable(action) {
if(action) {
actions[action].element.addClass("imagekit-action--disabled");
} else {
$.each(actions, function() { this.element.addClass("imagekit-action--disabled"); });
}
return _public;
}
function enable(action) {
if(action) {
actions[action].element.removeClass("imagekit-action--disabled");
} else {
$.each(actions, function() { this.element.removeClass("imagekit-action--disabled"); });
}
return _public;
}
function icon(action, oldClass, newClass) {
var elm = actions[action].icon;
elm.removeClass(oldClass);
elm.addClass(newClass);
return _public;
}
function register(action, callback) {
actions[action].element.click(function(e) {
e.preventDefault();
callback();
});
return _public;
}
_public = {
disable : disable,
enable : enable,
icon : icon,
register : register,
};
return _public;
}
/* ===== ImageKit Widget ================================================== */
function Widget(options) {
var settings = options,
api = new ImageKitAPI($.extend(ImageKitSettings, {
error: function(response) {
error(response.message);
}
})),
actions = new Actions(),
infoElm = document.querySelector(".js-imagekit-info"),
createdElm = document.querySelector(".js-imagekit-created"),
pendingElm = document.querySelector(".js-imagekit-pending"),
progress = new ProgressBar();
/* ----- Internal Interface Methods --------------------------------------- */
function updateStatus(status) {
createdElm.innerHTML = status.created;
pendingElm.innerHTML = status.pending;
}
function error(message, onClose) {
onClose = onClose || false;
actions.disable();
progress.disable();
var $overlay = $("<div/>").addClass("imagekit-modal");
$overlay.append('<i class="imagekit-error-icon">'); // fa fa-exclamation-triangle fa-2x
$overlay.append($('<p/>').html(message));
$("#imagekit-widget").append($overlay);
if(onClose) {
$overlay.append($('<a href="#" class="btn btn-rounded">OK</a>').click(function() {
$overlay.remove();
api.reset();
progress.hide();
actions.enable().icon("create", "fa-stop-circle-o", "fa-play-circle-o");
status();
onClose();
}));
}
}
function confirm(message, onClose) {
var $overlay = $("<div/>").addClass("imagekit-modal"),
esc,
close;
$overlay.append($('<p/>').html(message));
$("#imagekit-widget").append($overlay);
esc = function(e) {
if("key" in e ? (e.key == "Escape" || e.key == "Esc") : (e.keyCode == 27)) {
close(false);
} else if ("key" in e ? e.key == "Enter" : e.key == 13) {
close(true);
}
};
close = function(result) {
$overlay.remove();
onClose(result);
document.removeEventListener("keydown", esc);
};
var $buttons = $('<p class="imagekit-modal-buttons"/>');
$buttons.append($('<a href="#" class="btn btn-rounded">' + i18n('cancel') + '</a>').click(function() { close(false); } ));
$buttons.append("&nbsp;&nbsp;&nbsp;");
$buttons.append($('<a href="#" class="btn btn-rounded">' + i18n('ok') + '</a>').click(function() { close(true); } ));
$overlay.append($buttons);
document.addEventListener("keydown", esc);
}
/* ----- Widget Actions --------------------------------------------------- */
function status() {
if(api.running()) return;
api.status(function(result) {
updateStatus(result.data);
});
}
function clear() {
if(api.running()) return;
confirm(i18n('imagekit.widget.clear.confirm'), function(confirmed) {
if(confirmed) {
actions.disable();
progress
.value(false)
.text(i18n("imagekit.widget.progress.clearing"))
.show();
api.clear(function(status) {
progress.hide();
actions.enable();
updateStatus(status);
});
}
});
// if(window.confirm(i18n('imagekit.widget.clear.confirm'))) {
// actions.disable();
// progress
// .value(false)
// .text(i18n("imagekit.widget.progress.clearing"))
// .show();
// api.clear(function(status) {
// progress.hide();
// actions.enable();
// updateStatus(status);
// });
// }
}
function index(callback) {
callback = callback || function(){};
progress.text(i18n("imagekit.widget.progress.scanning"));
api.index(function (result) {
// step
progress
.value(result.current / result.total)
.text(i18n("imagekit.widget.progress.scanning") + " " + result.current + "/" + result.total);
updateStatus(result.status);
}, function (result) {
// complete
progress
.value(1)
.text(i18n("imagekit.widget.progress.scanned"));
updateStatus(result.status);
callback();
}, function (result) {
// error
error(result.message, function() {
});
});
}
function create(callback) {
callback = callback || function(){};
progress
.value(false)
.text(i18n('imagekit.widget.progress.creating'));
api.create(function (result) {
// step
var total = result.pending + result.created;
progress.value(result.created / total);
updateStatus(result);
}, function (result) {
// complete
progress.value(1);
updateStatus(result);
callback();
});
}
function run() {
actions
.disable("clear")
.icon("create", "fa-play-circle-o", "fa-stop-circle-o");
progress
.value(false)
.show();
function complete() {
progress.hide();
actions
.enable()
.icon("create", "fa-stop-circle-o", "fa-play-circle-o");
}
if (ImageKitSettings.discover) {
index(function() {
if(!api.cancelled()) {
create(complete);
} else {
complete();
}
});
} else {
create(complete);
}
}
function stop() {
if (api.running("index") || api.running("create")) {
actions.disable();
progress
.value(false)
.text(i18n('imagekit.widget.progress.cancelling'));
api.cancel();
return;
}
}
actions.register("clear", clear);
actions.register("create", function() {
if (!api.running()) {
run();
} else {
stop();
}
});
return {
status : status,
};
}
$(function() {
var widget = new Widget();
widget.status();
});
})(window, document, jQuery);

View file

@ -0,0 +1,131 @@
.imagekit-stats {
width: 100%;
th {
font-weight: normal;
text-align: left;
white-space: nowrap;
overflow: hidden;
&::after {
content: "";
display: inline-block;
width: 100%;
height: 1px;
background: currentColor;
position: relative;
bottom: .25em;
margin-left: .35em;
opacity: .2;
}
}
td {
padding-left: .35em;
width: 25%;
}
}
.imagekit-action--disabled {
opacity: .5;
color: inherit !important;
cursor: default !important;
}
.imagekit-modal {
background: rgba(#fff, .9);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
box-sizing: border-box;
padding: 1em 4em;
p {
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
}
.imagekit-modal-buttons {
padding-top: 1em;
}
.imagekit-error-icon {
display: inline-block;
background: inline('../images/icon-error@2x.png') 0 0 / 28px 32px no-repeat;
width: 28px;
height: 32px;
margin-bottom: .75em;
}
@keyframes imagekit-progress-indeterminate {
0% { background-position: 0 0; }
100% { background-position: -40px 0; }
}
.imagekit-progress {
/* Reset the default appearance */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
height: 12px;
background: #efefef;
border: 2px solid #000;
animation: 1s imagekit-progress-indeterminate linear infinite;
background-size: 40px 40px;
&::-webkit-progress-value {
background: transparent;
}
&::-moz-progress-bar {
background: transparent;
}
&[value]::-moz-progress-bar {
background: #000;
}
&[value]::-webkit-progress-value {
background: #000;
}
&:not([value]) {
background-image: inline('../images/progress-indeterminate@2x.png');
opacity: .5;
}
&[value] {
background-image: inline('../images/progress-running@2x.png')
}
&.is-hidden {
visibility: hidden;
}
&.is-disabled {
animation-play-state: paused;
}
}
.imagekit-progress-text {
font-size: 14px;
font-style: italic;
.imagekit-progress.is-hidden + & {
visibility: hidden;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
load([
'kirby\\plugins\\imagekit\\widget\\widget' => 'lib' . DS . 'widget.php',
'kirby\\plugins\\imagekit\\widget\\translations' => 'lib' . DS . 'translations.php',
'kirby\\plugins\\imagekit\\widget\\api' => 'lib' . DS . 'api.php',
'kirby\\plugins\\imagekit\\widget\\apicrawlerresponse' => 'lib' . DS . 'apicrawlerresponse.php',
], __DIR__);
// Initialize Widget and API
Widget::instance();
API::instance();

View file

@ -0,0 +1,48 @@
<style><?= f::read(__DIR__ . '/assets/css/widget.min.css') ?></style>
<div class="dashboard-box">
<div class="js-imagekit-info / text">
<table class="imagekit-stats">
<tr>
<th><?= $translations->get('imagekit.widget.status.pending') ?></th>
<td class="js-imagekit-pending"></td>
</tr>
<tr>
<th><?= $translations->get('imagekit.widget.status.created') ?></th>
<td class="js-imagekit-created"></td>
</tr>
</table>
</div>
</div>
<progress class="imagekit-progress is-hidden / js-imagekit-progress"></progress>
<p class="marginalia imagekit-progress-text / js-imagekit-progress-text"></span>
</p>
<?php if (imagekit()->license()->type === 'trial'): ?>
<p class="debug-warning marginalia" style="position: relative; padding-left: 30px; font-size: 14px; padding-top: 12px;">
<span class="fa fa-exclamation-triangle" style="position: absolute; top: 15px; left: 5px; font-size: 14px;"></span>
<?php printf($translations->get('imagekit.widget.license.trial'), 'http://sites.fastspring.com/fabianmichael/product/imagekit') ?>
</p>
<?php endif ?>
<script>
<?php
echo 'window.ImageKitSettings = ' . json_encode([
'api' => kirby()->urls()->index() . '/plugins/imagekit/widget/api/',
'translations' => array_merge(
$translations->get(), [
'cancel' => i18n('cancel'),
'ok' => i18n('ok'),
]),
'discover' => kirby()->option('imagekit.widget.discover'),
]) . ';';
if(kirby()->option('imagekit.debug')) {
echo f::read(__DIR__ . '/assets/js/src/widget.js');
} else {
echo f::read(__DIR__ . '/assets/js/dist/widget.min.js');
}
?>
</script>

View file

@ -0,0 +1,32 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
use Tpl;
$translations = Translations::load();
return [
'title' => [
'text' => $translations->get('imagekit.widget.title'),
],
'options' => [
[
'text' => $translations->get('imagekit.widget.action.clear'),
'icon' => 'trash-o',
'link' => '#imagekit-action-clear',
],
[
'text' => $translations->get('imagekit.widget.action.create'),
'icon' => 'play-circle-o',
'link' => '#imagekit-action-create',
],
],
'html' => function() use ($translations) {
return tpl::load(__DIR__ . DS . 'imagekit.html.php', compact('translations'));
}
];

View file

@ -0,0 +1,122 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
use Response;
use Exception;
use Kirby\Plugins\ImageKit\LazyThumb;
use Kirby\Plugins\ImageKit\ComplainingThumb;
use Whoops\Handler\Handler;
use Whoops\Handler\CallbackHandler;
class API {
public $kirby;
public static function instance() {
static $instance;
return $instance ?: $instance = new static();
}
protected function __construct() {
$self = $this;
$this->kirby = kirby();
$this->kirby->set('route', [
'pattern' => 'plugins/imagekit/widget/api/(:any)',
'action' => function($action) use ($self) {
if($error = $this->authorize()) {
return $error;
}
if(method_exists($self, $action)) {
return $this->$action();
} else {
throw new Exception('Invalid plugin action. The action "' . html($action) . '" is not defined.');
}
},
]);
if(isset($_SERVER['HTTP_X_IMAGEKIT_INDEXING'])) {
// Handle indexing request (discovery feature).
$this->handleCrawlerRequest();
}
}
protected function authorize() {
$user = kirby()->site()->user();
if (!$user || !$user->hasPanelAccess()) {
throw new Exception('Only logged-in users can use the ImageKit widget. Please reload this page to go to the login form.');
}
}
protected function handleCrawlerRequest() {
if($error = $this->authorize()) {
return $error;
}
if($this->kirby->option('representations.accept')) {
throw new Exception('ImageKits discover mode does currently not work, when the <code>representations.accept</code> setting is turned on. Please disable either this setting or disable <code>imagekit.widget.discover</code>.');
}
kirby()->set('component', 'response', '\\kirby\\plugins\\imagekit\\widget\\apicrawlerresponse');
}
public function status() {
return Response::success(true, lazythumb::status());
}
public function clear() {
return Response::success(lazythumb::clear(), lazythumb::status());
}
public function create() {
$pending = lazythumb::pending();
$step = kirby()->option('imagekit.widget.step');
// Always complain when trying to create thumbs from the widget
complainingthumb::enableSendError();
complainingthumb::setErrorFormat('json');
for($i = 0; $i < sizeof($pending) && $i < $step; $i++) {
lazythumb::process($pending[$i]);
}
return Response::success(true, lazythumb::status());
}
public function index() {
$index = [];
$this->kirby->cache()->flush();
$site = site();
$isMultilang = $site->multilang() && $site->languages()->count() > 1;
if($isMultilang) {
$languageCodes = [];
foreach($site->languages() as $language) {
$languageCodes[] = $language->code();
}
}
foreach($site->index() as $page) {
if($isMultilang) {
foreach($languageCodes as $code) {
$index[] = $page->url($code);
}
} else {
$index[] = $page->url();
}
}
return Response::success(true, $index);
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
use Exception;
use DOMDocument;
use Kirby;
use Response;
use Kirby\Plugins\ImageKit\LazyThumb;
use Url;
use V;
class APICrawlerResponse extends \Kirby\Component\Response {
public function __construct(Kirby $kirby) {
parent::__construct($kirby);
// Register listeners for redirects
header_register_callback([$this,'detectRedirectRequest']);
register_shutdown_function([$this,'detectRedirectRequest']);
}
public function detectRedirectRequest() {
// Redirects should be ignored by the widget, so
// override a redirect and just return a valid json
// response.
$redirect = in_array(http_response_code(), [301, 302, 303, 304, 307]);
$sent = headers_sent();
if($redirect && !$sent) {
header_remove('location');
echo Response::success(true, [
'links' => [],
'status' => LazyThumb::status(),
]);
exit;
}
}
public function make($response) {
// Try to generate response by calling Kirbys native
// respionse component.
$html = parent::make($response);
if(!class_exists('\DOMDocument')) {
throw new Exception('The discovery feature of ImageKit needs PHP with the <strong>libxml</strong> extension to run.');
}
$links = [];
try {
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML($html);
libxml_clear_errors();
$elements = array_merge(
iterator_to_array($doc->getElementsByTagName('a')),
iterator_to_array($doc->getElementsByTagName('link'))
);
foreach($elements as $elm) {
$rel = $elm->getAttribute('rel');
if($rel === 'next' || $rel === 'prev') {
$href = $elm->getAttribute('href');
if(v::url($href) && url::host($href) === url::host()) {
// Only add, if href is either a URL on the same
// domain as the API call was made to, as links
// could possibly link to sth. like `#page2` or
// `javascript:;` on AJAX-powered websites.
$links[] = $href;
}
}
}
} catch(Exception $e) {
return Response::error($e->getMessage(), 500);
}
return Response::success(true, [
'links' => array_unique($links),
'status' => LazyThumb::status(),
]);
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
/**
* A very simple translations class, which can load an
* associative PHP array of language strings.
*/
class Translations {
protected static $cache;
protected $translations = [];
public function __construct($code = 'en') {
$language_directory = dirname(__DIR__) . DS . 'translations';
if ($code !== 'en') {
if (!preg_match('/^[a-z]{2}([_-][a-z0-9]{2,})?$/i', $code) ||
!file_exists($language_directory . DS . $code . '.php')) {
// Set to fallback language, if not a valid code or no translation available.
$code = 'en';
}
}
$this->translations = require($language_directory . DS . $code . '.php');
}
public function get($key = null) {
if(is_null($key)) {
return $this->translations;
} else if (isset($this->translations[$key])) {
return $this->translations[$key];
} else {
return '[missing translation: ' . $key . ']';
}
}
public static function load($code = null) {
if(is_null($code)) {
$code = panel()->translation()->code();
}
if (!isset(static::$cache[$code])) {
static::$cache[$code] = new static($code);
}
return static::$cache[$code];
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Kirby\Plugins\ImageKit\Widget;
class Widget {
public static function instance() {
static $instance;
return $instance ?: $instance = new static();
}
protected function __construct() {
$kirby = kirby();
// Register the Widget
$kirby->set('widget', 'imagekit', dirname(__DIR__));
}
}

View file

@ -0,0 +1,20 @@
<?php
return [
'imagekit.widget.title' => 'Thumbnails',
'imagekit.widget.action.create' => 'Erstellen',
'imagekit.widget.action.clear' => 'Löschen',
'imagekit.widget.clear.confirm' => 'Sollen wirklich alle Thumbnails glöscht werden? Dies löscht alle Dateien im Ordner thumbs.',
'imagekit.widget.status.pending' => 'In Warteschlange',
'imagekit.widget.status.created' => 'Erstellt',
'imagekit.widget.progress.clearing' => 'Lösche thumbnails …',
'imagekit.widget.progress.scanning' => 'Durchsuche alle Seiten …',
'imagekit.widget.progress.scanned' => 'Suche abgeschlossen',
'imagekit.widget.progress.creating' => 'Erstelle thumbnails …',
'imagekit.widget.progress.cancelling' => 'Vorgang wird abgebrochen …',
'imagekit.widget.license.trial' => 'ImageKit läuft im Testmodus. Bitte unterstützen Sie die Entwicklung des plugins und <a href="%s" target="_blank">kaufen Sie eine Lizenz</a>. Wenn Sie bereits einen Lizenzschlüssel haben, tragen Sie ihn bitte in die Datei <code title="site/config/config.php" style="border-bottom: 1px dotted; font-family: monospace;">config.php</code> ein.',
];

View file

@ -0,0 +1,20 @@
<?php
return [
'imagekit.widget.title' => 'Thumbnails',
'imagekit.widget.action.create' => 'Create',
'imagekit.widget.action.clear' => 'Clear',
'imagekit.widget.clear.confirm' => 'Do you really want to clear your thumbnails? This will delete all files in your thumbs folder.',
'imagekit.widget.status.pending' => 'Pending',
'imagekit.widget.status.created' => 'Created',
'imagekit.widget.progress.clearing' => 'Clearing thumbs folder …',
'imagekit.widget.progress.scanning' => 'Scanning pages …',
'imagekit.widget.progress.scanned' => 'Scan complete',
'imagekit.widget.progress.creating' => 'Creating thumbnails …',
'imagekit.widget.progress.cancelling' => 'Cancelling …',
'imagekit.widget.license.trial' => 'ImageKit is running in trial mode. Please support the development of this plugin and <a href="%s" target="_blank">buy a license</a>. If you already have a license key, please add it to your <code title="site/config/config.php" style="border-bottom: 1px dotted; font-family: monospace;">config.php</code> file.',
];