Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
274284e
Install imagick==3.8.0 in app image
danloveg Oct 6, 2025
0134ffb
Create sfImagickAdapter class
danloveg Oct 6, 2025
84b8286
Fix logic when requested page > page count
danloveg Oct 6, 2025
936152a
Add deprecation warning to sfImageMagickAdapter
danloveg Oct 6, 2025
805d8df
Enable sfImagickAdapter
danloveg Oct 6, 2025
11cb9cd
Re-write ad hoc multi-page logic with Imagick
danloveg Oct 6, 2025
d531651
Simplify getting MIME type
danloveg Oct 6, 2025
b2f3d97
Fix incorrect page error issue
danloveg Oct 6, 2025
c7ada99
Add error checking in loadFile
danloveg Oct 6, 2025
509db25
Use constants instead of magic strings/nums
danloveg Oct 28, 2025
5015b72
Fix setting resolution
danloveg Oct 28, 2025
2d546f9
Add AtoM copyright block
danloveg Oct 28, 2025
42483ae
Check image magick availability for fallback cases
danloveg Oct 28, 2025
e056770
Change 'white' to constant in adapter
danloveg Oct 28, 2025
18a2d17
Revert changes to memcache installation
danloveg Nov 4, 2025
719dbed
Add missing cleanup command
danloveg Nov 4, 2025
266207d
Remove sfImageMagickAdapter references
danloveg Nov 4, 2025
a77b974
Remove the sfImageMagickAdapter.class.php file
danloveg Nov 4, 2025
eba011e
Update comments in QubitDigitalObject
danloveg Nov 4, 2025
a29a839
Add thumbnail tests for QubitDigitalObject
danloveg Nov 7, 2025
37e751b
Simplify logic in setPageCount()
danloveg Nov 7, 2025
5b950a2
Add more tests for thumbnails and page counts
danloveg Nov 7, 2025
6224535
Remove PDF tests that policy.xml is preventing
danloveg Nov 7, 2025
49302a5
Revert changes to BaseDigitalObject.php
danloveg Dec 22, 2025
4a92bf2
Add functions to check for image extensions
danloveg Dec 22, 2025
d6de81e
Adjust extension priority in plugin
danloveg Dec 22, 2025
c721411
Use pathinfo instead of regex for filename
danloveg Dec 22, 2025
a8b0610
Capitalize Imagick in message
danloveg Jan 20, 2026
a36230a
Remove types, fix error message
danloveg Feb 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ RUN set -xe \
gettext-dev \
libxslt-dev \
zlib-dev \
imagemagick-dev \
libmemcached-dev \
libzip-dev \
oniguruma-dev \
Expand All @@ -28,12 +29,12 @@ RUN set -xe \
xsl \
zip \
ldap \
&& pecl install apcu pcov xdebug \
&& pecl install apcu imagick-3.8.0 pcov xdebug \
&& curl -Ls https://github.com/websupport-sk/pecl-memcache/archive/refs/tags/8.2.tar.gz | tar xz -C / \
Comment thread
danloveg marked this conversation as resolved.
&& cd /pecl-memcache-8.2 \
&& phpize && ./configure && make && make install \
&& cd / && rm -rf /pecl-memcache-8.2 \
&& docker-php-ext-enable apcu memcache pcov xdebug \
&& docker-php-ext-enable apcu imagick memcache pcov xdebug \
&& apk add --no-cache --virtual .phpext-rundeps \
gettext \
libxslt \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public function earlyExecute()
$this->updateMessage = $this->i18n->__('Digital object derivative settings saved.');

// Relay info to template
$this->pdfinfoAvailable = sfImageMagickAdapter::pdfinfoToolAvailable();
$this->imagickAvailable = extension_loaded('imagick');
}

