diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php
index 06f3fecc0eab27563bf653fec14ba32f361fc568..8219ee2c30d461f8fa1e6b3cd02950e3ff8eb7cf 100644
--- a/module/VuFind/config/module.config.php
+++ b/module/VuFind/config/module.config.php
@@ -118,7 +118,7 @@ $config = [
             'VuFind\Controller\CombinedController' => 'VuFind\Controller\AbstractBaseFactory',
             'VuFind\Controller\ConfirmController' => 'VuFind\Controller\AbstractBaseFactory',
             'VuFind\Controller\ContentController' => 'VuFind\Controller\AbstractBaseFactory',
-            'VuFind\Controller\CoverController' => 'VuFind\Controller\AbstractBaseFactory',
+            'VuFind\Controller\CoverController' => 'VuFind\Controller\CoverControllerFactory',
             'VuFind\Controller\EdsController' => 'VuFind\Controller\AbstractBaseFactory',
             'VuFind\Controller\EdsrecordController' => 'VuFind\Controller\AbstractBaseFactory',
             'VuFind\Controller\EITController' => 'VuFind\Controller\AbstractBaseFactory',
@@ -309,6 +309,10 @@ $config = [
             'VuFind\ContentBlock\BlockLoader' => 'VuFind\ContentBlock\BlockLoaderFactory',
             'VuFind\ContentBlock\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
             'VuFind\Cookie\CookieManager' => 'VuFind\Cookie\CookieManagerFactory',
+            'VuFind\Cover\CachingProxy' => 'VuFind\Cover\CachingProxyFactory',
+            'VuFind\Cover\Generator' => 'VuFind\Cover\GeneratorFactory',
+            'VuFind\Cover\Layer\PluginManager' => 'VuFind\ServiceManager\AbstractPluginManagerFactory',
+            'VuFind\Cover\Loader' => 'VuFind\Cover\LoaderFactory',
             'VuFind\Cover\Router' => 'VuFind\Cover\RouterFactory',
             'VuFind\Crypt\HMAC' => 'VuFind\Crypt\HMACFactory',
             'VuFind\Date\Converter' => 'VuFind\Service\DateConverterFactory',
@@ -483,6 +487,7 @@ $config = [
             'content_summaries' => [ /* see VuFind\Content\Summaries\PluginManager for defaults */ ],
             'content_toc' => [ /* see VuFind\Content\TOC\PluginManager for defaults */ ],
             'contentblock' => [ /* see VuFind\ContentBlock\PluginManager for defaults */ ],
+            'cover_layer' => [ /* see VuFind\Cover\Layer\PluginManager for defaults */ ],
             'db_row' => [ /* see VuFind\Db\Row\PluginManager for defaults */ ],
             'db_table' => [ /* see VuFind\Db\Table\PluginManager for defaults */ ],
             'hierarchy_driver' => [ /* see VuFind\Hierarchy\Driver\PluginManager for defaults */ ],
diff --git a/module/VuFind/src/VuFind/Controller/CoverController.php b/module/VuFind/src/VuFind/Controller/CoverController.php
index e50be8caf7087a97a28278e31cedc561e7284995..c4d3b6770689de0de2db8989079236d5e41b6a2d 100644
--- a/module/VuFind/src/VuFind/Controller/CoverController.php
+++ b/module/VuFind/src/VuFind/Controller/CoverController.php
@@ -29,6 +29,7 @@ namespace VuFind\Controller;
 
 use VuFind\Cover\CachingProxy;
 use VuFind\Cover\Loader;
+use VuFind\Session\Settings as SessionSettings;
 
 /**
  * Generates covers for book entries
@@ -39,73 +40,42 @@ use VuFind\Cover\Loader;
  * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
  * @link     https://vufind.org Main Page
  */
-class CoverController extends AbstractBase
+class CoverController extends \Zend\Mvc\Controller\AbstractActionController
 {
     /**
      * Cover loader
      *
      * @var Loader
      */
-    protected $loader = false;
+    protected $loader;
 
     /**
-     * Caching proxy
+     * Proxy loader
      *
      * @var CachingProxy
      */
-    protected $proxy = false;
+    protected $proxy;
 
     /**
-     * Get the cover cache directory
+     * Session settings
      *
-     * @return string
+     * @var SessionSettings
      */
-    protected function getCacheDir()
-    {
-        return $this->serviceLocator->get('VuFind\Cache\Manager')
-            ->getCache('cover')->getOptions()->getCacheDir();
-    }
+    protected $sessionSettings = null;
 
     /**
-     * Get the cover loader object
+     * Constructor
      *
-     * @return Loader
+     * @param Loader          $loader Cover loader
+     * @param CachingProxy    $proxy  Proxy loader
+     * @param SessionSettings $ss     Session settings
      */
-    protected function getLoader()
-    {
-        // Construct object for loading cover images if it does not already exist:
-        if (!$this->loader) {
-            $cacheDir = $this->getCacheDir();
-            $this->loader = new Loader(
-                $this->getConfig(),
-                $this->serviceLocator->get('VuFind\Content\Covers\PluginManager'),
-                $this->serviceLocator->get('VuFindTheme\ThemeInfo'),
-                $this->serviceLocator->get('VuFindHttp\HttpService'),
-                $cacheDir
-            );
-            $initializer = new \VuFind\ServiceManager\ServiceInitializer();
-            $initializer($this->serviceLocator, $this->loader);
-        }
-        return $this->loader;
-    }
-
-    /**
-     * Get the caching proxy object
-     *
-     * @return CachingProxy
-     */
-    protected function getProxy()
-    {
-        if (!$this->proxy) {
-            $client = $this->serviceLocator->get('VuFindHttp\HttpService')
-                ->createClient();
-            $cacheDir = $this->getCacheDir() . '/proxy';
-            $config = $this->getConfig()->toArray();
-            $whitelist = isset($config['Content']['coverproxyCache'])
-                ? (array)$config['Content']['coverproxyCache'] : [];
-            $this->proxy = new CachingProxy($client, $cacheDir, $whitelist);
-        }
-        return $this->proxy;
+    public function __construct(Loader $loader, CachingProxy $proxy,
+        SessionSettings $ss
+    ) {
+        $this->loader = $loader;
+        $this->proxy = $proxy;
+        $this->sessionSettings = $ss;
     }
 
     /**
@@ -139,15 +109,15 @@ class CoverController extends AbstractBase
      */
     public function showAction()
     {
-        $this->disableSessionWrites();  // avoid session write timing bug
+        $this->sessionSettings->disableWrite(); // avoid session write timing bug
 
         // Special case: proxy a full URL:
-        $proxy = $this->params()->fromQuery('proxy');
-        if (!empty($proxy)) {
+        $url = $this->params()->fromQuery('proxy');
+        if (!empty($url)) {
             try {
-                $image = $this->getProxy()->fetch($proxy);
+                $image = $this->proxy->fetch($url);
                 return $this->displayImage(
-                    $image->getHeaders()->get('contenttype')->getFieldValue(),
+                    $image->getHeaders()->get('content-type')->getFieldValue(),
                     $image->getContent()
                 );
             } catch (\Exception $e) {
@@ -157,7 +127,7 @@ class CoverController extends AbstractBase
         }
 
         // Default case -- use image loader:
-        $this->getLoader()->loadImage($this->getImageParams());
+        $this->loader->loadImage($this->getImageParams());
         return $this->displayImage();
     }
 
@@ -168,8 +138,8 @@ class CoverController extends AbstractBase
      */
     public function unavailableAction()
     {
-        $this->disableSessionWrites();  // avoid session write timing bug
-        $this->getLoader()->loadUnavailable();
+        $this->sessionSettings->disableWrite(); // avoid session write timing bug
+        $this->loader->loadUnavailable();
         return $this->displayImage();
     }
 
@@ -187,7 +157,7 @@ class CoverController extends AbstractBase
         $response = $this->getResponse();
         $headers = $response->getHeaders();
         $headers->addHeaderLine(
-            'Content-type', $type ?: $this->getLoader()->getContentType()
+            'Content-type', $type ?: $this->loader->getContentType()
         );
 
         // Send proper caching headers so that the user's browser
@@ -205,7 +175,7 @@ class CoverController extends AbstractBase
             'Expires', gmdate('D, d M Y H:i:s', time() + $coverImageTtl) . ' GMT'
         );
 
-        $response->setContent($image ?: $this->getLoader()->getImage());
+        $response->setContent($image ?: $this->loader->getImage());
         return $response;
     }
 }
diff --git a/module/VuFind/src/VuFind/Controller/CoverControllerFactory.php b/module/VuFind/src/VuFind/Controller/CoverControllerFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..26de632cd2a86d138aa8747a1ccb5e1438d643bd
--- /dev/null
+++ b/module/VuFind/src/VuFind/Controller/CoverControllerFactory.php
@@ -0,0 +1,70 @@
+<?php
+/**
+ * Cover controller factory.
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Controller;
+
+use Interop\Container\ContainerInterface;
+use Zend\ServiceManager\Factory\FactoryInterface;
+
+/**
+ * Cover controller factory.
+ *
+ * @category VuFind
+ * @package  Controller
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class CoverControllerFactory implements 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 if any other error occurs
+     */
+    public function __invoke(ContainerInterface $container, $requestedName,
+        array $options = null
+    ) {
+        if (!empty($options)) {
+            throw new \Exception('Unexpected options sent to factory.');
+        }
+        return new $requestedName(
+            $container->get('VuFind\Cover\Loader'),
+            $container->get('VuFind\Cover\CachingProxy'),
+            $container->get('VuFind\Session\Settings')
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/CachingProxyFactory.php b/module/VuFind/src/VuFind/Cover/CachingProxyFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..068783abfdeaa8c7fb261285787aa5be2f986555
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/CachingProxyFactory.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Cover caching proxy factory.
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Cover;
+
+use Interop\Container\ContainerInterface;
+use Zend\ServiceManager\Factory\FactoryInterface;
+
+/**
+ * Cover caching proxy factory.
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class CachingProxyFactory implements 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 if any other error occurs
+     */
+    public function __invoke(ContainerInterface $container, $requestedName,
+        array $options = null
+    ) {
+        if (!empty($options)) {
+            throw new \Exception('Unexpected options sent to factory.');
+        }
+        $cacheDir = $container->get('VuFind\Cache\Manager')
+            ->getCache('cover')->getOptions()->getCacheDir();
+        $client = $container->get('VuFindHttp\HttpService')->createClient();
+        $config = $container->get('VuFind\Config\PluginManager')->get('config')
+            ->toArray();
+        $whitelist = isset($config['Content']['coverproxyCache'])
+            ? (array)$config['Content']['coverproxyCache'] : [];
+        return new $requestedName($client, $cacheDir . '/proxy', $whitelist);
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Generator.php b/module/VuFind/src/VuFind/Cover/Generator.php
index b3b69afbff0103c4e6c4e31fb200b452c806f6b2..299f5498b7600093bc83d282b156456f64f66ef4 100644
--- a/module/VuFind/src/VuFind/Cover/Generator.php
+++ b/module/VuFind/src/VuFind/Cover/Generator.php
@@ -27,6 +27,10 @@
  */
 namespace VuFind\Cover;
 
+use VuFind\Cover\Layer\LayerInterface;
+use VuFind\Cover\Layer\PluginManager as LayerManager;
+use VuFindTheme\ThemeInfo;
+
 /**
  * Dynamic Book Cover Generator
  *
@@ -39,46 +43,40 @@ namespace VuFind\Cover;
 class Generator
 {
     /**
-     * Style configuration
+     * Default settings (values used by setOptions() if not overridden).
      *
      * @var array
      */
-    protected $settings = [];
-
-    /**
-     * Base color used to fill initially created image.
-     *
-     * @var int
-     */
-    protected $baseColor;
-
-    /**
-     * Title's fill color
-     *
-     * @var int
-     */
-    protected $titleFillColor;
-
-    /**
-     * Title's border color
-     *
-     * @var int
-     */
-    protected $titleBorderColor;
-
-    /**
-     * Author's fill color
-     *
-     * @var int
-     */
-    protected $authorFillColor;
-
-    /**
-     * Author's border color
-     *
-     * @var int
-     */
-    protected $authorBorderColor;
+    protected $defaultSettings = [
+        'backgroundMode' => 'grid',
+        'textMode' => 'default',
+        'authorFont' => 'DroidSerif-Bold.ttf',
+        'titleFontSize' => 7,
+        'authorFontSize' => 6,
+        'lightness' => 220,
+        'maxTitleLines' => 5,
+        'minAuthorFontSize' => 5,
+        'saturation' => 80,
+        'size' => 84,
+        'textAlign' => 'center',
+        'titleFont' => 'DroidSerif-Bold.ttf',
+        'topPadding' => 19,
+        'bottomPadding' => 3,
+        'wrapWidth' => 80,
+        'titleFillColor' => 'black',
+        'titleBorderColor' => 'none',
+        'authorFillColor' => 'white',
+        'authorBorderColor' => 'black',
+        'baseColor' => 'white',
+        'accentColor' => 'random',
+    ];
+
+    /**
+     * Active style configuration
+     *
+     * @var object
+     */
+    protected $settings;
 
     /**
      * Base for image
@@ -88,75 +86,63 @@ class Generator
     protected $im;
 
     /**
-     * Width of image (pixels)
+     * ThemeInfo object
      *
-     * @var int
+     * @var ThemeInfo
      */
-    protected $width;
+    protected $themeTools;
 
     /**
-     * Height of image (pixels)
+     * Layer manager
      *
-     * @var int
+     * @var LayerManager
      */
-    protected $height;
+    protected $layerManager;
 
     /**
      * Constructor
      *
-     * @param \VuFindTheme\ThemeInfo $themeTools For font loading
-     * @param array                  $settings   Overwrite styles
+     * @param ThemeInfo    $themeTools For font loading
+     * @param LayerManager $lm         Layer manager
+     * @param array        $settings   Overwrite styles
      */
-    public function __construct($themeTools, $settings = [])
-    {
+    public function __construct(ThemeInfo $themeTools, LayerManager $lm,
+        array $settings = []
+    ) {
         $this->themeTools = $themeTools;
-        $default = [
-            'backgroundMode' => 'grid',
-            'textMode' => 'default',
-            'authorFont'   => 'DroidSerif-Bold.ttf',
-            'titleFontSize' => 7,
-            'authorFontSize' => 6,
-            'lightness'    => 220,
-            'maxTitleLines' => 5,
-            'minAuthorFontSize' => 5,
-            'saturation'   => 80,
-            'size'         => 84,
-            'textAlign'    => 'center',
-            'titleFont'    => 'DroidSerif-Bold.ttf',
-            'topPadding'   => 19,
-            'bottomPadding' => 3,
-            'wrapWidth'    => 80,
-            'titleFillColor' => 'black',
-            'titleBorderColor' => 'none',
-            'authorFillColor' => 'white',
-            'authorBorderColor' => 'black',
-            'baseColor' => 'white',
-            'accentColor' => 'random',
-        ];
-        foreach ($settings as $i => $setting) {
-            $default[$i] = $setting;
-        }
-        $default['authorFont'] = $this->fontPath($default['authorFont']);
-        $default['titleFont']  = $this->fontPath($default['titleFont']);
-        $this->settings = (object)$default;
-        $this->initImage();
-        $this->initColors();
+        $this->layerManager = $lm;
+        $this->setOptions($settings);
     }
 
     /**
-     * Initialize colors to be used in the image.
+     * Set the generator options.
+     *
+     * @param array $rawSettings Overwrite styles
      *
      * @return void
      */
-    protected function initColors()
+    public function setOptions($rawSettings)
     {
-        $this->baseColor = $this->getColor($this->settings->baseColor);
-        $this->titleFillColor = $this->getColor($this->settings->titleFillColor);
-        $this->titleBorderColor = $this->getColor($this->settings->titleBorderColor);
-        $this->authorFillColor = $this->getColor($this->settings->authorFillColor);
-        $this->authorBorderColor = $this->getColor(
-            $this->settings->authorBorderColor
-        );
+        // Merge incoming settings with defaults:
+        $settings = $rawSettings + $this->defaultSettings;
+
+        // Adjust font paths:
+        $settings['authorFont'] = $this->fontPath($settings['authorFont']);
+        $settings['titleFont']  = $this->fontPath($settings['titleFont']);
+
+        // Determine final dimensions:
+        $parts = explode('x', strtolower($settings['size']));
+        if (count($parts) < 2) {
+            $settings['width'] = $settings['height'] = $parts[0];
+        } else {
+            list($settings['width'], $settings['height']) = $parts;
+        }
+
+        // Store the results as an object:
+        $this->settings = (object)$settings;
+
+        // Reinitialize everything based on settings:
+        $this->initImage();
     }
 
     /**
@@ -167,14 +153,9 @@ class Generator
     protected function initImage()
     {
         // Create image
-        $parts = explode('x', strtolower($this->settings->size));
-        if (count($parts) < 2) {
-            $this->width = $this->height = $parts[0];
-        } else {
-            list($this->width, $this->height) = $parts;
-        }
-        if (!($this->im = imagecreate($this->width, $this->height))) {
-            throw new \Exception("Cannot Initialize new GD image stream");
+        $this->im = imagecreate($this->settings->width, $this->settings->height);
+        if (!$this->im) {
+            throw new \Exception('Cannot Initialize new GD image stream');
         }
     }
 
@@ -202,59 +183,6 @@ class Generator
         return $img;
     }
 
-    /**
-     * Check and allocates color
-     *
-     * @param string $color Legal color name from HTML4
-     *
-     * @return allocated color
-     */
-    protected function getColor($color)
-    {
-        switch (strtolower($color)) {
-        case 'black':
-            return imagecolorallocate($this->im, 0, 0, 0);
-        case 'silver':
-            return imagecolorallocate($this->im, 192, 192, 192);
-        case 'gray':
-            return imagecolorallocate($this->im, 128, 128, 128);
-        case 'white':
-            return imagecolorallocate($this->im, 255, 255, 255);
-        case 'maroon':
-            return imagecolorallocate($this->im, 128, 0, 0);
-        case 'red':
-            return imagecolorallocate($this->im, 255, 0, 0);
-        case 'purple':
-            return imagecolorallocate($this->im, 128, 0, 128);
-        case 'fuchsia':
-            return imagecolorallocate($this->im, 255, 0, 255);
-        case 'green':
-            return imagecolorallocate($this->im, 0, 128, 0);
-        case 'lime':
-            return imagecolorallocate($this->im, 0, 255, 0);
-        case 'olive':
-            return imagecolorallocate($this->im, 128, 128, 0);
-        case 'yellow':
-            return imagecolorallocate($this->im, 255, 255, 0);
-        case 'navy':
-            return imagecolorallocate($this->im, 0, 0, 128);
-        case 'blue':
-            return imagecolorallocate($this->im, 0, 0, 255);
-        case 'teal':
-            return imagecolorallocate($this->im, 0, 128, 128);
-        case 'aqua':
-            return imagecolorallocate($this->im, 0, 255, 255);
-        default:
-            if (substr($color, 0, 1) == '#' && strlen($color) == 7) {
-                $r = hexdec(substr($color, 1, 2));
-                $g = hexdec(substr($color, 3, 2));
-                $b = hexdec(substr($color, 5, 2));
-                return imagecolorallocate($this->im, $r, $g, $b);
-            }
-            return false;
-        }
-    }
-
     /**
      * Generates a dynamic cover image from elements of the item
      *
@@ -266,12 +194,11 @@ class Generator
      */
     public function generate($title, $author, $callnumber = null)
     {
-        // Generate seed from callnumber, title back up
-        $seed = $this->createSeed($title, $callnumber);
+        $details = compact('title', 'author', 'callnumber');
 
         // Build the image
-        $this->drawBackgroundLayer($seed);
-        $this->drawTextLayer($title, $author);
+        $this->getBackgroundLayer()->render($this->im, $details, $this->settings);
+        $this->getTextLayer()->render($this->im, $details, $this->settings);
 
         // Render the image
         $png = $this->renderPng();
@@ -280,311 +207,28 @@ class Generator
     }
 
     /**
-     * Draw the background behind the text.
-     *
-     * @param int $seed Seed value
+     * Get the layer plugin for the background
      *
-     * @return void
+     * @return LayerInterface
      */
-    protected function drawBackgroundLayer($seed)
+    protected function getBackgroundLayer()
     {
-        // Construct a method name using the mode setting; if the method is not
-        // defined, use the default drawGridBackground().
-        $mode = ucwords(strtolower($this->settings->backgroundMode));
-        $method = "draw{$mode}Background";
-        return method_exists($this, $method)
-            ? $this->$method($seed) : $this->drawGridBackground($seed);
-    }
-
-    /**
-     * Position the text on the image.
-     *
-     * @param string $title  Title of the book
-     * @param string $author Author of the book
-     *
-     * @return void
-     */
-    protected function drawTextLayer($title, $author)
-    {
-        // Construct a method name using the mode setting; if the method is not
-        // defined, use the default drawGridBackground().
-        $mode = ucwords(strtolower($this->settings->textMode));
-        $method = "draw{$mode}Text";
-        return method_exists($this, $method)
-            ? $this->$method($title, $author)
-            : $this->drawDefaultText($title, $author);
-    }
-
-    /**
-     * Position the text on the image using default rules.
-     *
-     * @param string $title  Title of the book
-     * @param string $author Author of the book
-     *
-     * @return void
-     */
-    protected function drawDefaultText($title, $author)
-    {
-        if (null !== $title) {
-            $this->drawTitle($title, $this->height / 8);
-        }
-        if (null !== $author) {
-            $this->drawAuthor($author);
-        }
-    }
-
-    /**
-     * Position the text on the image using "initials" rules.
-     *
-     * @param string $title  Title of the book
-     * @param string $author Author of the book
-     *
-     * @return void
-     */
-    protected function drawInitialText($title, $author)
-    {
-        // Get the first letter of title or author...
-        $initial = mb_substr($title . $author, 0, 1, 'UTF-8');
-
-        // Get the height of a character with no descenders:
-        $heightWithoutDescenders = $this->textHeight(
-            'O', $this->settings->titleFont, $this->settings->titleFontSize
-        );
-
-        // Get the height of the currently selected character:
-        $textHeight = $this->textHeight(
-            $initial, $this->settings->titleFont, $this->settings->titleFontSize
-        );
-
-        // Draw the letter... Note that the way we are using $textHeight and
-        // $heightWithoutDescenders is something of a fudge driven by the fact
-        // that PHP measures text in total pixels, but positions text using the
-        // baseline (thus not accounting for descenders). To truly vertically
-        // center something, we need more information than we can get without
-        // using an extension or library to read more information from the font
-        // file. The formula here is not particularly well-informed but seems
-        // to produce acceptable results for many scenarios.
-        $this->drawText(
-            $initial,
-            $heightWithoutDescenders + ($this->height - $textHeight) / 2,
-            $this->settings->titleFont,
-            $this->settings->titleFontSize,
-            $this->titleFillColor,
-            $this->titleBorderColor,
-            $this->settings->textAlign
+        $service = strtolower($this->settings->backgroundMode) . 'background';
+        return $this->layerManager->get(
+            $this->layerManager->has($service) ? $service : 'gridbackground'
         );
     }
 
     /**
-     * Generate an accent color from a seed value.
-     *
-     * @param int $seed Seed value
+     * Get the layer plugin for the text
      *
-     * @return int
+     * @return LayerInterface
      */
-    protected function getAccentColor($seed)
+    protected function getTextLayer()
     {
-        // Number to color, hsb to control saturation and lightness
-        if ($this->settings->accentColor == 'random') {
-            return $this->makeHSBColor(
-                $seed % 256,
-                $this->settings->saturation,
-                $this->settings->lightness
-            );
-        }
-        return $this->getColor($this->settings->accentColor);
-    }
-
-    /**
-     * Generates a solid color background, ala Google
-     *
-     * @param int $seed Seed value
-     *
-     * @return void
-     */
-    protected function drawSolidBackground($seed)
-    {
-        // Fill solid color
-        imagefilledrectangle(
-            $this->im,
-            0,
-            0,
-            $this->width,
-            $this->height,
-            $this->getAccentColor($seed)
-        );
-    }
-
-    /**
-     * Generates a grid of colors as primary feature
-     *
-     * @param int $seed Seed value
-     *
-     * @return void
-     */
-    protected function drawGridBackground($seed)
-    {
-        // Render the grid
-        $this->renderGrid($this->createPattern($seed), $this->getAccentColor($seed));
-    }
-
-    /**
-     * Generates a dynamic cover image from elements of the book
-     *
-     * @param string $title      Title of the book
-     * @param string $callnumber Callnumber of the book
-     *
-     * @return int unique number for this record
-     */
-    protected function createSeed($title, $callnumber)
-    {
-        // Turn callnumber into number
-        if (null == $callnumber) {
-            $callnumber = $title;
-        }
-        if (null !== $callnumber) {
-            $cv = 0;
-            for ($i = 0;$i < strlen($callnumber);$i++) {
-                $cv += ord($callnumber[$i]);
-            }
-            return $cv;
-        } else {
-            // If no callnumber, random
-            return ceil(rand(pow(2, 4), pow(2, 32)));
-        }
-    }
-
-    /**
-     * Turn number into pattern
-     *
-     * @param int $seed Seed used to generate the pattern
-     *
-     * @return string binary string describing a quarter of the pattern
-     */
-    protected function createPattern($seed)
-    {
-        // Convert to binary
-        $bc = decbin($seed);
-        // If we have less that a half of a quarter
-        if (strlen($bc) < 8) {
-            // Rotate square of the first 4 into a 4x2
-            // Simulate matrix rotation on string
-            $bc = substr($bc, 0, 3)
-                . substr($bc, 0, 1)
-                . substr($bc, 2, 2)
-                . substr($bc, 3, 1)
-                . substr($bc, 1, 1);
-        }
-        // If we have less than a quarter
-        if (strlen($bc) < 16) {
-            // Rotate the first 8 as a 4x2 into a 4x4
-            $bc .= strrev($bc);
-        }
-        return $bc;
-    }
-
-    /**
-     * Render title in wrapped, black text with white border
-     *
-     * @param string $title      Title to write
-     * @param int    $lineHeight Pixels we move down each line
-     *
-     * @return void
-     */
-    protected function drawTitle($title, $lineHeight)
-    {
-        $words = explode(' ', $title);
-        // Wrap words into image
-        // Add words until off image, go back and print
-        $line = '';
-        $lineCount = 0;
-        $i = 0;
-        while ($i < count($words)
-            && $lineCount < $this->settings->maxTitleLines - 1
-        ) {
-            $pline = $line;
-            // Format
-            $text = $words[$i];
-            $line .= $text . ' ';
-            $textWidth = $this->textWidth(
-                rtrim($line, ' '),
-                $this->settings->titleFont,
-                $this->settings->titleFontSize
-            );
-            if ($textWidth > $this->settings->wrapWidth) {
-                // Print black with white border
-                $this->drawText(
-                    rtrim($pline, ' '),
-                    $this->settings->topPadding + $lineHeight * $lineCount,
-                    $this->settings->titleFont,
-                    $this->settings->titleFontSize,
-                    $this->titleFillColor,
-                    $this->titleBorderColor
-                );
-                $line = $text . ' ';
-                $lineCount++;
-            }
-            $i++;
-        }
-        // Print the last words
-        $this->drawText(
-            rtrim($line, ' '),
-            $this->settings->topPadding + $lineHeight * $lineCount,
-            $this->settings->titleFont,
-            $this->settings->titleFontSize,
-            $this->titleFillColor,
-            $this->titleBorderColor
-        );
-        // Add ellipses if we've truncated
-        if ($i < count($words) - 1) {
-            $this->drawText(
-                '...',
-                $this->settings->topPadding
-                + $this->settings->maxTitleLines * $lineHeight,
-                $this->settings->titleFont,
-                $this->settings->titleFontSize + 1,
-                $this->titleFillColor,
-                $this->titleBorderColor
-            );
-        }
-    }
-
-    /**
-     * Render author at bottom in wrapped, white text with black border
-     *
-     * @param string $author Author to write
-     *
-     * @return void
-     */
-    protected function drawAuthor($author)
-    {
-        // Scale author to fit by incrementing fontsizes down
-        $fontSize = $this->settings->authorFontSize + 1;
-        do {
-            $fontSize--;
-            $textWidth = $this->textWidth(
-                $author,
-                $this->settings->authorFont,
-                $fontSize
-            );
-        } while ($textWidth > $this->settings->wrapWidth &&
-              $fontSize > $this->settings->minAuthorFontSize
-          );
-        // Too small to read? Align left
-        $textWidth = $this->textWidth(
-            $author,
-            $this->settings->authorFont,
-            $fontSize
-        );
-        $align = $textWidth > $this->width ? 'left' : null;
-        $this->drawText(
-            $author,
-            $this->height - $this->settings->bottomPadding,
-            $this->settings->authorFont,
-            $fontSize,
-            $this->authorFillColor,
-            $this->authorBorderColor,
-            $align
+        $service = strtolower($this->settings->textMode) . 'text';
+        return $this->layerManager->get(
+            $this->layerManager->has($service) ? $service : 'defaulttext'
         );
     }
 
@@ -602,171 +246,4 @@ class Generator
         $fileMatch = $this->themeTools->findContainingTheme($filenames, true);
         return empty($fileMatch) ? false : $fileMatch;
     }
-
-    /**
-     * Returns the width a string would render to
-     *
-     * @param string $text Text to test
-     * @param string $font Full font path
-     * @param string $size Size of the font
-     *
-     * @return int
-     */
-    protected function textWidth($text, $font, $size)
-    {
-        $p = imagettfbbox($size, 0, $font, $text);
-        return $p[2] - $p[0];
-    }
-
-    /**
-     * Returns the height a string would render to
-     *
-     * @param string $text Text to test
-     * @param string $font Full font path
-     * @param string $size Size of the font
-     *
-     * @return int
-     */
-    protected function textHeight($text, $font, $size)
-    {
-        $p = imagettfbbox($size, 0, $font, $text);
-        return $p[1] - $p[5];
-    }
-
-    /**
-     * Simulate outlined text
-     *
-     * @param string $text     Text to render
-     * @param int    $y        Top position
-     * @param string $font     Full path to font
-     * @param int    $fontSize Size of the font
-     * @param int    $mcolor   Main text color
-     * @param int    $scolor   Secondary border color
-     * @param string $align    'left','center','right'
-     *
-     * @return void
-     */
-    protected function drawText($text, $y, $font, $fontSize, $mcolor,
-        $scolor = false, $align = null
-    ) {
-        // If the wrap width is smaller than the image width, we want to account
-        // for this when right or left aligning to maintain padding on the image.
-        $wrapGap = ($this->width - $this->settings->wrapWidth) / 2;
-
-        $textWidth = $this->textWidth(
-            $text,
-            $font,
-            $fontSize
-        );
-        if ($textWidth > $this->width) {
-            $align = 'left';
-            $wrapGap = 0; // kill wrap gap to maximize text fit
-        }
-        if (null == $align) {
-            $align = $this->settings->textAlign;
-        }
-        if ($align == 'left') {
-            $x = $wrapGap;
-        }
-        if ($align == 'center') {
-            $x = ($this->width - $textWidth) / 2;
-        }
-        if ($align == 'right') {
-            $x = $this->width - ($textWidth + $wrapGap);
-        }
-
-        // Generate 5 lines of text, 4 offset in a border color
-        if ($scolor) {
-            imagettftext(
-                $this->im, $fontSize, 0, $x, $y + 1, $scolor, $font, $text
-            );
-            imagettftext(
-                $this->im, $fontSize, 0, $x, $y - 1, $scolor, $font, $text
-            );
-            imagettftext(
-                $this->im, $fontSize, 0, $x + 1, $y, $scolor, $font, $text
-            );
-            imagettftext(
-                $this->im, $fontSize, 0, $x - 1, $y, $scolor, $font, $text
-            );
-        }
-        // 1 centered in main color
-        imagettftext($this->im, $fontSize, 0, $x, $y, $mcolor, $font, $text);
-    }
-
-    /**
-     * Convert 16 long binary string to 8x8 color grid
-     * Reflects vertically and horizontally
-     *
-     * @param string $bc    Binary string of pattern
-     * @param int    $color Fill color
-     *
-     * @return void
-     */
-    protected function renderGrid($bc, $color)
-    {
-        $halfWidth = $this->width / 2;
-        $halfHeight = $this->height / 2;
-        $boxWidth  = $this->width / 8;
-        $boxHeight = $this->height / 8;
-
-        $bc = str_split($bc);
-        for ($k = 0;$k < 4;$k++) {
-            $x = $k % 2 ? $halfWidth : $halfWidth - $boxWidth;
-            $y = $k / 2 < 1 ? $halfHeight : $halfHeight - $boxHeight;
-            $u = $k % 2 ? $boxWidth : -$boxWidth;
-            $v = $k / 2 < 1 ? $boxHeight : -$boxHeight;
-            for ($i = 0;$i < 16;$i++) {
-                if ($bc[$i] == "1") {
-                    imagefilledrectangle(
-                        $this->im, $x, $y,
-                        $x + $boxWidth - 1, $y + $boxHeight - 1, $color
-                    );
-                }
-                $x += $u;
-                if ($x >= $this->width || $x < 0) {
-                    $x = $k % 2 ? $halfWidth : $halfWidth - $boxWidth;
-                    $y += $v;
-                }
-            }
-        }
-        //imagefilledrectangle($this->im,0,$size-11,$size-1,$size,$color);
-    }
-
-    /**
-     * Using HSB allows us to control the contrast while allowing randomness
-     *
-     * @param int $h Hue (0-255)
-     * @param int $s Saturation (0-255)
-     * @param int $v Lightness (0-255)
-     *
-     * @return int
-     */
-    protected function makeHSBColor($h, $s, $v)
-    {
-        $s /= 256.0;
-        if ($s == 0.0) {
-            return imagecolorallocate($this->im, $v, $v, $v);
-        }
-        $h /= (256.0 / 6.0);
-        $i = floor($h);
-        $f = $h - $i;
-        $p = (int)($v * (1.0 - $s));
-        $q = (int)($v * (1.0 - $s * $f));
-        $t = (int)($v * (1.0 - $s * (1.0 - $f)));
-        switch ($i) {
-        case 0:
-            return imagecolorallocate($this->im, $v, $t, $p);
-        case 1:
-            return imagecolorallocate($this->im, $q, $v, $p);
-        case 2:
-            return imagecolorallocate($this->im, $p, $v, $t);
-        case 3:
-            return imagecolorallocate($this->im, $p, $q, $v);
-        case 4:
-            return imagecolorallocate($this->im, $t, $p, $v);
-        default:
-            return imagecolorallocate($this->im, $v, $p, $q);
-        }
-    }
 }
diff --git a/module/VuFind/src/VuFind/Cover/GeneratorFactory.php b/module/VuFind/src/VuFind/Cover/GeneratorFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..36938fc0dde5cf5e5acb089d6f3f27b199aa35cd
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/GeneratorFactory.php
@@ -0,0 +1,69 @@
+<?php
+/**
+ * Cover generator factory.
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Cover;
+
+use Interop\Container\ContainerInterface;
+use Zend\ServiceManager\Factory\FactoryInterface;
+
+/**
+ * Cover generator factory.
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class GeneratorFactory implements 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 if any other error occurs
+     */
+    public function __invoke(ContainerInterface $container, $requestedName,
+        array $options = null
+    ) {
+        if (!empty($options)) {
+            throw new \Exception('Unexpected options sent to factory.');
+        }
+        return new $requestedName(
+            $container->get('VuFindTheme\ThemeInfo'),
+            $container->get('VuFind\Cover\Layer\PluginManager')
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/AbstractBackgroundLayer.php b/module/VuFind/src/VuFind/Cover/Layer/AbstractBackgroundLayer.php
new file mode 100644
index 0000000000000000000000000000000000000000..c529094743d7ca2d0d60b28bc7e62f318f7bba29
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/AbstractBackgroundLayer.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Abstract cover background layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Abstract cover background layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+abstract class AbstractBackgroundLayer extends AbstractLayer
+{
+    /**
+     * Generates a dynamic cover image from elements of the book
+     *
+     * @param string $title      Title of the book
+     * @param string $callnumber Callnumber of the book
+     *
+     * @return int unique number for this record
+     */
+    protected function createSeed($title, $callnumber)
+    {
+        // Turn callnumber into number
+        if (null == $callnumber) {
+            $callnumber = $title;
+        }
+        if (null !== $callnumber) {
+            $cv = 0;
+            for ($i = 0;$i < strlen($callnumber);$i++) {
+                $cv += ord($callnumber[$i]);
+            }
+            return $cv;
+        } else {
+            // If no callnumber, random
+            return ceil(rand(pow(2, 4), pow(2, 32)));
+        }
+    }
+
+    /**
+     * Generate an accent color from a seed value.
+     *
+     * @param resource $im       Active image resource
+     * @param int      $seed     Seed value
+     * @param object   $settings Generator settings object
+     *
+     * @return int
+     */
+    protected function getAccentColor($im, $seed, $settings)
+    {
+        // Number to color, hsb to control saturation and lightness
+        return $settings->accentColor == 'random'
+            ? $this->getHSBColor(
+                $im, $seed % 256, $settings->saturation, $settings->lightness
+            ) : $this->getColor($im, $settings->accentColor);
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/AbstractLayer.php b/module/VuFind/src/VuFind/Cover/Layer/AbstractLayer.php
new file mode 100644
index 0000000000000000000000000000000000000000..b7f7a8333e13f7bd4ceb145fca043d9b2515996c
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/AbstractLayer.php
@@ -0,0 +1,128 @@
+<?php
+/**
+ * Abstract cover layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Abstract cover layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+abstract class AbstractLayer implements LayerInterface
+{
+    /**
+     * Mapping of color names to RGB values.
+     *
+     * @var array
+     */
+    protected $colorMap = [
+        'black' => [0, 0, 0],
+        'silver' => [192, 192, 192],
+        'gray' => [128, 128, 128],
+        'white' => [255, 255, 255],
+        'maroon' => [128, 0, 0],
+        'red' => [255, 0, 0],
+        'purple' => [128, 0, 128],
+        'fuchsia' => [255, 0, 255],
+        'green' => [0, 128, 0],
+        'lime' => [0, 255, 0],
+        'olive' => [128, 128, 0],
+        'yellow' => [255, 255, 0],
+        'navy' => [0, 0, 128],
+        'blue' => [0, 0, 255],
+        'teal' => [0, 128, 128],
+        'aqua' => [0, 255, 255],
+    ];
+
+    /**
+     * Check and allocates color
+     *
+     * @param resource $im    Image resource being updated
+     * @param string   $color Legal color name from HTML4
+     *
+     * @return allocated color
+     */
+    protected function getColor($im, $color)
+    {
+        // Case one: named color found in map
+        $key = strtolower($color);
+        if (isset($this->colorMap[$key])) {
+            return imagecolorallocate($im, ...$this->colorMap[$key]);
+        }
+        // Case two: hex color
+        if (substr($color, 0, 1) == '#' && strlen($color) == 7) {
+            $r = hexdec(substr($color, 1, 2));
+            $g = hexdec(substr($color, 3, 2));
+            $b = hexdec(substr($color, 5, 2));
+            return imagecolorallocate($im, $r, $g, $b);
+        }
+        // Default case: unsupported color
+        return false;
+    }
+
+    /**
+     * Using HSB allows us to control the contrast while allowing randomness
+     *
+     * @param resource $im Active image resource
+     * @param int      $h  Hue (0-255)
+     * @param int      $s  Saturation (0-255)
+     * @param int      $v  Lightness (0-255)
+     *
+     * @return int
+     */
+    protected function getHSBColor($im, $h, $s, $v)
+    {
+        $s /= 256.0;
+        if ($s == 0.0) {
+            return imagecolorallocate($im, $v, $v, $v);
+        }
+        $h /= (256.0 / 6.0);
+        $i = floor($h);
+        $f = $h - $i;
+        $p = (int)($v * (1.0 - $s));
+        $q = (int)($v * (1.0 - $s * $f));
+        $t = (int)($v * (1.0 - $s * (1.0 - $f)));
+        switch ($i) {
+        case 0:
+            return imagecolorallocate($im, $v, $t, $p);
+        case 1:
+            return imagecolorallocate($im, $q, $v, $p);
+        case 2:
+            return imagecolorallocate($im, $p, $v, $t);
+        case 3:
+            return imagecolorallocate($im, $p, $q, $v);
+        case 4:
+            return imagecolorallocate($im, $t, $p, $v);
+        default:
+            return imagecolorallocate($im, $v, $p, $q);
+        }
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/AbstractTextLayer.php b/module/VuFind/src/VuFind/Cover/Layer/AbstractTextLayer.php
new file mode 100644
index 0000000000000000000000000000000000000000..1812adbbfd89e23fd6d3d372089e6233d8407f37
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/AbstractTextLayer.php
@@ -0,0 +1,121 @@
+<?php
+/**
+ * Abstract cover text layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Abstract cover text layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+abstract class AbstractTextLayer extends AbstractLayer
+{
+    /**
+     * Returns the width a string would render to
+     *
+     * @param string $text Text to test
+     * @param string $font Full font path
+     * @param string $size Size of the font
+     *
+     * @return int
+     */
+    protected function textWidth($text, $font, $size)
+    {
+        $p = imagettfbbox($size, 0, $font, $text);
+        return $p[2] - $p[0];
+    }
+
+    /**
+     * Returns the height a string would render to
+     *
+     * @param string $text Text to test
+     * @param string $font Full font path
+     * @param string $size Size of the font
+     *
+     * @return int
+     */
+    protected function textHeight($text, $font, $size)
+    {
+        $p = imagettfbbox($size, 0, $font, $text);
+        return $p[1] - $p[5];
+    }
+
+    /**
+     * Simulate outlined text
+     *
+     * @param resource $im       Active image resource
+     * @param object   $settings Generator settings object
+     * @param string   $text     Text to render
+     * @param int      $y        Top position
+     * @param string   $font     Full path to font
+     * @param int      $fontSize Size of the font
+     * @param int      $mcolor   Main text color
+     * @param int      $scolor   Secondary border color
+     * @param string   $align    'left','center','right'
+     *
+     * @return void
+     */
+    protected function drawText($im, $settings, $text, $y, $font, $fontSize, $mcolor,
+        $scolor = false, $align = null
+    ) {
+        // If the wrap width is smaller than the image width, we want to account
+        // for this when right or left aligning to maintain padding on the image.
+        $wrapGap = ($settings->width - $settings->wrapWidth) / 2;
+
+        $textWidth = $this->textWidth($text, $font, $fontSize);
+        if ($textWidth > $settings->width) {
+            $align = 'left';
+            $wrapGap = 0; // kill wrap gap to maximize text fit
+        }
+        if (null == $align) {
+            $align = $settings->textAlign;
+        }
+        if ($align == 'left') {
+            $x = $wrapGap;
+        }
+        if ($align == 'center') {
+            $x = ($settings->width - $textWidth) / 2;
+        }
+        if ($align == 'right') {
+            $x = $settings->width - ($textWidth + $wrapGap);
+        }
+
+        // Generate 5 lines of text, 4 offset in a border color
+        if ($scolor) {
+            imagettftext($im, $fontSize, 0, $x, $y + 1, $scolor, $font, $text);
+            imagettftext($im, $fontSize, 0, $x, $y - 1, $scolor, $font, $text);
+            imagettftext($im, $fontSize, 0, $x + 1, $y, $scolor, $font, $text);
+            imagettftext($im, $fontSize, 0, $x - 1, $y, $scolor, $font, $text);
+        }
+        // 1 centered in main color
+        imagettftext($im, $fontSize, 0, $x, $y, $mcolor, $font, $text);
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/DefaultText.php b/module/VuFind/src/VuFind/Cover/Layer/DefaultText.php
new file mode 100644
index 0000000000000000000000000000000000000000..28dd5f78eb43f2ec7bec0fd458022a7b752c85aa
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/DefaultText.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Default cover text layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Default cover text layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+class DefaultText extends AbstractTextLayer
+{
+    /**
+     * Render the layer
+     *
+     * @param resource $im       Image resource to draw on
+     * @param array    $details  Cover details array (with title/author/call_number)
+     * @param object   $settings Settings object
+     *
+     * @return void
+     */
+    public function render($im, $details, $settings)
+    {
+        if (null !== $details['title']) {
+            $lineHeight = $settings->height / 8;
+            $this->drawTitle($im, $settings, $details['title'], $lineHeight);
+        }
+        if (null !== $details['author']) {
+            $this->drawAuthor($im, $settings, $details['author']);
+        }
+    }
+
+    /**
+     * Render title in wrapped, black text with white border
+     *
+     * @param resource $im         Image resource to draw on
+     * @param object   $settings   Settings object
+     * @param string   $title      Title to write
+     * @param int      $lineHeight Pixels we move down each line
+     *
+     * @return void
+     */
+    protected function drawTitle($im, $settings, $title, $lineHeight)
+    {
+        $titleFillColor = $this->getColor($im, $settings->titleFillColor);
+        $titleBorderColor = $this->getColor($im, $settings->titleBorderColor);
+        $words = explode(' ', $title);
+        // Wrap words into image
+        // Add words until off image, go back and print
+        $line = '';
+        $lineCount = 0;
+        $i = 0;
+        while ($i < count($words)
+            && $lineCount < $settings->maxTitleLines - 1
+        ) {
+            $pline = $line;
+            // Format
+            $text = $words[$i];
+            $line .= $text . ' ';
+            $textWidth = $this->textWidth(
+                rtrim($line, ' '),
+                $settings->titleFont,
+                $settings->titleFontSize
+            );
+            if ($textWidth > $settings->wrapWidth) {
+                // Print black with white border
+                $this->drawText(
+                    $im,
+                    $settings,
+                    rtrim($pline, ' '),
+                    $settings->topPadding + $lineHeight * $lineCount,
+                    $settings->titleFont,
+                    $settings->titleFontSize,
+                    $titleFillColor,
+                    $titleBorderColor
+                );
+                $line = $text . ' ';
+                $lineCount++;
+            }
+            $i++;
+        }
+        // Print the last words
+        $this->drawText(
+            $im,
+            $settings,
+            rtrim($line, ' '),
+            $settings->topPadding + $lineHeight * $lineCount,
+            $settings->titleFont,
+            $settings->titleFontSize,
+            $titleFillColor,
+            $titleBorderColor
+        );
+        // Add ellipses if we've truncated
+        if ($i < count($words) - 1) {
+            $this->drawText(
+                $im,
+                $settings,
+                '...',
+                $settings->topPadding + $settings->maxTitleLines * $lineHeight,
+                $settings->titleFont,
+                $settings->titleFontSize + 1,
+                $titleFillColor,
+                $titleBorderColor
+            );
+        }
+    }
+
+    /**
+     * Render author at bottom in wrapped, white text with black border
+     *
+     * @param resource $im       Image resource to draw on
+     * @param object   $settings Settings object
+     * @param string   $author   Author to write
+     *
+     * @return void
+     */
+    protected function drawAuthor($im, $settings, $author)
+    {
+        $authorFillColor = $this->getColor($im, $settings->authorFillColor);
+        $authorBorderColor = $this->getColor($im, $settings->authorBorderColor);
+        // Scale author to fit by incrementing fontsizes down
+        $fontSize = $settings->authorFontSize + 1;
+        do {
+            $fontSize--;
+            $textWidth = $this->textWidth(
+                $author,
+                $settings->authorFont,
+                $fontSize
+            );
+        } while ($textWidth > $settings->wrapWidth &&
+              $fontSize > $settings->minAuthorFontSize
+          );
+        // Too small to read? Align left
+        $textWidth = $this->textWidth(
+            $author,
+            $settings->authorFont,
+            $fontSize
+        );
+        $align = $textWidth > $settings->width ? 'left' : null;
+        $this->drawText(
+            $im,
+            $settings,
+            $author,
+            $settings->height - $settings->bottomPadding,
+            $settings->authorFont,
+            $fontSize,
+            $authorFillColor,
+            $authorBorderColor,
+            $align
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/GridBackground.php b/module/VuFind/src/VuFind/Cover/Layer/GridBackground.php
new file mode 100644
index 0000000000000000000000000000000000000000..2626a68e9514234e9de5ac44f518ffde6c0ce745
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/GridBackground.php
@@ -0,0 +1,131 @@
+<?php
+/**
+ * Grid cover background layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Grid cover background layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+class GridBackground extends AbstractBackgroundLayer
+{
+    /**
+     * Render the layer
+     *
+     * @param resource $im       Image resource to draw on
+     * @param array    $details  Cover details array (with title/author/call_number)
+     * @param object   $settings Settings object
+     *
+     * @return void
+     */
+    public function render($im, $details, $settings)
+    {
+        // Generate a grid of colors as primary feature
+        $seed = $this->createSeed($details['title'], $details['callnumber']);
+        $pattern = $this->createPattern($seed);
+        $accentColor = $this->getAccentColor($im, $seed, $settings);
+        $this->renderGrid($im, $pattern, $accentColor, $settings);
+    }
+
+    /**
+     * Turn number into pattern
+     *
+     * @param int $seed Seed used to generate the pattern
+     *
+     * @return string binary string describing a quarter of the pattern
+     */
+    protected function createPattern($seed)
+    {
+        // Convert to binary
+        $bc = decbin($seed);
+        // If we have less that a half of a quarter
+        if (strlen($bc) < 8) {
+            // Rotate square of the first 4 into a 4x2
+            // Simulate matrix rotation on string
+            $bc = substr($bc, 0, 3)
+                . substr($bc, 0, 1)
+                . substr($bc, 2, 2)
+                . substr($bc, 3, 1)
+                . substr($bc, 1, 1);
+        }
+        // If we have less than a quarter
+        if (strlen($bc) < 16) {
+            // Rotate the first 8 as a 4x2 into a 4x4
+            $bc .= strrev($bc);
+        }
+        return $bc;
+    }
+
+    /**
+     * Convert 16 long binary string to 8x8 color grid
+     * Reflects vertically and horizontally
+     *
+     * @param resource $im       Active image resource
+     * @param string   $pattern  Binary string of pattern
+     * @param int      $color    Fill color
+     * @param object   $settings Generator settings object
+     *
+     * @return void
+     */
+    protected function renderGrid($im, $pattern, $color, $settings)
+    {
+        imagefilledrectangle(
+            $im, 0, 0, $settings->width, $settings->height,
+            $this->getColor($im, $settings->baseColor)
+        );
+        $halfWidth = $settings->width / 2;
+        $halfHeight = $settings->height / 2;
+        $boxWidth  = $settings->width / 8;
+        $boxHeight = $settings->height / 8;
+
+        $bc = str_split($pattern);
+        for ($k = 0;$k < 4;$k++) {
+            $x = $k % 2 ? $halfWidth : $halfWidth - $boxWidth;
+            $y = $k / 2 < 1 ? $halfHeight : $halfHeight - $boxHeight;
+            $u = $k % 2 ? $boxWidth : -$boxWidth;
+            $v = $k / 2 < 1 ? $boxHeight : -$boxHeight;
+            for ($i = 0;$i < 16;$i++) {
+                if ($bc[$i] == "1") {
+                    imagefilledrectangle(
+                        $im, $x, $y,
+                        $x + $boxWidth - 1, $y + $boxHeight - 1, $color
+                    );
+                }
+                $x += $u;
+                if ($x >= $settings->width || $x < 0) {
+                    $x = $k % 2 ? $halfWidth : $halfWidth - $boxWidth;
+                    $y += $v;
+                }
+            }
+        }
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/InitialText.php b/module/VuFind/src/VuFind/Cover/Layer/InitialText.php
new file mode 100644
index 0000000000000000000000000000000000000000..8519166bc729e9e5d5249dda79cecc6d6fe1d692
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/InitialText.php
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Initial cover text layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Initial cover text layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+class InitialText extends AbstractTextLayer
+{
+    /**
+     * Render the layer
+     *
+     * @param resource $im       Image resource to draw on
+     * @param array    $details  Cover details array (with title/author/call_number)
+     * @param object   $settings Settings object
+     *
+     * @return void
+     */
+    public function render($im, $details, $settings)
+    {
+        // Get the first letter of title or author...
+        $initial = mb_substr($details['title'] . $details['author'], 0, 1, 'UTF-8');
+
+        // Get the height of a character with no descenders:
+        $heightWithoutDescenders
+            = $this->textHeight('O', $settings->titleFont, $settings->titleFontSize);
+
+        // Get the height of the currently selected character:
+        $textHeight = $this
+            ->textHeight($initial, $settings->titleFont, $settings->titleFontSize);
+
+        // Draw the letter... Note that the way we are using $textHeight and
+        // $heightWithoutDescenders is something of a fudge driven by the fact
+        // that PHP measures text in total pixels, but positions text using the
+        // baseline (thus not accounting for descenders). To truly vertically
+        // center something, we need more information than we can get without
+        // using an extension or library to read more information from the font
+        // file. The formula here is not particularly well-informed but seems
+        // to produce acceptable results for many scenarios.
+        $this->drawText(
+            $im,
+            $settings,
+            $initial,
+            $heightWithoutDescenders + ($settings->height - $textHeight) / 2,
+            $settings->titleFont,
+            $settings->titleFontSize,
+            $this->getColor($im, $settings->titleFillColor),
+            $this->getColor($im, $settings->titleBorderColor),
+            $settings->textAlign
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/LayerInterface.php b/module/VuFind/src/VuFind/Cover/Layer/LayerInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..faf9e6886349d261477f36382c10d32cd724ce40
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/LayerInterface.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Cover layer interface
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Cover layer interface
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+interface LayerInterface
+{
+    /**
+     * Render the layer
+     *
+     * @param resource $im       Image resource to draw on
+     * @param array    $details  Cover details array (with title/author/call_number)
+     * @param object   $settings Settings object
+     *
+     * @return void
+     */
+    public function render($im, $details, $settings);
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/PluginManager.php b/module/VuFind/src/VuFind/Cover/Layer/PluginManager.php
new file mode 100644
index 0000000000000000000000000000000000000000..303ec2450e55fb9859f42eb4b898d351dbc72120
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/PluginManager.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Cover layer plugin manager
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Cover layer plugin manager
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager
+{
+    /**
+     * Default plugin aliases.
+     *
+     * @var array
+     */
+    protected $aliases = [
+        'defaulttext' => 'VuFind\Cover\Layer\DefaultText',
+        'gridbackground' => 'VuFind\Cover\Layer\GridBackground',
+        'initialtext' => 'VuFind\Cover\Layer\InitialText',
+        'solidbackground' => 'VuFind\Cover\Layer\SolidBackground',
+    ];
+
+    /**
+     * Default plugin factories.
+     *
+     * @var array
+     */
+    protected $factories = [
+        'VuFind\Cover\Layer\DefaultText' =>
+            'Zend\ServiceManager\Factory\InvokableFactory',
+        'VuFind\Cover\Layer\GridBackground' =>
+            'Zend\ServiceManager\Factory\InvokableFactory',
+        'VuFind\Cover\Layer\InitialText' =>
+            'Zend\ServiceManager\Factory\InvokableFactory',
+        'VuFind\Cover\Layer\SolidBackground' =>
+            'Zend\ServiceManager\Factory\InvokableFactory',
+    ];
+
+    /**
+     * Return the name of the base class or interface that plug-ins must conform
+     * to.
+     *
+     * @return string
+     */
+    protected function getExpectedInterface()
+    {
+        return 'VuFind\Cover\Layer\LayerInterface';
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Layer/SolidBackground.php b/module/VuFind/src/VuFind/Cover/Layer/SolidBackground.php
new file mode 100644
index 0000000000000000000000000000000000000000..2adbc2daa5893f416656e6bbc3e10c001f8f8616
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/Layer/SolidBackground.php
@@ -0,0 +1,59 @@
+<?php
+/**
+ * Solid cover background layer
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+namespace VuFind\Cover\Layer;
+
+/**
+ * Solid cover background layer
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development:plugins:hierarchy_components Wiki
+ */
+class SolidBackground extends AbstractBackgroundLayer
+{
+    /**
+     * Render the layer
+     *
+     * @param resource $im       Image resource to draw on
+     * @param array    $details  Cover details array (with title/author/call_number)
+     * @param object   $settings Settings object
+     *
+     * @return void
+     */
+    public function render($im, $details, $settings)
+    {
+        // Fill solid color
+        $seed = $this->createSeed($details['title'], $details['callnumber']);
+        $accentColor = $this->getAccentColor($im, $seed, $settings);
+        imagefilledrectangle(
+            $im, 0, 0, $settings->width, $settings->height, $accentColor
+        );
+    }
+}
diff --git a/module/VuFind/src/VuFind/Cover/Loader.php b/module/VuFind/src/VuFind/Cover/Loader.php
index 5aa367a42494a2dd9d58aa06d9d0911ecd1522ee..1ca347798e586f35e9f7f9e78df2d2e1c1194443 100644
--- a/module/VuFind/src/VuFind/Cover/Loader.php
+++ b/module/VuFind/src/VuFind/Cover/Loader.php
@@ -43,6 +43,14 @@ use VuFindCode\ISBN;
  */
 class Loader extends \VuFind\ImageLoader
 {
+    /**
+     * Class for rendering cover images dynamically if no API match found. Omit
+     * to disable functionality.
+     *
+     * @var Generator
+     */
+    protected $generator = null;
+
     /**
      * Filename constructed from ISBN
      *
@@ -194,19 +202,15 @@ class Loader extends \VuFind\ImageLoader
     }
 
     /**
-     * Get Cover Generator Object (or return false if disabled)
+     * Set Cover Generator Object
+     *
+     * @param Generator $generator Cover generator
      *
-     * @return VuFind\Cover\Generator|bool
+     * @return void
      */
-    public function getCoverGenerator()
+    public function setCoverGenerator(Generator $generator)
     {
-        if (isset($this->config->Content->makeDynamicCovers)
-            && $this->config->Content->makeDynamicCovers
-        ) {
-            $settings = $this->getCoverGeneratorSettings();
-            return new \VuFind\Cover\Generator($this->themeTools, $settings);
-        }
-        return false;
+        $this->generator = $generator;
     }
 
     /**
@@ -307,8 +311,9 @@ class Loader extends \VuFind\ImageLoader
         } elseif (!$this->fetchFromAPI()
             && !$this->fetchFromContentType()
         ) {
-            if ($generator = $this->getCoverGenerator()) {
-                $this->image = $generator->generate(
+            if ($this->generator) {
+                $this->generator->setOptions($this->getCoverGeneratorSettings());
+                $this->image = $this->generator->generate(
                     $settings['title'], $settings['author'], $settings['callnumber']
                 );
                 $this->contentType = 'image/png';
diff --git a/module/VuFind/src/VuFind/Cover/LoaderFactory.php b/module/VuFind/src/VuFind/Cover/LoaderFactory.php
new file mode 100644
index 0000000000000000000000000000000000000000..a8bd85075a701009fb6c453c0ac610de9eb7e95d
--- /dev/null
+++ b/module/VuFind/src/VuFind/Cover/LoaderFactory.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Cover loader factory.
+ *
+ * PHP version 7
+ *
+ * Copyright (C) Villanova University 2018.
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+namespace VuFind\Cover;
+
+use Interop\Container\ContainerInterface;
+use Zend\ServiceManager\Factory\FactoryInterface;
+
+/**
+ * Cover loader factory.
+ *
+ * @category VuFind
+ * @package  Cover_Generator
+ * @author   Demian Katz <demian.katz@villanova.edu>
+ * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
+ * @link     https://vufind.org/wiki/development Wiki
+ */
+class LoaderFactory implements 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 if any other error occurs
+     */
+    public function __invoke(ContainerInterface $container, $requestedName,
+        array $options = null
+    ) {
+        if (!empty($options)) {
+            throw new \Exception('Unexpected options sent to factory.');
+        }
+        $cacheDir = $container->get('VuFind\Cache\Manager')
+            ->getCache('cover')->getOptions()->getCacheDir();
+        $config = $container->get('VuFind\Config\PluginManager')->get('config');
+        $loader = new $requestedName(
+            $config,
+            $container->get('VuFind\Content\Covers\PluginManager'),
+            $container->get('VuFindTheme\ThemeInfo'),
+            $container->get('VuFindHttp\HttpService'),
+            $cacheDir
+        );
+        // Add cover generator if enabled:
+        if ($config->Content->makeDynamicCovers ?? false) {
+            $loader->setCoverGenerator($container->get('VuFind\Cover\Generator'));
+        }
+        return $loader;
+    }
+}