Skip to content

Commit 8842db0

Browse files
committed
Initial commit
0 parents  commit 8842db0

9 files changed

+722
-0
lines changed

.gitignore

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Vendor files
2+
node_modules
3+
vendor
4+
5+
# System files
6+
.DS_STORE
7+
Thumbs.db
8+
config.rb
9+
10+
# IDE
11+
/.vscode/tasks.json
12+
13+
# Pre- and post-processing
14+
.cache
15+
.parcel-cache
16+
.sass-cache
17+
*.scssc
18+
*.sassc

ImagePlaceholders.module.php

+182
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
<?php namespace ProcessWire;
2+
3+
use Daun\Placeholders\PlaceholderBlurHash;
4+
5+
// Register the private namespace used by this module
6+
wire('classLoader')->addNamespace('Daun', __DIR__ . '/lib');
7+
8+
class ImagePlaceholders extends WireData implements Module
9+
{
10+
static public function getModuleInfo()
11+
{
12+
return [
13+
'title' => 'Image Placeholders',
14+
'summary' => 'Generate low-quality image placeholders (LQIP) on upload',
15+
'href' => 'http://modules.processwire.com/modules/image-placeholders/',
16+
'version' => '0.1.0',
17+
'author' => 'daun',
18+
'singular' => true,
19+
'autoload' => true,
20+
'icon' => 'picture-o',
21+
'requires' => [
22+
'PHP>=7.0',
23+
'ProcessWire>=3.0.155',
24+
'FieldtypeImage'
25+
]
26+
];
27+
}
28+
29+
protected int $defaultLqipWidth = 20;
30+
protected array $generators = [];
31+
32+
public function init()
33+
{
34+
$this->generators = [
35+
// PlaceholderNone::class => $this->_('None'),
36+
// PlaceholderThumbHash::class => $this->_('ThumbHash'),
37+
PlaceholderBlurHash::class => $this->_('BlurHash'),
38+
// PlaceholderAverageColor::class => $this->_('Average Color'),
39+
// PlaceholderDominantColor::class => $this->_('Dominant Color'),
40+
// PlaceholderProcessWire::class => $this->_('Image variant'),
41+
// PlaceholderSVG::class => $this->_('SVG'),
42+
];
43+
44+
// On image upload, generate placeholder
45+
$this->addHookAfter('FieldtypeImage::savePageField', $this, 'handleImageFieldSave');
46+
// Add settings to image field config screen
47+
$this->addHookAfter('FieldtypeImage::getConfigInputfields', $this, 'addImageFieldSettings');
48+
// Add `Pageimage.lqip` property that returns the placeholder data uri
49+
$this->addHookProperty('Pageimage::lqip', function (HookEvent $event) {
50+
$event->return = $this->getPlaceholderDataUri($event->object);
51+
});
52+
// Add `Pageimage.lqip($width, $height)` method that returns the placeholder in a given size
53+
$this->addHookMethod('Pageimage::lqip', function (HookEvent $event) {
54+
$width = $event->arguments(0) ?: null;
55+
$height = $event->arguments(1) ?: null;
56+
$event->return = $this->getPlaceholderDataUri($event->object, $width, $height);
57+
});
58+
}
59+
60+
public function handleImageFieldSave(HookEvent $event): void
61+
{
62+
$page = $event->arguments(0);
63+
$field = $event->arguments(1);
64+
$images = $page->get($field->name);
65+
$placeholderType = $this->getPlaceholderType($field);
66+
67+
if (!$placeholderType || !$images->count() || $page->hasStatus(Page::statusDeleted)) {
68+
return;
69+
}
70+
71+
$image = $images->last(); // get the last added images (should be the last uploaded image)
72+
[, $placeholder] = $this->getPlaceholder($image, true);
73+
if (!$placeholder) {
74+
[$type, $placeholder] = $this->generatePlaceholder($image);
75+
if ($placeholder) {
76+
$this->setPlaceholder($image, [$type, $placeholder]);
77+
}
78+
}
79+
}
80+
81+
protected function getPlaceholderType(Field $field): string
82+
{
83+
return $field->generateLqip ?? '';
84+
}
85+
86+
protected function getPlaceholder(Pageimage $image, bool $checks = false): array
87+
{
88+
$type = $image->filedata("image-placeholder-type");
89+
$data = $image->filedata("image-placeholder-data");
90+
if ($checks) {
91+
$expectedType = $this->getPlaceholderType($image->field);
92+
if ($type !== $expectedType) {
93+
$data = null;
94+
}
95+
}
96+
return [$type, $data];
97+
}
98+
99+
protected function setPlaceholder(Pageimage $image, array $placeholder): void
100+
{
101+
[$type, $data] = $placeholder;
102+
$image->filedata("image-placeholder-type", $type);
103+
$image->filedata("image-placeholder-data", $data);
104+
$image->page->save($image->field->name, ["quiet" => true, "noHooks" => true]);
105+
}
106+
107+
protected function generatePlaceholder(Pageimage $image): array
108+
{
109+
$type = $this->getPlaceholderType($image->field);
110+
$handler = $this->getPlaceholderGenerator($type);
111+
$placeholder = '';
112+
113+
try {
114+
$placeholder = $handler::generatePlaceholder($image);
115+
} catch (\Throwable $e) {
116+
$this->wire()->error($e->getMessage());
117+
}
118+
119+
return [$type, $placeholder];
120+
}
121+
122+
protected function getPlaceholderDataUri(Pageimage $image, ?int $width = null, ?int $height = null): string
123+
{
124+
[$type, $placeholder] = $this->getPlaceholder($image, false);
125+
if (!$placeholder) {
126+
return '';
127+
}
128+
129+
$handler = $this->getPlaceholderGenerator($type);
130+
$width = $width ?: $this->defaultLqipWidth;
131+
$height = $height ?: $width / ($image->width / $image->height);
132+
133+
try {
134+
return $handler::generateDataURI($placeholder, $width, $height);
135+
} catch (\Throwable $e) {
136+
$this->wire()->error($e->getMessage());
137+
return '';
138+
}
139+
}
140+
141+
protected function getPlaceholderGenerator(string $type): string
142+
{
143+
foreach ($this->generators as $class => $label) {
144+
if ($class::$name === $type) {
145+
return $class;
146+
}
147+
}
148+
return PlaceholderNone::class;
149+
}
150+
151+
protected function addImageFieldSettings(HookEvent $event)
152+
{
153+
$modules = $this->wire()->modules;
154+
155+
$inputfields = $event->return;
156+
$field = $event->arguments(0);
157+
// $children = $inputfields->get('children'); // Due there is no first() in InputfieldWrapper
158+
159+
/** @var InputfieldFieldset $fs */
160+
$fs = $modules->get('InputfieldFieldset');
161+
$fs->name = '_files_fieldset_placeholders';
162+
$fs->label = $this->_('Image placeholders');
163+
$fs->icon = 'picture-o';
164+
// $inputfields->insertAfter($fs, $children->first());
165+
$inputfields->add($fs);
166+
167+
// Create field for choosing placeholder type
168+
/** @var InputfieldRadios $f */
169+
$f = $modules->get('InputfieldRadios');
170+
$f->name = 'generateLqip';
171+
$f->label = $this->_('Placeholders');
172+
$f->description = $this->_('Choose whether this field should generate low-quality image placeholders (LQIP) on upload.');
173+
$f->icon = 'toggle-on';
174+
$f->optionColumns = 1;
175+
$f->addOption('', $this->_('None'));
176+
foreach ($this->generators as $class => $label) {
177+
$f->addOption($class::$name, $label);
178+
}
179+
$f->value = $field->generateLqip;
180+
$fs->add($f);
181+
}
182+
}

