Skip to content

Commit eb06413

Browse files
committed
Merge branch 'develop'
2 parents 4fe2e96 + af5c974 commit eb06413

File tree

8 files changed

+164
-45
lines changed

8 files changed

+164
-45
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 0.1.2
4+
5+
- Check for supported file formats
6+
- Regenerate placeholder when replacing single image
7+
38
## 0.1.1
49

510
- Allow generating missing placeholders for existing images

ImagePlaceholders.module.php

Lines changed: 48 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<?php namespace ProcessWire;
22

3+
use Daun\Image;
4+
use Daun\Placeholders\PlaceholderAverageColor;
35
use Daun\Placeholders\PlaceholderBlurHash;
46
use Daun\Placeholders\PlaceholderThumbHash;
57

@@ -33,30 +35,27 @@ static public function getModuleInfo()
3335
public function init()
3436
{
3537
$this->generators = [
36-
// PlaceholderNone::class => $this->_('None'),
3738
PlaceholderThumbHash::class => $this->_('ThumbHash'),
3839
PlaceholderBlurHash::class => $this->_('BlurHash'),
39-
// PlaceholderAverageColor::class => $this->_('Average Color'),
40+
PlaceholderAverageColor::class => $this->_('Average color'),
4041
// PlaceholderDominantColor::class => $this->_('Dominant Color'),
41-
// PlaceholderProcessWire::class => $this->_('Image variant'),
42-
// PlaceholderSVG::class => $this->_('SVG'),
4342
];
4443

45-
// On image upload, generate placeholder
46-
$this->addHookAfter('FieldtypeImage::savePageField', $this, 'handleImageUpload');
47-
4844
// Add settings to image field config screen
4945
$this->addHookAfter('FieldtypeImage::getConfigInputfields', $this, 'addImageFieldSettings');
5046

51-
// Generate palceholders for existing images on field save
47+
// Generate placeholders for existing images on field save
5248
$this->addHookAfter('FieldtypeImage::savedField', $this, 'handleImageFieldtypeSave');
5349

54-
// Add `Pageimage.lqip` property that returns the placeholder data uri
50+
// Generate placeholder on image upload
51+
$this->addHookAfter('FieldtypeImage::savePageField', $this, 'handleImageUpload');
52+
53+
// Add `$image->lqip` property that returns the placeholder data uri
5554
$this->addHookProperty('Pageimage::lqip', function (HookEvent $event) {
5655
$event->return = $this->getPlaceholderDataUri($event->object);
5756
});
5857

59-
// Add `Pageimage.lqip($width, $height)` method that returns the placeholder in a given size
58+
// Add `$image->lqip($width, $height)` method that returns the placeholder in a given size
6059
$this->addHookMethod('Pageimage::lqip', function (HookEvent $event) {
6160
$width = (int) $event->arguments(0) ?: 0;
6261
$height = (int) $event->arguments(1) ?: 0;
@@ -72,7 +71,9 @@ public function handleImageUpload(HookEvent $event): void
7271
$type = $this->getPlaceholderType($field);
7372
if ($type && $images->count() && !$page->hasStatus(Page::statusDeleted)) {
7473
$image = $images->last(); // get the last added images (should be the last uploaded image)
75-
$this->generateAndSavePlaceholder($image);
74+
if ($this->isSupportedImageFormat($image)) {
75+
$this->generateAndSavePlaceholder($image);
76+
}
7677
}
7778
}
7879

@@ -91,16 +92,18 @@ public function generateAndSavePlaceholder(Pageimage $image, bool $force = false
9192

9293
protected function getPlaceholderType(Field $field): string
9394
{
94-
return $field->generateLqip ?? '';
95+
return $field->imagePlaceholderType ?: '';
9596
}
9697

9798
protected function getPlaceholder(Pageimage $image, bool $checks = false): array
9899
{
99100
$type = $image->filedata("image-placeholder-type");
100101
$data = $image->filedata("image-placeholder-data");
101102
if ($checks) {
102-
$expectedType = $this->getPlaceholderType($image->field);
103-
if ($type !== $expectedType) {
103+
$created = $image->filedata("image-placeholder-created");
104+
$current = $created && $image->modified <= $created;
105+
$expected = $this->getPlaceholderType($image->field);
106+
if (!$current || $type !== $expected) {
104107
$data = null;
105108
}
106109
}
@@ -112,33 +115,31 @@ protected function setPlaceholder(Pageimage $image, array $placeholder): void
112115
[$type, $data] = $placeholder;
113116
$image->filedata("image-placeholder-type", $type);
114117
$image->filedata("image-placeholder-data", $data);
118+
$image->filedata("image-placeholder-created", time());
115119
$image->page->save($image->field->name, ["quiet" => true, "noHooks" => true]);
116120
}
117121

118122
protected function generatePlaceholder(Pageimage $image): array
119123
{
120-
$type = $this->getPlaceholderType($image->field);
121-
$handler = $this->getPlaceholderGenerator($type);
122-
$placeholder = '';
123-
124124
try {
125+
$type = $this->getPlaceholderType($image->field);
126+
$handler = $this->getPlaceholderGenerator($type);
125127
$placeholder = $handler::generatePlaceholder($image);
128+
return [$type, $placeholder];
126129
} catch (\Throwable $e) {
127130
if ($this->wire()->user->isSuperuser()) {
128131
$this->wire()->error("Error generating image placeholder: {$e->getMessage()}");
129132
}
130133
$this->wire()->log("Error generating image placeholder: {$e->getMessage()}: {$e->getTraceAsString()}");
131134
}
132135

133-
return [$type, $placeholder];
136+
return [null, null];
134137
}
135138

136139
protected function getPlaceholderDataUri(Pageimage $image, int $width = 0, int $height = 0): string
137140
{
138141
[$type, $placeholder] = $this->getPlaceholder($image, false);
139-
if (!$placeholder) {
140-
return '';
141-
}
142+
if (!$placeholder) return '';
142143

143144
$handler = $this->getPlaceholderGenerator($type);
144145
$width = $width ?: $this->defaultLqipWidth;
@@ -172,20 +173,20 @@ protected function createPlaceholdersForField(Field $field, bool $force = false)
172173
$this->message(sprintf($this->_('Generating missing image placeholders in field `%s`'), $field->name));
173174
}
174175

175-
$count = 0;
176176
$total = 0;
177+
$generated = 0;
177178
$pages = $this->wire()->pages->findMany("{$field->name}.count>0, check_access=0");
178179
foreach ($pages as $page) {
179180
$images = $page->getUnformatted($field->name);
180-
$total += $images->count();
181181
foreach ($images as $image) {
182+
$total++;
182183
if ($this->generateAndSavePlaceholder($image, $force)) {
183-
$count++;
184+
$generated++;
184185
}
185186
}
186187
}
187188

188-
$this->message(sprintf($this->_('Generated %d placeholders of %d images in field `%s`'), $count, $total, $field->name));
189+
$this->message(sprintf($this->_('Generated %d placeholders of %d images in field `%s`'), $generated, $total, $field->name));
189190
}
190191

191192
protected function addImageFieldSettings(HookEvent $event)
@@ -201,13 +202,14 @@ protected function addImageFieldSettings(HookEvent $event)
201202
$fs->name = '_files_fieldset_placeholders';
202203
$fs->label = $this->_('Image placeholders');
203204
$fs->icon = 'picture-o';
205+
$fs->addClass('InputfieldIsOffset');
204206
// $inputfields->insertAfter($fs, $children->first());
205207
$inputfields->add($fs);
206208

207209
// Placeholder type
208210
/** @var InputfieldRadios $f */
209211
$f = $modules->get('InputfieldRadios');
210-
$f->name = 'generateLqip';
212+
$f->name = 'imagePlaceholderType';
211213
$f->label = $this->_('Placeholder type');
212214
$f->description = $this->_('Choose whether this field should generate low-quality image placeholders (LQIP) on upload.');
213215
$f->icon = 'toggle-on';
@@ -216,19 +218,19 @@ protected function addImageFieldSettings(HookEvent $event)
216218
foreach ($this->generators as $class => $label) {
217219
$f->addOption($class::$name, $label);
218220
}
219-
$f->value = $field->generateLqip;
221+
$f->value = $field->imagePlaceholderType;
220222
$fs->add($f);
221223

222224
// Generate missing placeholders for existing images
223225
/** @var InputfieldCheckbox $f */
224226
$f = $modules->get('InputfieldCheckbox');
225-
$f->name = 'generateLqipForExisting';
227+
$f->name = 'imagePlaceholdersGenerateMissing';
226228
$f->label = $this->_('Generate missing placeholders');
227229
$f->description = $this->_('Placeholders are only generated when uploading new images.') . ' '
228230
. $this->_('Check the box below and submit the form to batch-generate image placeholders for any existing images in this field.');
229231
$f->label2 = $this->_('Generate missing placeholders for existing images');
230232
$f->collapsed = true;
231-
$f->showIf = 'generateLqip!=""';
233+
$f->showIf = 'imagePlaceholderType!=""';
232234
$f->icon = 'question-circle-o';
233235
$f->value = 1;
234236
$f->checked = false;
@@ -237,30 +239,40 @@ protected function addImageFieldSettings(HookEvent $event)
237239
// Re-generate all placeholders for existing images
238240
/** @var InputfieldCheckbox $f */
239241
$f = $modules->get('InputfieldCheckbox');
240-
$f->name = 'generateLqipForAll';
242+
$f->name = 'imagePlaceholdersRegenerateAll';
241243
$f->label = $this->_('Re-generate all placeholders');
242244
$f->description = $this->_('Check the box below and submit the form to re-generate all placeholders for any existing images in this field. Useful after changing the placeholder type.');
243245
$f->label2 = $this->_('Re-generate all placeholders for existing images');
244246
$f->collapsed = true;
245-
// $f->showIf = 'generateLqipForExisting=1';
246-
$f->showIf = 'generateLqip!=""';
247+
// $f->showIf = 'imagePlaceholdersGenerateMissing=1';
248+
$f->showIf = 'imagePlaceholderType!=""';
247249
$f->icon = 'refresh';
248250
$f->value = 1;
249251
$f->checked = false;
250252
$fs->add($f);
251253

252-
// generateLqipForExisting
254+
// imagePlaceholdersGenerateMissing
253255
}
254256

255257
protected function handleImageFieldtypeSave(HookEvent $event)
256258
{
257259
/** @var FieldtypeImage $fieldtype */
258260
$field = $event->arguments(0);
259261

260-
if ($field->generateLqipForAll) {
262+
if ($field->imagePlaceholdersRegenerateAll) {
261263
$this->createPlaceholdersForField($field, true);
262-
} else if ($field->generateLqipForExisting) {
264+
} else if ($field->imagePlaceholdersGenerateMissing) {
263265
$this->createPlaceholdersForField($field, false);
264266
}
265267
}
268+
269+
protected function isSupportedImageFormat(Pageimage $image): bool
270+
{
271+
$format = Image::getImageType($image->filename);
272+
return in_array($format, $this->supportedImageFormats());
273+
}
274+
275+
protected function supportedImageFormats() {
276+
return [\IMAGETYPE_GIF, \IMAGETYPE_JPEG, \IMAGETYPE_PNG];
277+
}
266278
}

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Low-Quality Image Placeholders (LQIP) are used to improve the perceived performa
1010
displaying a small, low-quality version of an image while the high-quality version is being loaded.
1111
The LQIP technique is often used in combination with lazy loading.
1212

13-
## How does it work
13+
## How it works
1414

1515
This module will automatically generate an image placeholder for each image that is uploaded to
1616
fields configured to use them. In your frontend templates, you can access the image placeholder as
@@ -21,8 +21,15 @@ a data URI string to display while the high-quality image is loading. See below
2121
Currently, the module supports generating two types of image placeholders. The default is
2222
`ThumbHash`.
2323

24-
- [BlurHash](https://blurha.sh/): the original format developed by Wolt
25-
- [ThumbHash](https://evanw.github.io/thumbhash/): a newer format with better color rendering and alpha channel support
24+
### BlurHash
25+
26+
[BlurHash](https://blurha.sh/) is the original placeholder algorithm, developed at Wolt. It
27+
currently has no support for alpha channels and will render transparency in black.
28+
29+
### ThumbHash
30+
31+
[ThumbHash](https://evanw.github.io/thumbhash/) is a newer algorith with improved color rendering
32+
and support for transparency.
2633

2734
## Installation
2835

@@ -40,6 +47,9 @@ You'll need to configure your image fields to generate image placeholders.
4047

4148
`Setup``Fields``[images]``Details``Image placeholders`
4249

50+
There, you can choose the type of placeholder to generate. If you're installing the module on an
51+
existing site, you can also choose to batch-generate placeholders for any existing images.
52+
4353
## Usage
4454

4555
Accessing an image's `lqip` property will return a data uri string of its placeholder. Using it as

TODO.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Todo
2+
3+
- Blurhash module compatibility if previously installed
4+
- Add examples and screenshots to readme
5+
- Dominant color

lib/Image.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,38 @@
33
namespace Daun;
44

55
class Image {
6+
static $imageTypes = [
7+
'gif' => \IMAGETYPE_GIF,
8+
'jpg' => \IMAGETYPE_JPEG,
9+
'jpeg' => \IMAGETYPE_JPEG,
10+
'png' => \IMAGETYPE_PNG
11+
];
12+
13+
public static function getImageType(string $path) {
14+
if (!file_exists($path) || !is_readable($path) || is_dir($path) || !exif_imagetype($path)) {
15+
return null;
16+
}
17+
18+
$type = null;
19+
if (function_exists('exif_imagetype')) {
20+
return exif_imagetype($path);
21+
}
22+
23+
$info = @getimagesize($path);
24+
if (isset($info[2])) {
25+
return $info[2];
26+
}
27+
28+
$extension = strtolower(pathinfo($path, \PATHINFO_EXTENSION));
29+
if (static::$imageTypes[$extension]) {
30+
return static::$imageTypes[$extension];
31+
}
32+
33+
return null;
34+
}
35+
636
public static function readImageContents(string $path) {
7-
if (!file_exists($path) || is_dir($path) || !exif_imagetype($path)) {
37+
if (!file_exists($path) || !is_readable($path) || is_dir($path) || !exif_imagetype($path)) {
838
// $this->errors("Image file does not exist", Notice::log);
939
return null;
1040
}
@@ -30,6 +60,20 @@ public static function contain(int $width, int $height, int $max): array {
3060
return [$width, $height];
3161
}
3262

63+
public static function generateDataURIFromRGB(int $r, int $g, int $b): string {
64+
$image = imagecreatetruecolor(1, 1);
65+
imagefill($image, 0, 0, imagecolorallocate($image, $r, $g, $b));
66+
67+
ob_start();
68+
imagepng($image);
69+
$contents = ob_get_contents();
70+
ob_end_clean();
71+
imagedestroy($image);
72+
73+
$data = base64_encode($contents);
74+
return "data:image/png;base64,{$data}";
75+
}
76+
3377
public static function supportsImagick(): bool {
3478
return class_exists('\\Imagick');
3579
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
namespace Daun\Placeholders;
4+
5+
use Daun\Image;
6+
use Daun\Placeholder;
7+
use ProcessWire\Pageimage;
8+
9+
class PlaceholderAverageColor extends Placeholder {
10+
public static string $name = 'average-color';
11+
12+
public static function generatePlaceholder(Pageimage $image): string {
13+
$contents = Image::readImageContents($image->filename);
14+
if ($contents) {
15+
try {
16+
return static::readAverageImageColor($contents);
17+
} catch (\Exception $e) {
18+
throw new \Exception("Error encoding average color: {$e->getMessage()}");
19+
}
20+
}
21+
return '';
22+
}
23+
24+
public static function generateDataURI(string $hash, int $width = 0, int $height = 0): string {
25+
if (!$hash || $width <= 0 || $height <= 0) {
26+
return static::$fallback;
27+
}
28+
29+
[$r, $g, $b] = explode('.', $hash);
30+
return Image::generateDataURIFromRGB($r, $g, $b);
31+
}
32+
33+
protected static function readAverageImageColor(?string $contents): string {
34+
if (!$contents) {
35+
return '';
36+
}
37+
38+
$image = imagecreatefromstring($contents);
39+
$image = imagescale($image, 1, 1);
40+
$rgba = imagecolorsforindex($image, imagecolorat($image, 0, 0));
41+
imagedestroy($image);
42+
43+
$channels = array_slice(array_values($rgba), 0, 4);
44+
45+
return implode('.', $channels);
46+
}
47+
}

0 commit comments

Comments
 (0)