Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 30 additions & 2 deletions config/vufind/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1097,9 +1097,13 @@ verify_server_certificate = false

; You can select Syndetics, LibraryThing, Summon, OpenLibrary,
; Contentcafe, Buchhandel, Google, BrowZine, ObalkyKnih, Orb, Koha, Demo,
; and/or LocalFile. Service-specific notes:
; FormatBased, and/or LocalFile. Service-specific notes:
; - BrowZine requires you to have BrowZine.ini configured appropriately.
; - Buchhandel requires a valid token in the [Buchhandel] section.
; - FormatBased provides a cover image depending on the format of the
; record (Solr field "format") if none of the other providers found
; one. It is meant to be used as the last entry of the list. See the
; [FormatBasedCovers] section for configuration options.
; - Google Books caching behavior can be customized in the [Cache_GoogleCover]
; section.
; - Koha requires the koha_cover_url setting below. Additionally, for
Expand Down Expand Up @@ -1136,7 +1140,7 @@ verify_server_certificate = false
; - Orb requires that you complete the [Orb] section. Cache settings can be
; adjusted in the [Cache_OrbCover] section.
; - Summon service takes a Serials Solutions client key, NOT Summon API key!
;coverimages = Syndetics:MySyndeticsId,LibraryThing:MyLibraryThingId,Google,ObalkyKnih,OpenLibrary,Summon:MySerialsSolutionsClientKey,Buchhandel,Contentcafe:MyContentCafeID,BrowZine,LocalFile:PathToFile,Koha,Orb
;coverimages = Syndetics:MySyndeticsId,LibraryThing:MyLibraryThingId,Google,ObalkyKnih,OpenLibrary,Summon:MySerialsSolutionsClientKey,Buchhandel,Contentcafe:MyContentCafeID,BrowZine,LocalFile:PathToFile,Koha,Orb,FormatBased

; When using the Koha cover provider, you should fill in this setting:
;koha_cover_url = "https://localhost/cgi-bin/koha/opac-image.pl"
Expand Down Expand Up @@ -1274,6 +1278,30 @@ authors = disabled
; These configuration settings have been superseded by the geofeatures.ini file.
; See the [MapTab] section of the geofeatures.ini file for more information.

[FormatBasedCovers]
; Settings for the FormatBased cover loader (see the coverimages setting above).
; It provides a cover image depending on the format of the record (Solr field
; "format") if none of the other cover providers found one. It is meant to be
; used as the last entry of coverimages.
;
; image_dir: directory containing one image file per format, named after the
; format value, e.g. "Book.svg" for format "Book"
; (png, jpg, jpeg, gif, webp or svg).
; If not set, the default directory
; <VUFIND_HOME>/themes/bootstrap5/images/format-covers is used,
; which contains a set of simple default images.
; default: image used for all formats without a dedicated image; may also be
; omitted, in which case a file named "default.*" in image_dir is
; used if present.
; Any other key is an explicit mapping of a format value to an image file path
; (takes precedence over image_dir). Paths may be absolute filesystem paths or
; file:// or http(s):// URLs.
;
; Examples:
;image_dir = "/path/to/image/directory"
;default = "/path/to/default.png"
;e-Book = "/path/to/ebook.png"

; This section controls the behavior of the cover generator when makeDynamicCovers
; above is non-false.
;
Expand Down
180 changes: 180 additions & 0 deletions module/VuFind/src/VuFind/Content/Covers/FormatBased.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
<?php

/**
* FormatBased cover content loader.
*
* PHP version 8
*
* Copyright (C) Universitätsbibliothek Mannheim 2026.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see
* <https://www.gnu.org/licenses/>.
*
* @category VuFind
* @package Content
* @author Stefan Weil <sw@weilnetz.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/

namespace VuFind\Content\Covers;

use VuFind\Record\Loader as RecordLoader;

use function in_array;

/**
* FormatBased cover content loader.
*
* Returns a cover image depending on the format of the record. This is meant
* to be used as the last entry of the coverimages list in the [Content]
* section of config.ini: it only produces a result if none of the other
* providers did, and maps the record format (Solr field "format") to a
* locally stored image file.
*
* Configuration is read from the [FormatBasedCovers] section of config.ini:
* - image_dir: directory containing one image file per format, named after
* the format value (e.g. "book.png" for format "book"); if not set, the
* default directory <VUFIND_HOME>/themes/bootstrap5/images/format-covers
* (which contains a set of simple default images) is used;
* - default: image file used for all formats without a dedicated image;
* - any other key is treated as an explicit mapping of a format value to an
* image file path (takes precedence over image_dir).
*
* Image paths may be absolute filesystem paths or file:// or http(s):// URLs.
*
* @category VuFind
* @package Content
* @author Stefan Weil <sw@weilnetz.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class FormatBased extends \VuFind\Content\AbstractCover
{
/**
* Image file extensions to look for in image_dir, in order of preference.
*
* @var array
*/
protected $extensions = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'];

/**
* Constructor.
*
* @param RecordLoader $recordLoader Record loader
* @param string $imageDir Directory containing per-format images
* @param array $mapping Explicit format => image path mapping
* @param string $default Image path used for unknown formats
*/
public function __construct(
protected RecordLoader $recordLoader,
protected string $imageDir = '',
protected array $mapping = [],
protected string $default = ''
) {
$this->cacheAllowed = true;
$this->supportsRecordid = true;
}

