diff --git a/config/vufind/config.ini b/config/vufind/config.ini index 02fd3fc28766..5fe931d6bdb8 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -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 @@ -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" @@ -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 +; /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. ; diff --git a/module/VuFind/src/VuFind/Content/Covers/FormatBased.php b/module/VuFind/src/VuFind/Content/Covers/FormatBased.php new file mode 100644 index 000000000000..53610db49404 --- /dev/null +++ b/module/VuFind/src/VuFind/Content/Covers/FormatBased.php @@ -0,0 +1,180 @@ +. + * + * @category VuFind + * @package Content + * @author Stefan Weil + * @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 /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 + * @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; + } +} diff --git a/module/VuFind/src/VuFind/Content/Covers/FormatBasedFactory.php b/module/VuFind/src/VuFind/Content/Covers/FormatBasedFactory.php new file mode 100644 index 000000000000..83fc214ad9f2 --- /dev/null +++ b/module/VuFind/src/VuFind/Content/Covers/FormatBasedFactory.php @@ -0,0 +1,97 @@ +. + * + * @category VuFind + * @package Content + * @author Stefan Weil + * @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 + * @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); + } +} diff --git a/module/VuFind/src/VuFind/Content/Covers/PluginManager.php b/module/VuFind/src/VuFind/Content/Covers/PluginManager.php index 15ecebb036b2..55d09c9341d0 100644 --- a/module/VuFind/src/VuFind/Content/Covers/PluginManager.php +++ b/module/VuFind/src/VuFind/Content/Covers/PluginManager.php @@ -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, @@ -75,6 +76,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @var array */ protected $factories = [ + FormatBased::class => FormatBasedFactory::class, ObalkyKnih::class => ObalkyKnihContentFactory::class, ]; diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedFactoryTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedFactoryTest.php new file mode 100644 index 000000000000..ab54182ad567 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedFactoryTest.php @@ -0,0 +1,153 @@ +. + * + * @category VuFind + * @package Tests + * @author Stefan Weil + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ + +namespace VuFindTest\Content\Covers; + +use VuFind\Config\ConfigManagerInterface; +use VuFind\Content\Covers\FormatBased; +use VuFind\Content\Covers\FormatBasedFactory; +use VuFind\Record\Loader as RecordLoader; + +use function defined; + +/** + * Unit tests for FormatBased cover loader factory. + * + * @category VuFind + * @package Tests + * @author Stefan Weil + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +class FormatBasedFactoryTest extends \PHPUnit\Framework\TestCase +{ + /** + * Read a protected property of the created service. + * + * @param FormatBased $service Service to inspect + * @param string $name Property name + * + * @return mixed + */ + protected function getProperty(FormatBased $service, string $name) + { + $property = new \ReflectionProperty(FormatBased::class, $name); + return $property->getValue($service); + } + + /** + * Create the service using the given config array. + * + * @param array $config Config array as returned by ConfigManagerInterface + * + * @return FormatBased + */ + protected function createService(array $config): FormatBased + { + $configManager = $this->createMock(ConfigManagerInterface::class); + $configManager->method('getConfigArray')->with('config')->willReturn($config); + $recordLoader = $this->createMock(RecordLoader::class); + $container = $this->createMock(\Psr\Container\ContainerInterface::class); + $container->method('get')->willReturnCallback( + static function (string $id) use ($configManager, $recordLoader) { + return $id === ConfigManagerInterface::class + ? $configManager + : $recordLoader; + } + ); + $factory = new FormatBasedFactory(); + return $factory($container, FormatBased::class); + } + + /** + * Test that image_dir defaults to the bootstrap5 theme directory. + * + * @return void + */ + public function testDefaultImageDir(): void + { + $service = $this->createService([]); + $expected = defined('APPLICATION_PATH') + ? APPLICATION_PATH . '/themes/bootstrap5/images/format-covers' + : ''; + $this->assertSame($expected, $this->getProperty($service, 'imageDir')); + } + + /** + * Test that an explicit image_dir setting is used. + * + * @return void + */ + public function testExplicitImageDir(): void + { + $service = $this->createService( + ['FormatBasedCovers' => ['image_dir' => ' /tmp/images ']] + ); + $this->assertSame('/tmp/images', $this->getProperty($service, 'imageDir')); + } + + /** + * Test that an explicit empty image_dir falls back to the default. + * + * @return void + */ + public function testEmptyImageDirFallsBackToDefault(): void + { + $service = $this->createService( + ['FormatBasedCovers' => ['image_dir' => ' ']] + ); + $expected = defined('APPLICATION_PATH') + ? APPLICATION_PATH . '/themes/bootstrap5/images/format-covers' + : ''; + $this->assertSame($expected, $this->getProperty($service, 'imageDir')); + } + + /** + * Test that the default setting and format mappings are parsed. + * + * @return void + */ + public function testDefaultAndMappings(): void + { + $service = $this->createService( + [ + 'FormatBasedCovers' => [ + 'default' => '/tmp/default.png', + 'e-Book' => '/tmp/ebook.png', + 'Journal' => '/tmp/journal.png', + ], + ] + ); + $this->assertSame('/tmp/default.png', $this->getProperty($service, 'default')); + $this->assertSame( + ['e-Book' => '/tmp/ebook.png', 'Journal' => '/tmp/journal.png'], + $this->getProperty($service, 'mapping') + ); + } +} diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedTest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedTest.php new file mode 100644 index 000000000000..abc4b6edafb8 --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/Content/Covers/FormatBasedTest.php @@ -0,0 +1,281 @@ +. + * + * @category VuFind + * @package Tests + * @author Stefan Weil + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ + +namespace VuFindTest\Content\Covers; + +use VuFind\Content\Covers\FormatBased; +use VuFind\Record\Loader as RecordLoader; + +use function dirname; + +/** + * Unit tests for FormatBased cover loader. + * + * @category VuFind + * @package Tests + * @author Stefan Weil + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org + */ +class FormatBasedTest extends \PHPUnit\Framework\TestCase +{ + /** + * Directory holding test images. + * + * @var string + */ + protected $imageDir; + + /** + * Standard setup method. + * + * @return void + */ + protected function setUp(): void + { + parent::setUp(); + $this->imageDir = sys_get_temp_dir() . '/vufind_formatbased_test_' . uniqid(); + mkdir($this->imageDir, 0o777, true); + foreach (['book.png', 'journal.jpg', 'default.png'] as $file) { + file_put_contents($this->imageDir . '/' . $file, 'fake image data'); + } + } + + /** + * Standard teardown method. + * + * @return void + */ + protected function tearDown(): void + { + foreach (glob($this->imageDir . '/*') ?: [] as $file) { + unlink($file); + } + rmdir($this->imageDir); + parent::tearDown(); + } + + /** + * Create a cover loader with a mocked record loader returning the + * given formats. + * + * @param array $formats Formats reported by the record driver + * @param array $args Constructor arguments after the record loader + * + * @return FormatBased + */ + protected function createLoader(array $formats, array $args = []): FormatBased + { + $driver = new class ($formats) { + /** + * Constructor. + * + * @param array $formats Formats reported by the record driver + * + * @return void + */ + public function __construct(protected array $formats) + { + } + + /** + * Get the formats reported by the record driver. + * + * @return array + */ + public function getFormats(): array + { + return $this->formats; + } + }; + $recordLoader = $this->createMock(RecordLoader::class); + $recordLoader + ->method('load') + ->willReturn($driver); + return new FormatBased($recordLoader, ...$args); + } + + /** + * Test that the loader only supports requests with a recordid. + * + * @return void + */ + public function testSupports(): void + { + $loader = $this->createLoader([], [$this->imageDir]); + $this->assertTrue($loader->supports(['recordid' => '1'])); + $this->assertFalse($loader->supports(['isbn' => new \VuFindCode\ISBN('123')])); + $this->assertFalse($loader->supports([])); + } + + /** + * Test that the loader allows caching. + * + * @return void + */ + public function testCacheAllowed(): void + { + $loader = $this->createLoader([], [$this->imageDir]); + $this->assertTrue($loader->isCacheAllowed()); + } + + /** + * Test that an image from image_dir is returned for a known format. + * + * @return void + */ + public function testImageDirLookup(): void + { + $loader = $this->createLoader(['book'], [$this->imageDir]); + $this->assertSame( + 'file://' . $this->imageDir . '/book.png', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that jpg files in image_dir are found, too. + * + * @return void + */ + public function testImageDirLookupJpg(): void + { + $loader = $this->createLoader(['journal'], [$this->imageDir]); + $this->assertSame( + 'file://' . $this->imageDir . '/journal.jpg', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that an explicit mapping takes precedence over image_dir. + * + * @return void + */ + public function testMappingPrecedence(): void + { + $other = $this->imageDir . '/book.png'; + $loader = $this->createLoader(['book'], [$this->imageDir, ['book' => $other . '.copy']]); + file_put_contents($other . '.copy', 'fake image data'); + $this->assertSame( + 'file://' . $other . '.copy', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that http(s) URLs are passed through unchanged. + * + * @return void + */ + public function testHttpUrlPassthrough(): void + { + $loader = $this->createLoader( + ['book'], + ['', ['book' => 'https://example.com/book.png']] + ); + $this->assertSame( + 'https://example.com/book.png', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that unknown formats fall back to the default image. + * + * @return void + */ + public function testDefaultImage(): void + { + $loader = $this->createLoader(['Map'], [$this->imageDir]); + $this->assertSame( + 'file://' . $this->imageDir . '/default.png', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that an explicit default setting is used. + * + * @return void + */ + public function testExplicitDefault(): void + { + $other = $this->imageDir . '/default.png'; + $loader = $this->createLoader(['Map'], ['', [], $other]); + $this->assertSame( + 'file://' . $other, + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } + + /** + * Test that false is returned when no image is configured. + * + * @return void + */ + public function testNoConfiguredImage(): void + { + $loader = $this->createLoader(['Map']); + $this->assertFalse($loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr'])); + } + + /** + * Test that format values containing path separators cannot escape + * the image directory. + * + * @return void + */ + public function testPathTraversalSanitization(): void + { + // If sanitization were broken, "../evil" would resolve to + // /../evil.png: + file_put_contents(dirname($this->imageDir) . '/evil.png', 'sentinel'); + $loader = $this->createLoader(['../evil'], [$this->imageDir]); + $this->assertSame( + 'file://' . $this->imageDir . '/default.png', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + unlink(dirname($this->imageDir) . '/evil.png'); + } + + /** + * Test that records without a format fall back to the default image. + * + * @return void + */ + public function testEmptyFormat(): void + { + $loader = $this->createLoader([], [$this->imageDir]); + $this->assertSame( + 'file://' . $this->imageDir . '/default.png', + $loader->getUrl(null, 'small', ['recordid' => '1', 'source' => 'Solr']) + ); + } +} diff --git a/themes/bootstrap5/images/format-covers/Article.svg b/themes/bootstrap5/images/format-covers/Article.svg new file mode 100644 index 000000000000..a212dbbdd774 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Article.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Book.svg b/themes/bootstrap5/images/format-covers/Book.svg new file mode 100644 index 000000000000..bc4b8fea87d2 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Book.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/BookChapter.svg b/themes/bootstrap5/images/format-covers/BookChapter.svg new file mode 100644 index 000000000000..ca62861563b6 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/BookChapter.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Braille.svg b/themes/bootstrap5/images/format-covers/Braille.svg new file mode 100644 index 000000000000..1a3887a19732 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Braille.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/ConferenceProceeding.svg b/themes/bootstrap5/images/format-covers/ConferenceProceeding.svg new file mode 100644 index 000000000000..9ee4a871956a --- /dev/null +++ b/themes/bootstrap5/images/format-covers/ConferenceProceeding.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Database.svg b/themes/bootstrap5/images/format-covers/Database.svg new file mode 100644 index 000000000000..80e5257fe9be --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Database.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Dissertation.svg b/themes/bootstrap5/images/format-covers/Dissertation.svg new file mode 100644 index 000000000000..d28eea84cca5 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Dissertation.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Electronic.svg b/themes/bootstrap5/images/format-covers/Electronic.svg new file mode 100644 index 000000000000..10a5938e67c0 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Electronic.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Globe.svg b/themes/bootstrap5/images/format-covers/Globe.svg new file mode 100644 index 000000000000..6ed4e7334820 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Globe.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/GovernmentDocument.svg b/themes/bootstrap5/images/format-covers/GovernmentDocument.svg new file mode 100644 index 000000000000..79124d0f88b9 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/GovernmentDocument.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Journal.svg b/themes/bootstrap5/images/format-covers/Journal.svg new file mode 100644 index 000000000000..ec15f7e89bfb --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Journal.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Kit.svg b/themes/bootstrap5/images/format-covers/Kit.svg new file mode 100644 index 000000000000..f50b7cd0baca --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Kit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Map.svg b/themes/bootstrap5/images/format-covers/Map.svg new file mode 100644 index 000000000000..541476da8a74 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Map.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Microfilm.svg b/themes/bootstrap5/images/format-covers/Microfilm.svg new file mode 100644 index 000000000000..ff840ec86227 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Microfilm.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/MusicalScore.svg b/themes/bootstrap5/images/format-covers/MusicalScore.svg new file mode 100644 index 000000000000..e88ca91b7727 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/MusicalScore.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Newspaper.svg b/themes/bootstrap5/images/format-covers/Newspaper.svg new file mode 100644 index 000000000000..659897f0b108 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Newspaper.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Photo.svg b/themes/bootstrap5/images/format-covers/Photo.svg new file mode 100644 index 000000000000..3594ebcbdaf7 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Photo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/bootstrap5/images/format-covers/PhysicalObject.svg b/themes/bootstrap5/images/format-covers/PhysicalObject.svg new file mode 100644 index 000000000000..ee81eb3cd113 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/PhysicalObject.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Serial.svg b/themes/bootstrap5/images/format-covers/Serial.svg new file mode 100644 index 000000000000..31eebad7b789 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Serial.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Software.svg b/themes/bootstrap5/images/format-covers/Software.svg new file mode 100644 index 000000000000..4e83dcf6e772 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Software.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/themes/bootstrap5/images/format-covers/SoundRecording.svg b/themes/bootstrap5/images/format-covers/SoundRecording.svg new file mode 100644 index 000000000000..a42e0b794911 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/SoundRecording.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Text.svg b/themes/bootstrap5/images/format-covers/Text.svg new file mode 100644 index 000000000000..3ffee29ac6dd --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Text.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Thesis.svg b/themes/bootstrap5/images/format-covers/Thesis.svg new file mode 100644 index 000000000000..157561bc7e31 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Thesis.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/Video.svg b/themes/bootstrap5/images/format-covers/Video.svg new file mode 100644 index 000000000000..573e58a778bd --- /dev/null +++ b/themes/bootstrap5/images/format-covers/Video.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/themes/bootstrap5/images/format-covers/default.svg b/themes/bootstrap5/images/format-covers/default.svg new file mode 100644 index 000000000000..669e2e5a2541 --- /dev/null +++ b/themes/bootstrap5/images/format-covers/default.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/themes/bootstrap5/images/format-covers/eBook.svg b/themes/bootstrap5/images/format-covers/eBook.svg new file mode 100644 index 000000000000..c2df798d9f8b --- /dev/null +++ b/themes/bootstrap5/images/format-covers/eBook.svg @@ -0,0 +1,7 @@ + + + + + + +