protected function addField($name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
</h2>
<div id="derivatives-collapse" class="accordion-collapse collapse show" aria-labelledby="derivatives-heading">
<div class="accordion-body">
<?php if ($pdfinfoAvailable) { ?>
<?php if ($imagickAvailable) { ?>
<?php echo render_field(
$form->digital_object_derivatives_pdf_page_number
->label(__('PDF page number for image derivative'))
Expand All @@ -39,7 +39,7 @@
); ?>
<?php } else { ?>
<div class="alert alert-danger" role="alert">
<?php echo __('The pdfinfo tool is required to use this functionality. Please contact your system administrator.'); ?>
<?php echo __('The Imagick PHP extension is required to use this functionality. Please contact your system administrator.'); ?>
</div>
<?php } ?>

Expand All @@ -55,7 +55,7 @@
</div>
</div>

<?php if ($pdfinfoAvailable) { ?>
<?php if ($imagickAvailable) { ?>
<section class="actions mb-3">
<input class="btn atom-btn-outline-success" type="submit" value="<?php echo __('Save'); ?>">
</section>
Expand Down
2 changes: 1 addition & 1 deletion lib/Qubit.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public static function saveTemporaryFile($name, $contents)

// Get a unique file name (to avoid clashing file names)
$tmpFileName = null;
while (file_exists($tmpFileName) || null == $tmpFileName) {
while (null == $tmpFileName || file_exists($tmpFileName)) {
$uniqueString = substr(md5(time()), 0, 8);
$tmpFileName = $tmpDir.'/QUBIT'.$uniqueString.'.'.$extension;
}
Expand Down
160 changes: 101 additions & 59 deletions lib/model/QubitDigitalObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ class QubitDigitalObject extends BaseDigitalObject
public const THUMB_MIME_TYPE = 'image/jpeg';
public const THUMB_EXTENSION = 'jpg';

// Constants for exploding multi-page assets
public const MULTI_PAGE_ASSET_EXTRACT_BACKGROUND = 'white';
public const MULTI_PAGE_ASSET_EXTRACT_FORMAT = 'jpeg';
public const MULTI_PAGE_ASSET_EXTRACT_QUALITY = 100;
public const MULTI_PAGE_ASSET_EXTRACT_RESOLUTION = 300; // This controls the DPI

// Cached properties for finding the state of loaded image manipulation extensions
public static $IMAGICK_EXTENSION_LOADED;
public static $GD_EXTENSION_LOADED;

// Variables for save actions
public $assets = [];
public $indexOnSave = true;
Expand Down Expand Up @@ -1929,38 +1939,31 @@ public function createRepresentations($usageId, $connection = null)
/**
* Set 'page_count' property for this asset.
*
* NOTE: requires the ImageMagick library
* Requires the imagick extension.
*
* @param null|mixed $connection
*
* @return QubitDigitalObject this object
*/
public function setPageCount($connection = null)
{
if ($this->canThumbnail() && sfImageMagickAdapter::isImageMagickAvailable()) {
$filename = ($this->derivativesGeneratedFromExternalMaster($this->usageId)) ? $this->getLocalPath() : $this->getAbsolutePath();
if (!$this->canThumbnail() || !self::imagickExtensionLoaded()) {
return $this;
}

$extension = pathinfo($filename, PATHINFO_EXTENSION);
$filename = ($this->derivativesGeneratedFromExternalMaster($this->usageId)) ? $this->getLocalPath() : $this->getAbsolutePath();

// If processing a PDF, attempt to use pdfinfo as it's faster
if ('pdf' == strtolower($extension) && sfImageMagickAdapter::pdfinfoToolAvailable()) {
$pages = sfImageMagickAdapter::getPdfinfoPageCount($filename);
} else {
$command = 'identify '.$filename;
exec($command, $output, $status);
$pages = count($output);
}
$im = new Imagick();
Comment thread
danloveg marked this conversation as resolved.
$im->pingImage($filename);
$pages = $im->getNumberImages();
$im->clear();

if (0 == $status) {
// Add "number of pages" property
$pageCount = new QubitProperty();
$pageCount->setObjectId($this->id);
$pageCount->setName('page_count');
$pageCount->setScope('digital_object');
$pageCount->setValue($pages, ['sourceCulture' => true]);
$pageCount->save($connection);
}
}
$pageCount = new QubitProperty();
$pageCount->setObjectId($this->id);
$pageCount->setName('page_count');
$pageCount->setScope('digital_object');
$pageCount->setValue($pages, ['sourceCulture' => true]);
$pageCount->save($connection);

return $this;
}
Expand All @@ -1980,6 +1983,8 @@ public function getPageCount()
if ($pageCount) {
return (int) $pageCount->getValue();
}

return 0;
}

// TODO: add $options for filter
Expand All @@ -1996,35 +2001,50 @@ public function getPage($index)

/**
* Explode multi-page asset into multiple image files.
*
* @return unknown
*/
public function explodeMultiPageAsset()
{
$pageCount = $this->getPageCount();

if ($pageCount > 1 && $this->canThumbnail()) {
$fileList = [];

if ($pageCount > 1 && $this->canThumbnail() && self::imagickExtensionLoaded()) {
if ($this->derivativesGeneratedFromExternalMaster($this->usageId)) {
$path = $this->localPath;
} else {
$path = $this->getAbsolutePath();
}

$filenameMinusExtension = preg_replace('/\.[a-zA-Z]{2,3}$/', '', $path);
$filenameMinusExtension = pathinfo($path, PATHINFO_FILENAME);

$command = 'convert -density 300 -alpha remove -quality 100 ';
$command .= $path;
$command .= ' '.$filenameMinusExtension.'_%02d.'.self::THUMB_EXTENSION;
exec($command, $output, $status);
$imagick = new Imagick();
Comment thread
danloveg marked this conversation as resolved.

if (1 == $status) {
throw new sfException('Encountered error'.(is_array($output) && count($output) > 0 ? ': '.implode('\n'.$output) : ' ').' while running convert (ImageMagick).');
}
// Set read resolution before opening file
$imagick->setResolution(
self::MULTI_PAGE_ASSET_EXTRACT_RESOLUTION,
self::MULTI_PAGE_ASSET_EXTRACT_RESOLUTION,
);
$imagick->readImage($path);

foreach ($imagick as $index => $page) {
$page->setImageAlphaChannel(Imagick::ALPHACHANNEL_REMOVE);
$page->setImageBackgroundColor(self::MULTI_PAGE_ASSET_EXTRACT_BACKGROUND);
$page->setImageFormat(self::MULTI_PAGE_ASSET_EXTRACT_FORMAT);
$page->setImageCompressionQuality(self::MULTI_PAGE_ASSET_EXTRACT_QUALITY);

// Build an array of the exploded file names
for ($i = 0; $i < $pageCount; ++$i) {
$fileList[] = $filenameMinusExtension.sprintf('_%02d.', $i).self::THUMB_EXTENSION;
$filename = sprintf(
'%s_%02d.%s',
$filenameMinusExtension,
$index,
self::THUMB_EXTENSION,
);

$page->writeImage($filename);

$fileList[] = $filename;
}

$imagick->clear();
}

return $fileList;
Expand All @@ -2038,21 +2058,21 @@ public function explodeMultiPageAsset()
* object and linked child digital object and move the derived
* asset to the appropriate directory for the new (child) info object
*
* NOTE: Requires the Imagemagick library for creating derivative assets
* NOTE: Requires the imagick extension for creating derivative assets
*
* @param null|mixed $connection
*
* @return QubitDigitalObject this object
*/
public function createCompoundChildren($connection = null)
{
// Bail out if the imagemagick library is not installed
if (false === sfImageMagickAdapter::isImageMagickAvailable()) {
$pages = $this->explodeMultiPageAsset();

// Bail out if no pages were found
if (0 === count($pages)) {
return $this;
}

$pages = $this->explodeMultiPageAsset();

foreach ($pages as $i => $filepath) {
$filename = basename($filepath);

Expand Down Expand Up @@ -2167,6 +2187,38 @@ public static function getMin($settings)
* -----------------------------------------------------------------------
*/

/**
* Determine whether the imagick extension is loaded.
*
* @return bool true if the extension is loaded and can be used, false otherwise
*/
public static function imagickExtensionLoaded(): bool
{
if (null !== self::$IMAGICK_EXTENSION_LOADED) {
return self::$IMAGICK_EXTENSION_LOADED;
}

self::$IMAGICK_EXTENSION_LOADED = extension_loaded('imagick');

return self::$IMAGICK_EXTENSION_LOADED;
}

/**
* Determine whether the gd extension is loaded.
*
* @return bool true if the extension is loaded and can be used, false otherwise
*/
public static function gdExtensionLoaded()
{
if (null !== self::$GD_EXTENSION_LOADED) {
return self::$GD_EXTENSION_LOADED;
}

self::$GD_EXTENSION_LOADED = extension_loaded('gd');

return self::$GD_EXTENSION_LOADED;
}

/**
* Create a thumbnail derivative for the current digital object.
*
Expand Down Expand Up @@ -2204,7 +2256,7 @@ public function getLocalPath()
if (null === $this->localPath && QubitTerm::EXTERNAL_FILE_ID == $this->usageId) {
$filename = basename($this->path);
if (false === $contents = $this->file_get_contents_if_not_empty($this->path)) {
throw new sfException(sprintf('Error reading file or file is empty.', $filepath));
throw new sfException("Error reading file '{$filename}' or file is empty.");
}
$this->localPath = Qubit::saveTemporaryFile($filename, $contents);
}
Expand Down Expand Up @@ -2382,7 +2434,7 @@ public static function resizeImage($originalImageName, $width = null, $height =
}

/**
* Get a valid adapter for the sfThumbnail library (either GD or ImageMagick)
* Get a valid adapter for the sfThumbnail library (either GD or imagick)
* Cache the adapter value because is very expensive to calculate it.
*
* @return mixed name of adapter on success, false on failure
Expand All @@ -2396,9 +2448,9 @@ public static function getThumbnailAdapter()
return $context->get('thumbnailAdapter');
}

if (sfImageMagickAdapter::isImageMagickAvailable()) {
$adapter = 'sfImageMagickAdapter';
} elseif (QubitDigitalObject::hasGdExtension()) {
if (self::imagickExtensionLoaded()) {
$adapter = 'sfImagickAdapter';
} elseif (self::gdExtensionLoaded()) {
$adapter = 'sfGDAdapter';
}

Expand All @@ -2407,16 +2459,6 @@ public static function getThumbnailAdapter()
return $adapter;
}

/**
* Test if GD Extension for PHP is installed.
*
* @return bool true if GD extension found
*/
public static function hasGdExtension()
{
return extension_loaded('gd');
}

/**
* Wrapper for canThumbnailMimeType() for use on instantiated objects.
*
Expand Down Expand Up @@ -2445,13 +2487,13 @@ public static function canThumbnailMimeType($mimeType)

$canThumbnail = false;

// For Images, we can create thumbs with either GD or ImageMagick
// For Images, we can create thumbs with either GD or imagick
if ('image' == substr($mimeType, 0, 5) && strlen($adapter)) {
$canThumbnail = true;
}

// For PDFs we can only create thumbs with ImageMagick
elseif ('application/pdf' == $mimeType && 'sfImageMagickAdapter' == $adapter) {
// For PDFs we can only create thumbs with imagick
elseif ('application/pdf' == $mimeType && 'sfImagickAdapter' == $adapter) {
$canThumbnail = true;
}

Expand Down
2 changes: 1 addition & 1 deletion lib/model/om/BaseDigitalObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public function __isset($name)

try
{
return call_user_func_array(array($this, 'QubitObject::__isset'), $args);
return parent::__isset(...$args);
}
catch (sfException $e)
{
Expand Down
9 changes: 9 additions & 0 deletions plugins/sfThumbnailPlugin/README
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

The `sfThumbnailPlugin` creates thumbnails from images. It relies on your choice of the [http://php.net/gd/ GD] or [http://www.imagemagick.org ImageMagick] libraries.

== IMPORTANT NOTE ==

This is a custom version of this plugin specific to AtoM! The `sfImageMagickAdapter` mentioned below has been replaced by the `sfImagickAdapter` class. It works the same, but uses the `imagick` PHP extension instead of `exec`-ing image magick commands.

For the examples below, you can essentially replace `sfImageMagickAdapter` with `sfImagickAdapter`. Two caveats:

- There is no binary for imagick, so the `'convert'` option mentioned below does not do anything.
- The custom thumbnailing `'method'` option is not implemented in the `sfImagickAdapter` class.

== Installation ==

To install the plugin for a symfony project, the usual process is to use the symfony command line:
Expand Down
Loading
Loading