/**
* Get image URL for a particular recordId (or false if not found).
*
* @param string $key API key, unused
* @param string $size Size of image to load (small/medium/large), unused
* @param array $ids Associative array of identifiers
*
* @return string|bool URL of the image, or false if no valid image is found
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
public function getUrl($key, $size, $ids)
{
$recordId = $ids['recordid'];
$source = $ids['source'] ?? DEFAULT_SEARCH_BACKEND;
$driver = $this->recordLoader->load($recordId, $source);
$formats = method_exists($driver, 'getFormats') ? $driver->getFormats() : [];
$format = $formats[0] ?? '';
$file = $this->findFile($format);
return $file ? $this->toUrl($file) : false;
}

/**
* Find the image file for the given format.
*
* @param string $format Record format value
*
* @return string|bool Path or URL of the image, or false if none found
*/
protected function findFile(string $format)
{
if ($format !== '') {
// Explicit mapping takes precedence:
if (isset($this->mapping[$format])) {
return $this->mapping[$format];
}
if ($this->imageDir !== '') {
$file = $this->findImageInDir($this->imageDir, $format);
if ($file !== false) {
return $file;
}
}
}
// Fall back to the default image:
if ($this->default !== '') {
return $this->default;
}
if ($this->imageDir !== '') {
return $this->findImageInDir($this->imageDir, 'default');
}
return false;
}

/**
* Look for an image file with the given name in a directory.
*
* @param string $dir Directory to search
* @param string $name Base file name (sanitized here, as it may come
* from the index)
*
* @return string|bool Path of the image, or false if not found
*/
protected function findImageInDir(string $dir, string $name)
{
$safe = preg_replace('/[^A-Za-z0-9._-]/', '', $name);
if (in_array($safe, ['', '.', '..'], true)) {
return false;
}
foreach ($this->extensions as $ext) {
$file = rtrim($dir, '/') . '/' . $safe . '.' . $ext;
if (is_readable($file)) {
return $file;
}
}
return false;
}

/**
* Convert a filesystem path to a URL understood by the image loader.
*
* @param string $file File path or URL
*
* @return string URL
*/
protected function toUrl(string $file)
{
if (preg_match('#^(file|https?)://#', $file)) {
return $file;
}
return 'file://' . $file;
}
}
97 changes: 97 additions & 0 deletions module/VuFind/src/VuFind/Content/Covers/FormatBasedFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

/**
* FormatBased cover loader factory.
*
* PHP version 8
*
* Copyright (C) Universitätsbibliothek Mannheim 2026.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see
* <https://www.gnu.org/licenses/>.
*
* @category VuFind
* @package Content
* @author Stefan Weil <sw@weilnetz.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development:plugins:cover_loaders Wiki
*/

namespace VuFind\Content\Covers;

use Laminas\ServiceManager\Exception\ServiceNotCreatedException;
use Laminas\ServiceManager\Exception\ServiceNotFoundException;
use Psr\Container\ContainerExceptionInterface as ContainerException;
use Psr\Container\ContainerInterface;
use VuFind\Config\ConfigManagerInterface;
use VuFind\Record\Loader as RecordLoader;

use function defined;
use function in_array;
use function is_string;

/**
* FormatBased cover loader factory.
*
* @category VuFind
* @package Content
* @author Stefan Weil <sw@weilnetz.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development:plugins:cover_loaders Wiki
*/
class FormatBasedFactory implements \Laminas\ServiceManager\Factory\FactoryInterface
{
/**
* Create an object.
*
* @param ContainerInterface $container Service manager
* @param string $requestedName Service being created
* @param null|array $options Extra options (optional)
*
* @return object
*
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException&\Throwable if any other error occurs
*/
public function __invoke(
ContainerInterface $container,
$requestedName,
?array $options = null
) {
if (!empty($options)) {
throw new \Exception('Unexpected options passed to factory.');
}
$config = $container->get(ConfigManagerInterface::class)->getConfigArray('config');
$section = $config['FormatBasedCovers'] ?? [];
$imageDir = isset($section['image_dir']) ? trim((string)$section['image_dir']) : '';
if ($imageDir === '' && defined('APPLICATION_PATH')) {
// Use the default set of format images shipped with the bootstrap5 theme:
$imageDir = APPLICATION_PATH . '/themes/bootstrap5/images/format-covers';
}
$default = isset($section['default']) ? trim((string)$section['default']) : '';
$mapping = [];
foreach ($section as $name => $value) {
if (in_array($name, ['image_dir', 'default'], true) || !is_string($value)) {
continue;
}
$value = trim($value);
if ($value !== '') {
$mapping[$name] = $value;
}
}
$recordLoader = $container->get(RecordLoader::class);
return new $requestedName($recordLoader, $imageDir, $mapping, $default);
}
}
2 changes: 2 additions & 0 deletions module/VuFind/src/VuFind/Content/Covers/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
'browzine' => BrowZine::class,
'contentcafe' => ContentCafe::class,
'demo' => Demo::class,
'formatbased' => FormatBased::class,
'google' => Google::class,
'koha' => Koha::class,
'librarything' => LibraryThing::class,
Expand All @@ -75,6 +76,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
* @var array
*/
protected $factories = [
FormatBased::class => FormatBasedFactory::class,
ObalkyKnih::class => ObalkyKnihContentFactory::class,
];

Expand Down
Loading