LICENCE

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright 2023 Philipp Daun
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# ProcessWire Image Placeholders
2+
3+
A ProcessWire module to generate image placeholders for smoother lazyloading.
4+
5+
## Why use image placeholders?
6+
7+
Low-Quality Image Placeholders (LQIP) are used to improve the perceived performance of sites by
8+
displaying a small, low-quality version of an image while the high-quality version is being loaded.
9+
The LQIP technique is often used in combination with lazy loading.
10+
11+
## How does it work
12+
13+
This module will automatically generate an image placeholder for each image that is uploaded to
14+
fields configured to use them. In your frontend templates, you can access the image placeholder as
15+
a data URI string to display while the high-quality image is loading. See below for markup examples.
16+
17+
## Placeholder types
18+
19+
Currently, the module supports generating three types of image placeholders. The default is
20+
`ThumbHash`.
21+
22+
- [BlurHash](https://blurha.sh/): the original format developed by Wolt
23+
- [ThumbHash](https://evanw.github.io/thumbhash/): a newer format with better color rendering and alpha channel support
24+
- ProcessWire: generate a tiny variation of the image and cache it
25+
26+
## Installation
27+
28+
Install the module from the root of your ProcessWire installation.
29+
30+
```sh
31+
composer require daun/processwire-image-placeholders
32+
```
33+
34+
Open the admin panel of your site and navigate to `Modules``Site``ImagePlaceholders` to finish installation.
35+
36+
## Configuration
37+
38+
You'll need to configure your image fields to generate image placeholders.
39+
40+
`Setup``Fields``[images]``Details``Image placeholders`
41+
42+
## Usage
43+
44+
Accessing an image's `lqip` property will return a data uri string of its placeholder. Using it as
45+
a method allows setting a custom width and/or height of the placeholder.
46+
47+
```php
48+
$page->image->lqip; // data:image/png;base64,R0lGODlhEAAQAMQAA
49+
$page->image->lqip(300); // 300 x auto
50+
$page->image->lqip(300, 200); // 300 x 200px
51+
```
52+
53+
Depending on your lazyloading technique, you can either use this as image `src` or render it as a
54+
separate image.
55+
56+
```php
57+
<img src="<?= $page->image->lqip ?>" data-src="<?= $page->image->url ?>">
58+
```
59+
60+
## Support
61+
62+
Please [open an issue](https://github.com/daun/processwire-image-placeholders/issues/new) for support.
63+
64+
## License
65+
66+
[MIT](./LICENCE)

composer.json

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "daun/processwire-image-placeholders",
3+
"type": "processwire-module",
4+
"extra": {
5+
"installer-name": "ImagePlaceholders"
6+
},
7+
"description": "Generate low-quality image placeholders (LQIP) on upload.",
8+
"keywords": [ "processwire", "image", "placeholder", "lqip", "thumbhash", "blurhash", "lazyload" ],
9+
"homepage": "https://github.com/daun/processwire-image-placeholders",
10+
"license": "MIT",
11+
"authors": [
12+
{
13+
"name": "Philipp Daun",
14+
"email": "[email protected]",
15+
"homepage": "https://philippdaun.net"
16+
}
17+
],
18+
"require": {
19+
"php": ">=7.0",
20+
"composer/installers": "~1.0",
21+
"kornrunner/blurhash": "~1.2.1",
22+
"ext-curl": "*",
23+
"ext-gd": "*",
24+
"ext-exif": "*",
25+
"srwiez/thumbhash": "^1.2"
26+
},
27+
"config": {
28+
"allow-plugins": {
29+
"composer/installers": true
30+
}
31+
}
32+
}

0 commit comments

Comments
 (0)