You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by dk...@apache.org on 2021/08/11 01:10:17 UTC

[sling-org-apache-sling-thumbnails] branch main created (now dcaba14)

This is an automated email from the ASF dual-hosted git repository.

dklco pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-thumbnails.git.


      at dcaba14  Initial commit from whiteboard

This branch includes the following new commits:

     new dcaba14  Initial commit from whiteboard

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[sling-org-apache-sling-thumbnails] 01/01: Initial commit from whiteboard

Posted by dk...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

dklco pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-thumbnails.git

commit dcaba14478b83f5e491f752d039a0a1f5b6c2c89
Author: Dan Klco <kl...@adobe.com>
AuthorDate: Tue Aug 10 21:04:36 2021 -0400

    Initial commit from whiteboard
---
 .gitignore                                         |   13 +
 NOTICE                                             |    8 +
 README.md                                          |  315 +++
 bnd.bnd                                            |    3 +
 docs/original.jpeg                                 |  Bin 0 -> 46600 bytes
 docs/rendered.jpeg                                 |  Bin 0 -> 17076 bytes
 pom.xml                                            |  331 +++
 src/main/features/base.json                        |   23 +
 src/main/features/dependencies.json                |   20 +
 .../sling/thumbnails/BadRequestException.java      |   47 +
 .../apache/sling/thumbnails/OutputFileFormat.java  |   71 +
 .../apache/sling/thumbnails/RenderedResource.java  |   62 +
 .../apache/sling/thumbnails/RenditionSupport.java  |   92 +
 .../apache/sling/thumbnails/ThumbnailSupport.java  |   89 +
 .../apache/sling/thumbnails/Transformation.java    |   35 +
 .../thumbnails/TransformationHandlerConfig.java    |   41 +
 .../org/apache/sling/thumbnails/Transformer.java   |   44 +
 .../thumbnails/extension/ThumbnailProvider.java    |   52 +
 .../extension/TransformationHandler.java           |   53 +
 .../sling/thumbnails/extension/package-info.java   |   26 +
 .../internal/DynamicTransformServlet.java          |  151 ++
 .../thumbnails/internal/RenditionSupportImpl.java  |  116 +
 .../internal/ThumbnailSupportConfig.java           |   43 +
 .../thumbnails/internal/ThumbnailSupportImpl.java  |  112 +
 .../thumbnails/internal/ThumbnailsWebConsole.java  |  113 +
 .../thumbnails/internal/TransformServlet.java      |  175 ++
 .../thumbnails/internal/TransformationCache.java   |   93 +
 .../internal/TransformationServiceUser.java        |   50 +
 .../sling/thumbnails/internal/TransformerImpl.java |  139 +
 .../internal/models/RenderedResourceImpl.java      |   95 +
 .../models/TransformationHandlerConfigImpl.java    |   62 +
 .../internal/models/TransformationImpl.java        |   72 +
 .../internal/providers/ImageThumbnailProvider.java |   44 +
 .../internal/providers/PdfThumbnailProvider.java   |   57 +
 .../providers/SlideShowThumbnailProvider.java      |  106 +
 .../internal/providers/TikaFallbackProvider.java   |  102 +
 .../internal/transformers/ColorizeHandler.java     |   79 +
 .../internal/transformers/CropHandler.java         |   74 +
 .../internal/transformers/FlipHandler.java         |   69 +
 .../internal/transformers/GreyscaleHandler.java    |   53 +
 .../internal/transformers/ResizeHandler.java       |   80 +
 .../internal/transformers/RotateHandler.java       |   54 +
 .../internal/transformers/ScaleHandler.java        |   70 +
 .../internal/transformers/TransparencyHandler.java |   64 +
 .../org/apache/sling/thumbnails/package-info.java  |   26 +
 src/main/resources/OSGI-INF/l10n/bundle.properties |   43 +
 .../sling/thumbnails/internal/ContextHelper.java   |   46 +
 .../internal/DynamicTransformServletTest.java      |  280 ++
 .../internal/RenditionSupportImplTest.java         |  155 ++
 .../internal/ThumbnailSupportImplTest.java         |  130 +
 .../internal/ThumbnailsWebConsoleTest.java         |   95 +
 .../thumbnails/internal/TransformServletTest.java  |  241 ++
 .../thumbnails/internal/TransformerImplTest.java   |  121 +
 .../internal/models/RenderedResourceImplTest.java  |  211 ++
 .../internal/models/TransformationImplTest.java    |   80 +
 .../providers/ImageThumbnailProviderTest.java      |   74 +
 .../providers/PdfThumbnailProviderTest.java        |   66 +
 .../providers/SlideShowThumbnailProviderTest.java  |   84 +
 .../providers/TikaFallbackProviderTest.java        |   65 +
 .../internal/transformers/ColorizeHandlerTest.java |  111 +
 .../internal/transformers/CropHandlerTest.java     |   99 +
 .../internal/transformers/FlipHandlerTest.java     |   72 +
 .../transformers/GreyscaleHandlerTest.java         |   57 +
 .../internal/transformers/ResizeHandlerTest.java   |   94 +
 .../internal/transformers/RotateHandlerTest.java   |   92 +
 .../internal/transformers/ScaleHandlerTest.java    |  104 +
 .../transformers/TransparencyHandlerTest.java      |   87 +
 src/test/resources/Sling.docx                      |  Bin 0 -> 15143 bytes
 src/test/resources/Sling.ppt                       |  Bin 0 -> 123392 bytes
 src/test/resources/Sling.pptx                      |  Bin 0 -> 99100 bytes
 src/test/resources/apache.png                      |  Bin 0 -> 12022 bytes
 src/test/resources/conf.json                       |  355 +++
 src/test/resources/content.json                    |  115 +
 src/test/resources/editor.min.css                  | 2854 ++++++++++++++++++++
 src/test/resources/sling.pdf                       |  Bin 0 -> 251268 bytes
 src/test/resources/thumbnail.png                   |  Bin 0 -> 23882 bytes
 src/test/resources/web-console.txt                 |   24 +
 77 files changed, 9179 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a99da84
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+.project
+.classpath
+.settings
+target
+bin
+*.iml
+.idea
+.DS_Store
+dependency-reduced-pom.xml
+.vscode
+node_modules
+openwhisk_action.zip
+.java-version
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..767d735
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,8 @@
+Apache Sling Thumbnails
+Copyright 2019 The Apache Software Foundation
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
+
+This product includes software developed at
+https://github.com/coobird/thumbnailator
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..35708f7
--- /dev/null
+++ b/README.md
@@ -0,0 +1,315 @@
+[![Apache Sling](https://sling.apache.org/res/logos/sling.png)](https://sling.apache.org)
+
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
+
+# Apache Sling Thumbnails
+
+Generate and transform thumbnails from file-like Resources.
+
+## Use
+
+This library registers two servlets for generating thumbnails.
+
+### Dynamic Transform Servlet
+
+Generates and transforms thumbnails "on the fly" based on a request path, format and transformation JSON payload. Available for POST requests at the path `/bin/sling/thumbnails/transform` with the parameters `resource` (required) and `format` (optional), for example, given the following image at `/content/image/test.png`"
+
+![docs/original.jpeg](docs/original.jpeg)<br/>
+_Image credit [Markos Mant](https://unsplash.com/@markos_mant) from [Unsplash](https://unsplash.com/photos/0nKRq0IknHw)_
+
+Sending a request like:
+
+URL: http://localhost:8080/bin/sling/thumbnails/transform?resource=/content/image/test.png&format=jpeg
+
+BODY:
+
+```
+[
+    {
+        "handlerType": "sling/thumbnails/transformers/crop",
+        "properties": {
+            "width": 300,
+            "height": 300
+        }
+    },
+    {
+        "handlerType": "sling/thumbnails/transformers/colorize",
+        "properties": {
+            "red": 112,
+            "green": 66,
+            "blue": 20,
+            "alpha": 0.4
+        }
+    }
+]
+```
+
+Will result in the following thumbnail:
+
+![docs/rendered.jpeg](docs/rendered.jpeg)
+
+#### Saving Thumbnails
+
+The Dynamic Transform Servlet can save the generated thumbnail for any Persistable resource type. To do so add the URL parameter `renditionName`, e.g.:
+
+http://localhost:8080/bin/sling/thumbnails/transform?resource=/content/image/test.png&format=jpeg&renditionName=myrendition.jpeg
+
+Once saved, the rendition can be access directly using the configured Rendition Path for the Resource Type or using the Transform Servlet. Note that the rendition will only be available on the resource the servlet is executed on.
+
+### Transform Servlet
+
+The second servlet uses [Sling Context Aware Configurations](https://sling.apache.org/documentation/bundles/context-aware-configuration/context-aware-configuration.html) to generate thumbnails based on pre-defined transformation pipelines. Note that the [RenderedResource](src/main/java/org/apache/sling/thumbnails/RenderedResource.java) model is useful for retrieving the available transformation pipelines and existing renditions for a particular resource.
+
+Each available transformation can then be accessed in the form:
+
+{/path/to/file.ext}.transform/{transformation-name}.{final-extension}
+
+This for a request like:
+
+/content/images/test.png.transform/my-transformation.jpeg
+
+The servlet would:
+
+1.  Get the resource `/content/images/test.png`
+2.  Apply the transformation `my-transformation` (found in any CA Config)
+3.  Convert the result to a JPEG
+
+#### CA Config Structure
+
+The structure for the transformations under the CA Config root (e.g. /conf/global/) should include files/transformations, as such:
+
+```
+/conf/global: {
+  "jcr:primaryType": "sling:Folder",
+  "files": {
+    "jcr:primaryType": "sling:Folder",
+    "transformations": {
+      "jcr:primaryType": "sling:Folder",
+      "transformation": {
+        "jcr:primaryType": "nt:unstructured",
+        "name": "transformation",
+        "sling:resourceType": "sling/thumbnails/transformation",
+        "handlers": {
+          "jcr:primaryType": "nt:unstructured",
+          "resize": {
+            "jcr:primaryType": "nt:unstructured",
+            "height": 200,
+            "width": 200,
+            "sling:resourceType": "sling/thumbnails/transformers/resize"
+          }
+        }
+      }
+    }
+  }
+}
+```
+
+#### Persistence
+
+The Transform Servlet supports persisting renditions and using the persisted renditions instead of rendering the image on the fly. To persist renditions, you must configure the `persistableTypes` in the [Thumbnail Support Configuration](#Configuration)
+
+The `persistableTypes` node type must also be in the `supportedTypes` list. The rendition will be persisted at the provided path as an `nt:file` node with the name provided when requesting the rendition.
+
+## Installation
+
+This library can be installed on Sling 11+ but does require the following libraries:
+
+- org.apache.commons:commons-compress:1.20
+- org.apache.commons:commons-math3:3.6.1
+- org.apache.servicemix.bundles:org.apache.servicemix.bundles.poi:4.1.2_2
+- org.apache.servicemix.bundles:org.apache.servicemix.bundles.xmlbeans:3.1.0_2
+
+Add a service user `sling-thumbnails` with the following permissions:
+
+- jcr:write,jcr:nodeTypeManagement,jcr:versionManagement on /content
+- jcr:read on /
+
+Add a service user mapping: `org.apache.sling.thumbnails:sling-commons-thumbnails=sling-thumbnails`
+
+Note that this library generates [Sling Feature Models](https://sling.apache.org/documentation/development/feature-model.html) which can be used to install it easily with the Sling Feature Launcher:
+
+- org.apache.sling:org.apache.sling.thumbnails:slingosgifeature:base:{RELEASE_VERSION}
+- org.apache.sling:org.apache.sling.thumbnails:slingosgifeature:default:{RELEASE_VERSION}
+- org.apache.sling:org.apache.sling.thumbnails:slingosgifeature:dependencies:{RELEASE_VERSION}
+
+## Configuration
+
+This module requires configuring the following pid:
+
+```
+PID = org.apache.sling.thumbnails.internal.ThumbnailSupportImpl
+  errorResourcePath = /static/sling-cms/thumbnails/file.png
+  errorSuffix = /sling-cms-thumbnail.png
+  persistedTypes = []
+  supportedTypes = [nt:file=jcr:content/jcr:mimeType]
+```
+
+The the values should be set as follows:
+
+ - `errorResourcePath` - the path to a resource to transform if an error occurs instead of returning the default error page
+ - `errorSuffix` - the transformation to call on the error resource if an error occurs instead of returning the default error page
+ - `persistedTypes` - The types which support persistence of renditions in the format _resourceType=rendition-path_
+ - `supportedTypes` - The types which support thumbnail generation and transformation in the format _resourceType=metdata-path_
+
+**Note:** the _supportedTypes_ (and by extension _persistedTypes_) must be adaptable to from a Resource to an java.io.InputStream.
+
+Generally, this configuration should be provided by the application including this functionality. See [ThumbnailSupportConfig](src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportConfig.java) for more on the configuration values.
+
+## Primary Concepts
+
+There are two main concepts in this library:
+
+- `ThumbnailProviders` - implement the interface [ThumbnailProvider](src/main/java/org/apache/sling/thumbnails/ThumbnailProvider.java) and are responsible for generating a thumbnail from a defined file node
+- `TransformationHandler` - implement the class [TransformationHandler](src/main/java/org/apache/sling/thumbnails/TransformationHandler.java) and are responsible for executing a single transformation as part of a thumbnail transformation pipeline
+
+Default Thumbnail Providers and Transformation Handlers are provided with this library and both interfaces are available for extension.
+
+## Thumbnail Providers
+
+Thumbnail Providers implement the `ThumbnailProvider` interface and are responsible for generating a thumbnail from a file resource. Each handler is expected to indicate whether or not it can handle the provided resource / mime type and will be evaluated in order of their Service Ranking with the `TikaFallbackProvider` having a service ranking of Integer.MIN_VALUE
+
+The following are the included Thumbnail Providers:
+
+### Image Thumbnail Provider
+
+Directly uses the image as the thumbnail
+
+_Implementation_: `org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider`
+
+_Supported Type(s)_: All image types, except SVG
+
+### PDF Thumbnail Provider
+
+Generates a PDF thumbnail using PDFBox
+
+_Implementation_: `org.apache.sling.thumbnails.internal.providers.PDFThumbnailProvider`
+
+_Supported Type(s)_: PDF documents
+
+### Slide Show Thumbnail Provider
+
+Generates a thumbnail from PPTX / PPT documents using POI
+
+_Implementation_: `org.apache.sling.thumbnails.internal.providers.SlideShowThumbnailProvider`
+
+_Supported Type(s)_: PPTX / PPT documents
+
+### Tika Fallback Thumbnail Provider
+
+Generates a thumbnail using Apache Tika
+
+_Implementation_: `org.apache.sling.thumbnails.internal.providers.TikaFallbackProvider`
+
+_Supported Type(s)_: Any remaining document type
+
+## Transformation Handlers
+
+Transformation Handlers implement the `TransformationHandler` interface and are responsible for invoking tranformation effects on thumbnails. Each Transformation Handler is identified by a Handler Type which should correspond to a Sling Resource Type. Only one Transformation Handler is expected to be registered per resource type.
+
+Most of the transformnation handlers use the [Thumbnailator](https://github.com/coobird/thumbnailator) library under the hood to perform the transformations.
+
+The following are the included Transformation Handlers:
+
+### Colorize Hander
+
+Adds a color tint to an image.
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.ColorizeHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/colorize`
+
+_Parameters_
+
+- red - the red color value (0-255)
+- green - the green color value (0-255)
+- blue - the blue color value (0-255)
+- alpha - the level of transparency, with lower being more transparent (0.0 - 1.0)
+
+### Crop Handler
+
+Crops an image to the size specified. Note the width and height must be specified.
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.ColorizeHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/colorize`
+
+_Parameters_
+
+- height - the height to constrain the crop (1+)
+- width - the width to constrain the crop (1+)
+- position - one of:
+  - BOTTOM_CENTER
+  - BOTTOM_LEFT
+  - BOTTOM_RIGHT
+  - CENTER
+  - CENTER_LEFT
+  - CENTER_RIGHT
+  - TOP_CENTER
+  - TOP_LEFT
+  - TOP_RIGHT
+
+### Greyscale Handler
+
+Converts an image to greyscale. Note this will remove transparency.
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.GreyscaleHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/greyscale`
+
+_Parameters_
+
+N/A
+
+### Resize Handler
+
+Resizes an image to the size specified. If only one dimension is specified the image will be sized to the other
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.ResizeHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/resize`
+
+_Parameters_
+
+- height - the height to constrain the crop (1+)
+- width - the width to constrain the crop (1+)
+- keepAspectRatio - boolean, if false, the exact width and height will be used which will not preserve the aspect ratio of the original image.
+
+### Rotate Handler
+
+Rotates an image.
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.RotateHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/rotate`
+
+_Parameters_
+
+- degrees - the number of degrees to rotate the image (any number)
+
+### Scale Handler
+
+Sets the scaling factor for the width and height of the image. If the scaling factor for the width and height are not equal, then the image will not preserve the aspect ratio of the original image.
+
+Note: either both or the width+height must be set.
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.ScaleHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/scale`
+
+_Parameters_
+
+- both - scale the thumbnail by the same factor for width and height (0+ - 1.0+)
+- width - scale the thumbnail width (0+ - 1.0+)
+- height - scale the thumbnail height (0+ - 1.0+)
+
+### Transparency Handler
+
+Makes an image transparent
+
+_Implementation_: `org.apache.sling.thumbnails.internal.transformers.TransparencyHandler`
+
+_Handler Type_: `sling/thumbnails/transformers/transparency`
+
+_Parameters_
+
+- alpha - the level of transparency, with lower being more transparent (0.0 - 1.0)
diff --git a/bnd.bnd b/bnd.bnd
new file mode 100644
index 0000000..318e24a
--- /dev/null
+++ b/bnd.bnd
@@ -0,0 +1,3 @@
+Sling-Model-Packages: org.apache.sling.thumbnails
+
+-includeresource: lib/thumbnailator.jar=thumbnailator-[[0-9\.]]*.jar;lib:=true
diff --git a/docs/original.jpeg b/docs/original.jpeg
new file mode 100644
index 0000000..c95412c
Binary files /dev/null and b/docs/original.jpeg differ
diff --git a/docs/rendered.jpeg b/docs/rendered.jpeg
new file mode 100644
index 0000000..69e4136
Binary files /dev/null and b/docs/rendered.jpeg differ
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..5a3b07d
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,331 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional
+    information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except
+    in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to
+    in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
+    the License for the specific language governing permissions and limitations under the License. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <artifactId>sling-bundle-parent</artifactId>
+        <groupId>org.apache.sling</groupId>
+        <version>43</version>
+    </parent>
+    <artifactId>org.apache.sling.thumbnails</artifactId>
+    <name>Apache Sling Thumbnail Support</name>
+    <description>An API and Service for creating and transforming images and documents into thumbnails</description>
+    <version>1.0.0-SNAPSHOT</version>
+
+    <properties>
+        <sling.java.version>8</sling.java.version>
+    </properties>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-maven-plugin</artifactId>
+                <version>5.3.0</version>
+            </plugin>
+            <plugin>
+                <groupId>biz.aQute.bnd</groupId>
+                <artifactId>bnd-baseline-maven-plugin</artifactId>
+                <configuration>
+                    <failOnMissing>false</failOnMissing>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <source>8</source>
+                    <target>8</target>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <!-- Used by maven-remote-resources-plugin -->
+                        <exclude>src/main/appended-resources/META-INF/*</exclude>
+                        <!-- Generated by maven-remote-resources-plugin -->
+                        <exclude>velocity.log</exclude>
+                        <!-- don't check anything in target -->
+                        <exclude>target/*</exclude>
+                        <!-- for some reason Rat doesn't seem to pick up that PPTs are binary -->
+                        <exclude>src/test/resources/*.ppt</exclude>
+                        <!-- exclude verify text -->
+                        <exclude>src/test/resources/*.txt</exclude>
+                    </excludes>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>verify</phase>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <!-- Code Coverage report generation -->
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.7</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>generate-code-coverage-report</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>slingfeature-maven-plugin</artifactId>
+                <version>1.4.18</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <framework>
+                        <groupId>org.apache.felix</groupId>
+                        <artifactId>org.apache.felix.framework</artifactId>
+                        <version>6.0.3</version>
+                    </framework>
+                    <aggregates>
+                        <aggregate>
+                            <classifier>default</classifier>
+                            <filesInclude>**/*.json</filesInclude>
+                            <title>Apache Sling Thumbnails - Default</title>
+                        </aggregate>
+                    </aggregates>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>attach-features</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>aggregate-features</goal>
+                            <goal>attach-features</goal>
+                        </goals>
+                        <configuration>
+                            <replacePropertyVariables>project.version</replacePropertyVariables>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.5</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.webconsole</artifactId>
+            <version>4.3.8</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr-commons</artifactId>
+            <version>2.16.3</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>21.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.inject</groupId>
+            <artifactId>javax.inject</artifactId>
+            <version>1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.annotation</artifactId>
+            <version>7.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <version>1.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.metatype.annotations</artifactId>
+            <version>1.4.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.core</artifactId>
+            <version>6.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+            <version>6.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <!-- Sling Dependencies -->
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.18.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.caconfig.api</artifactId>
+            <version>1.1.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.classloader</artifactId>
+            <version>1.4.2</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.event.api</artifactId>
+            <version>1.0.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.models.api</artifactId>
+            <version>1.3.6</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Utilities -->
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.6</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>15.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>2.9.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.8.1</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
+            <version>1.3.2</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Document handling -->
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <version>0.4.14</version>
+        </dependency>
+        <dependency>
+            <artifactId>pdfbox</artifactId>
+            <groupId>org.apache.pdfbox</groupId>
+            <version>2.0.12</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+            <version>5.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-scratchpad</artifactId>
+            <version>5.0.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tika</groupId>
+            <artifactId>tika-core</artifactId>
+            <version>1.19.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Testing dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+            <version>3.0.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>3.6.28</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.xml.bind</groupId>
+            <artifactId>jakarta.xml.bind-api</artifactId>
+            <version>2.3.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.jaxb</groupId>
+            <artifactId>jaxb-runtime</artifactId>
+            <version>2.3.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.reflections</groupId>
+            <artifactId>reflections</artifactId>
+            <version>0.9.10</version>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/src/main/features/base.json b/src/main/features/base.json
new file mode 100644
index 0000000..98ca98c
--- /dev/null
+++ b/src/main/features/base.json
@@ -0,0 +1,23 @@
+{
+    "bundles": [
+        {
+            "id": "org.apache.sling:org.apache.sling.thumbnails:${project.version}",
+            "start-order": "20"
+        }
+    ],
+    "configurations":{
+        "org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~sling-thumbnails":{
+            "user.mapping":[
+                "org.apache.sling.thumbnails:sling-thumbnails=[sling-thumbnails]"
+            ]
+        }
+    },
+    "repoinit:TEXT|true": [
+        "create service user sling-thumbnails with path system/sling",
+        "set principal ACL for sling-thumbnails",
+        "allow   jcr:write,jcr:nodeTypeManagement,jcr:versionManagement    on /content",
+        "allow   jcr:read    on /",
+        "end"
+        
+    ]
+}
diff --git a/src/main/features/dependencies.json b/src/main/features/dependencies.json
new file mode 100644
index 0000000..610eba5
--- /dev/null
+++ b/src/main/features/dependencies.json
@@ -0,0 +1,20 @@
+{
+  "bundles": [
+    {
+      "id": "org.apache.commons:commons-compress:1.20",
+      "start-order": "15"
+    },
+    {
+      "id": "org.apache.commons:commons-math3:3.6.1",
+      "start-order": "15"
+    },
+    {
+      "id": "org.apache.servicemix.bundles:org.apache.servicemix.bundles.poi:4.1.2_2",
+      "start-order": "15"
+    },
+    {
+      "id": "org.apache.servicemix.bundles:org.apache.servicemix.bundles.xmlbeans:3.1.0_2",
+      "start-order": "15"
+    }
+  ]
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/BadRequestException.java b/src/main/java/org/apache/sling/thumbnails/BadRequestException.java
new file mode 100644
index 0000000..03d7f6d
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/BadRequestException.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.util.stream.Collectors;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Exception to indicate that the request provided is invalid
+ */
+@ProviderType
+public class BadRequestException extends RuntimeException {
+
+    public BadRequestException(String message) {
+        super(message);
+    }
+
+    public BadRequestException(String message, Exception cause) {
+        super(message, cause);
+    }
+
+    public BadRequestException(String message, ValueMap properties) {
+        super(String.format(message, properties.entrySet().stream().map(en -> en.getKey() + "=" + en.getValue())
+                .collect(Collectors.joining("\n"))));
+    }
+
+    public BadRequestException(String message, ValueMap properties, Exception cause) {
+        super(String.format(message, properties.entrySet().stream().map(en -> en.getKey() + "=" + en.getValue())
+                .collect(Collectors.joining("\n"))), cause);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/thumbnails/OutputFileFormat.java b/src/main/java/org/apache/sling/thumbnails/OutputFileFormat.java
new file mode 100644
index 0000000..9e72f4f
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/OutputFileFormat.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import com.google.common.net.MediaType;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Enumeration of the valid output formats for the thumbnail generator.
+ */
+@ProviderType
+public enum OutputFileFormat {
+    GIF(MediaType.GIF.toString()), JPEG(MediaType.JPEG.toString()), PNG(MediaType.PNG.toString());
+
+    /**
+     * Loads the output format requested in the specified request suffix.
+     * 
+     * @param request the current request from which to get the suffix
+     * @return the format for the suffix
+     */
+    public static OutputFileFormat forRequest(SlingHttpServletRequest request) {
+        return forValue(StringUtils.substringAfterLast(request.getRequestPathInfo().getSuffix(), "."));
+
+    }
+
+    /**
+     * Loads the output format
+     * 
+     * @param request the requested format
+     * @return the format requested
+     */
+    public static OutputFileFormat forValue(@NotNull String format) {
+        format = format.toUpperCase();
+        if ("JPG".equals(format)) {
+            format = "JPEG";
+        }
+        try {
+            return Enum.valueOf(OutputFileFormat.class, format);
+        } catch (IllegalArgumentException | NullPointerException e) {
+            throw new BadRequestException("Could not get valid extension from: " + format);
+        }
+    }
+
+    private String mimeType;
+
+    private OutputFileFormat(String mimeType) {
+        this.mimeType = mimeType;
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/RenderedResource.java b/src/main/java/org/apache/sling/thumbnails/RenderedResource.java
new file mode 100644
index 0000000..1af98de
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/RenderedResource.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.util.List;
+
+import org.apache.sling.api.resource.Resource;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * A model interface for getting the renditions for a resource.
+ */
+public interface RenderedResource {
+
+    /**
+     * Gets the transformations defined in this resource's CA Configs
+     * 
+     * @return the transformations available for the resource
+     */
+    @NotNull
+    List<Transformation> getAvailableTransformations();
+
+    /**
+     * Get the renditions for the requested resource
+     * 
+     * @return the renditions for the resource
+     */
+    @NotNull
+    List<Resource> getRenditions();
+
+    /**
+     * Get the relative path to the renditions within the resource, e.g.
+     * jcr:content/renditions
+     * 
+     * @return the relative path to the renditions
+     */
+    @NotNull
+    String getRenditionsPath();
+
+    /**
+     * Gets all of the supported renditions for this resource, e.g. the union of the
+     * transformations defined in this resource's CA Configs without extensions and
+     * the existing renditions (with extensions)
+     * 
+     * @return the list of the supported renditions
+     */
+    List<String> getSupportedRenditions();
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/RenditionSupport.java b/src/main/java/org/apache/sling/thumbnails/RenditionSupport.java
new file mode 100644
index 0000000..f655e24
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/RenditionSupport.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.io.InputStream;
+import java.util.List;
+
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Service for interacting with renditions
+ */
+@ProviderType
+public interface RenditionSupport {
+
+    /**
+     * Retrieves the rendition with the specified rendition name, if one exists.
+     * 
+     * @param file          the file from which to retrieve the rendition
+     * @param renditionName the rendition to retrieve
+     * @return the rendition resource or null
+     */
+    @Nullable
+    Resource getRendition(@NotNull Resource file, @NotNull String renditionName);
+
+    /**
+     * Retrieves the inputstream of the data of a rendition with the specified
+     * rendition name, if one exists.
+     * 
+     * @param file          the file from which to retrieve the rendition
+     * @param renditionName the rendition to retrieve
+     * @return the rendition contents or null
+     */
+    @Nullable
+    InputStream getRenditionContent(@NotNull Resource file, @NotNull String renditionName);
+
+    /**
+     * Retrieves all of the renditions for the specified file.
+     * 
+     * @param file the file from which to retrieve the renditions
+     * @return the renditions
+     */
+    @NotNull
+    List<Resource> listRenditions(@NotNull Resource file);
+
+    /**
+     * Returns true if the requested rendition exists for the specified file.
+     * 
+     * @param file          the file to check
+     * @param renditionName the rendition name to check (including extension)
+     * @return true if the rendition exists, false otherwise
+     */
+    boolean renditionExists(@NotNull Resource file, @NotNull String renditionName);
+
+    /**
+     * Checks if the file supports renditions, e.g. it's defined as a Persistable
+     * Type.
+     * 
+     * @param file the file to check
+     * @return true if the file supports renditons, false otherwise
+     */
+    boolean supportsRenditions(@NotNull Resource file);
+
+    /**
+     * Sets the content of the rendition, overriding any existing content
+     * 
+     * @param file
+     * @param renditionName
+     * @param baos
+     */
+    void setRendition(@NotNull Resource file, @NotNull String renditionName, @NotNull InputStream baos)
+            throws PersistenceException;
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/ThumbnailSupport.java b/src/main/java/org/apache/sling/thumbnails/ThumbnailSupport.java
new file mode 100644
index 0000000..a83022e
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/ThumbnailSupport.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.util.Set;
+
+import org.jetbrains.annotations.NotNull;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Supports configuring which resource types should be supported as files for
+ * thumbnail generation. Each type supported must be adaptable to a
+ * java.io.InputStream.
+ */
+@ProviderType
+public interface ThumbnailSupport {
+
+    /**
+     * Gets the property path for the resources's file meta type
+     * 
+     * @param resourceType the resource typefor which to look up the meta type
+     * @return the meta type path for the node type
+     * @throws java.lang.IllegalArgumentException the specified resource typeis not
+     *                                            a supported
+     */
+    @NotNull
+    String getMetaTypePropertyPath(@NotNull String resourceType);
+
+    /**
+     * Gets all of the resource types which support persisting renditions
+     * 
+     * @return the set of persistable resource types
+     */
+    @NotNull
+    Set<String> getPersistableTypes();
+
+    /**
+     * Gets the path under which to persist the renditions for the specified
+     * resource type
+     * 
+     * @param resourceType the resource type to get the rendition path
+     * @return the path under which to persist the renditions for the resource type
+     * @throws java.lang.IllegalArgumentException the specified resource typeis not
+     *                                            a supported or does not support
+     *                                            persistence
+     */
+    @NotNull
+    String getRenditionPath(@NotNull String resourceType);
+
+    /**
+     * Gets the error Suffix for configuring the transformation servlet
+     * 
+     * @return the error suffix for the servlet
+     */
+    @NotNull
+    String getServletErrorSuffix();
+
+    /**
+     * Gets the error resource path for configuring the transformation servlet
+     * 
+     * @return the error resource path for the servlet
+     */
+    @NotNull
+    String getServletErrorResourcePath();
+
+    /**
+     * Gets all of the resource types which support generating and transforming
+     * thumbnails. These resource types must be adaptable to a java.io.InputStream
+     * 
+     * @return the set of supported resource types
+     */
+    @NotNull
+    Set<String> getSupportedTypes();
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/Transformation.java b/src/main/java/org/apache/sling/thumbnails/Transformation.java
new file mode 100644
index 0000000..9ae1347
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/Transformation.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.util.List;
+
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Model representing a transformation, a series of handlers
+ */
+@ProviderType
+public interface Transformation {
+
+    List<TransformationHandlerConfig> getHandlers();
+
+    String getName();
+    
+    String getPath();
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/TransformationHandlerConfig.java b/src/main/java/org/apache/sling/thumbnails/TransformationHandlerConfig.java
new file mode 100644
index 0000000..8d5ca04
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/TransformationHandlerConfig.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * A configuration for an instance of a Transformation Handler.
+ */
+@ProviderType
+public interface TransformationHandlerConfig {
+
+    /**
+     * Get the hander type, generally, this will be a Sling Resource type
+     * 
+     * @return the handler type
+     */
+    String getHandlerType();
+
+    /**
+     * The configuration properties for the transformation hander
+     * 
+     * @return the configuration properties
+     */
+    ValueMap getProperties();
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/Transformer.java b/src/main/java/org/apache/sling/thumbnails/Transformer.java
new file mode 100644
index 0000000..6d024fa
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/Transformer.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.Resource;
+import org.osgi.annotation.versioning.ProviderType;
+
+/**
+ * Transforms a resource using the registered TransformationHandlers to invoke
+ * transformations on the file.
+ */
+@ProviderType
+public interface Transformer {
+
+    /**
+     * Transforms the resource
+     * 
+     * @param resource       the resource to transform
+     * @param transformation the transformation to execute
+     * @param format         the format of the stream to return
+     * @param out            the OutputStream to which to write the transformed file
+     * @throws IOException an exception occurs transforming the resource
+     */
+    void transform(Resource resource, Transformation transformation, OutputFileFormat format, OutputStream out)
+            throws IOException;
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/extension/ThumbnailProvider.java b/src/main/java/org/apache/sling/thumbnails/extension/ThumbnailProvider.java
new file mode 100644
index 0000000..a249e36
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/extension/ThumbnailProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.extension;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.sling.api.resource.Resource;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/**
+ * Service for retrieving a thumbnail for the specified Resource.
+ */
+@ConsumerType
+public interface ThumbnailProvider {
+
+    /**
+     * Returns true if the ThumbnailProvider applies for the specified resource.
+     * 
+     * @param resource the resource to check. This resource should be a supported
+     *                 resource type
+     * @param metaType the meta type string found for the file based on it's
+     *                 resource type
+     * @return true if this ThumbnailProvider will create a thumbnail for this
+     *         resource, false otherwise
+     */
+    boolean applies(Resource resource, String metaType);
+
+    /**
+     * Get the thumbnail from the specified resource.
+     * 
+     * @param resource the resource from which to retrieve the thumbnail
+     * @return the thumbnail
+     * @throws IOException an exception occurs retrieving the thumbnail
+     */
+    InputStream getThumbnail(Resource resource) throws IOException;
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/thumbnails/extension/TransformationHandler.java b/src/main/java/org/apache/sling/thumbnails/extension/TransformationHandler.java
new file mode 100644
index 0000000..cbd1c2d
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/extension/TransformationHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.extension;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.annotation.versioning.ConsumerType;
+
+/*
+ * Transformation handlers handle the transformation of files. Each transformation handler 
+ * implements a transformation command using the specifed configuration.
+ */
+@ConsumerType
+public interface TransformationHandler {
+
+    /**
+     * Get the resource type associated with this handler
+     * 
+     * @return the handler resource type
+     */
+    String getResourceType();
+
+    /**
+     * Handles the transformation of the file using the command values from the
+     * suffix segment.
+     * 
+     * @param inputStream  the inputstream from which to read the file to transform
+     *                     from
+     * @param outputStream the outputstream to write the transformed file to
+     * @param config       the configuration values for the transformation
+     * @throws IOException an exception occurs transforming the file
+     */
+    void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException;
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/extension/package-info.java b/src/main/java/org/apache/sling/thumbnails/extension/package-info.java
new file mode 100644
index 0000000..99e022d
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/extension/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Extension points for file thumbnail generation and transformation
+ *
+ * @since 1.0.0
+ */
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.thumbnails.extension;
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/DynamicTransformServlet.java b/src/main/java/org/apache/sling/thumbnails/internal/DynamicTransformServlet.java
new file mode 100644
index 0000000..d629997
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/DynamicTransformServlet.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.util.IOUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.Transformer;
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.internal.models.TransformationImpl;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A servlet to transform images using the Thumbnails API using a post body to
+ * transform the images
+ */
+@Component(immediate = true, service = Servlet.class, property = { "sling.servlet.methods=POST",
+        "sling.servlet.paths=/bin/sling/thumbnails/transform" })
+public class DynamicTransformServlet extends SlingAllMethodsServlet {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicTransformServlet.class);
+
+    private final transient Transformer transformer;
+
+    private final transient RenditionSupport renditionSupport;
+
+    @Activate
+    public DynamicTransformServlet(@Reference Transformer transformer, @Reference RenditionSupport renditionSupport) {
+        this.renditionSupport = renditionSupport;
+        this.transformer = transformer;
+    }
+
+    @Override
+    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+        log.trace("doPost");
+
+        try {
+
+            Resource resource = request.getResourceResolver()
+                    .getResource(Optional.ofNullable(request.getParameter("resource"))
+                            .orElseThrow(() -> new BadRequestException("Parameter resource must be supplied")));
+
+            if (resource == null) {
+                response.sendError(404, "No resource found at: " + request.getParameter("resource"));
+                return;
+            }
+
+            OutputFileFormat format = OutputFileFormat
+                    .forValue(Optional.ofNullable(request.getParameter("format")).orElse("jpeg"));
+            response.setHeader("Content-Disposition", "filename=" + resource.getName());
+            response.setContentType(format.getMimeType());
+
+            Transformation transformation = getTransformation(request);
+
+            log.debug("Transforming resource: {} with transformation: {} to {}", resource, transformation, format);
+            ByteArrayOutputStream baos = transform(resource, response, format.toString(), transformation);
+
+            String renditionName = request.getParameter("renditionName");
+            if (renditionName != null) {
+                if (StringUtils.isBlank(renditionName) && transformation.getName() != null) {
+                    renditionName = transformation.getName() + "." + format.toString().toLowerCase();
+                }
+                log.debug("Setting rendition: {}", renditionName);
+                if (renditionSupport.supportsRenditions(resource)) {
+                    renditionSupport.setRendition(resource, renditionName,
+                            new ByteArrayInputStream(baos.toByteArray()));
+                } else {
+                    throw new BadRequestException(
+                            "Type " + resource.getResourceType() + " does not support persisting renditions");
+                }
+            }
+        } catch (BadRequestException e) {
+            log.error("Could not render thumbnail due to bad request", e);
+            response.sendError(400, "Could not render thumbnail due to bad request: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("Failed to render thumbnail", e);
+            response.sendError(500, "Failed to render thumbnail");
+        }
+    }
+
+    private Transformation getTransformation(SlingHttpServletRequest request) throws IOException {
+        String transformationPath = request.getParameter("transformationResource");
+        if (StringUtils.isNotBlank(request.getParameter("transformationResource"))) {
+            return Optional.ofNullable(request.getResourceResolver().getResource(transformationPath))
+                    .map(r -> r.adaptTo(Transformation.class)).orElseThrow(
+                            () -> new BadRequestException("Requested invalid transformation: " + transformationPath));
+
+        } else {
+            return new TransformationImpl(parsePostBody(request));
+        }
+    }
+
+    private List<TransformationHandlerConfigImpl> parsePostBody(SlingHttpServletRequest request) throws IOException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        try {
+            return objectMapper.readValue(request.getReader(),
+                    new TypeReference<List<TransformationHandlerConfigImpl>>() {
+                    });
+        } catch (JsonProcessingException pe) {
+            throw new BadRequestException("Could not parse transformation request", pe);
+        }
+    }
+
+    private ByteArrayOutputStream transform(Resource resource, SlingHttpServletResponse response, String format,
+            Transformation transformation) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        transformer.transform(resource, transformation, OutputFileFormat.valueOf(format.toUpperCase()), baos);
+        IOUtils.copy(new ByteArrayInputStream(baos.toByteArray()), response.getOutputStream());
+        return baos;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/RenditionSupportImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/RenditionSupportImpl.java
new file mode 100644
index 0000000..d1e306e
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/RenditionSupportImpl.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.StreamSupport;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+@Component(service = RenditionSupport.class)
+public class RenditionSupportImpl implements RenditionSupport {
+
+    private final ThumbnailSupport thumbnailSupport;
+    private final TransformationServiceUser transformationServiceUser;
+
+    @Activate
+    public RenditionSupportImpl(@Reference ThumbnailSupport thumbnailSupport,
+            @Reference TransformationServiceUser transformationServiceUser) {
+        this.thumbnailSupport = thumbnailSupport;
+        this.transformationServiceUser = transformationServiceUser;
+    }
+
+    @Override
+    public @Nullable Resource getRendition(@NotNull Resource file, @NotNull String renditionName) {
+        if (supportsRenditions(file)) {
+            String subpath = thumbnailSupport.getRenditionPath(file.getResourceType());
+            return file.getChild(subpath + "/" + renditionName);
+        }
+        return null;
+    }
+
+    @Override
+    public @Nullable InputStream getRenditionContent(@NotNull Resource file, @NotNull String renditionName) {
+        return Optional.ofNullable(getRendition(file, renditionName)).map(r -> r.adaptTo(InputStream.class))
+                .orElse(null);
+    }
+
+    @Override
+    public @NotNull List<Resource> listRenditions(@NotNull Resource file) {
+        List<Resource> renditions = new ArrayList<>();
+        if (this.supportsRenditions(file)) {
+            Optional.ofNullable(file.getChild(thumbnailSupport.getRenditionPath(file.getResourceType())))
+                    .ifPresent(renditionFolder -> {
+                        StreamSupport.stream(renditionFolder.getChildren().spliterator(), false)
+                                .filter(c -> JcrConstants.NT_FILE.equals(c.getResourceType())).forEach(renditions::add);
+                    });
+        }
+        return renditions;
+    }
+
+    @Override
+    public boolean renditionExists(@NotNull Resource file, @NotNull String renditionName) {
+        return getRendition(file, renditionName) != null;
+    }
+
+    @Override
+    public boolean supportsRenditions(@NotNull Resource file) {
+        return thumbnailSupport.getPersistableTypes().contains(file.getResourceType());
+    }
+
+    @Override
+    public void setRendition(@NotNull Resource file, @NotNull String renditionName, @NotNull InputStream contents)
+            throws PersistenceException {
+        if (renditionName.indexOf("/") != 0) {
+            renditionName = "/" + renditionName;
+        }
+        try (ResourceResolver serviceResolver = transformationServiceUser.getTransformationServiceUser()) {
+
+            Resource renditionFile = ResourceUtil.getOrCreateResource(serviceResolver,
+                    file.getPath() + "/" + thumbnailSupport.getRenditionPath(file.getResourceType()) + renditionName,
+                    Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_FILE),
+                    JcrConstants.NT_UNSTRUCTURED, false);
+            Map<String, Object> properties = new HashMap<>();
+            properties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+            properties.put(JcrConstants.JCR_DATA, contents);
+            ResourceUtil.getOrCreateResource(serviceResolver, renditionFile.getPath() + "/" + JcrConstants.JCR_CONTENT,
+                    properties, JcrConstants.NT_UNSTRUCTURED, true);
+        } catch (LoginException le) {
+            throw new PersistenceException("Could not save due to LoginException", le);
+        }
+
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportConfig.java b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportConfig.java
new file mode 100644
index 0000000..bd447a8
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportConfig.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import org.osgi.service.metatype.annotations.AttributeDefinition;
+import org.osgi.service.metatype.annotations.ObjectClassDefinition;
+
+/**
+ * Configuration for the FileSettingsImpl
+ */
+@ObjectClassDefinition(name = "%thumbnailsupport.name", description = "%thumbnailsupport.description", localization = "OSGI-INF/l10n/bundle")
+public @interface ThumbnailSupportConfig {
+
+    @AttributeDefinition(name = "%thumbnailsupport.supportedTypes.name", description = "%thumbnailsupport.supportedTypes.description", defaultValue = {
+            "nt:file=jcr:content/jcr:mimeType" })
+    String[] supportedTypes() default { "nt:file=jcr:content/jcr:mimeType" };
+
+    @AttributeDefinition(name = "%thumbnailsupport.persistableTypes.name", description = "%thumbnailsupport.persistableTypes.description")
+    String[] persistableTypes() default {};
+
+    @AttributeDefinition(name = "%transformservlet.errorResourcePath.name", description = "%transformservlet.errorResourcePath.description")
+    String errorResourcePath() default "/static/sling-cms/thumbnails/file.png";
+
+    @AttributeDefinition(name = "%transformservlet.errorSuffix.name", description = "%transformservlet.errorSuffix.description")
+    String errorSuffix() default "/sling-cms-thumbnail.png";
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImpl.java
new file mode 100644
index 0000000..cb7b589
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImpl.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.jetbrains.annotations.NotNull;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.metatype.annotations.Designate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = { ThumbnailSupport.class })
+@Designate(ocd = ThumbnailSupportConfig.class)
+public class ThumbnailSupportImpl implements ThumbnailSupport {
+
+    private static final Logger log = LoggerFactory.getLogger(ThumbnailSupportImpl.class);
+
+    private final Map<String, String> persistableTypes = new HashMap<>();
+    private final Map<String, String> supportedTypes = new HashMap<>();
+
+    private final ThumbnailSupportConfig config;
+
+    @Activate
+    public ThumbnailSupportImpl(ThumbnailSupportConfig config) {
+        Arrays.stream(config.supportedTypes()).forEach(nt -> {
+            String[] cfg = nt.split("\\=");
+            if (cfg.length != 2 || StringUtils.isEmpty(cfg[0]) || StringUtils.isEmpty(cfg[1])) {
+                log.warn("Could not parse supported resource type from {}", nt);
+            } else if (supportedTypes.containsKey(cfg[0])) {
+                log.warn("Ignoring duplicate supported resource type: {}", cfg[0]);
+            } else {
+                supportedTypes.put(cfg[0], cfg[1]);
+            }
+        });
+
+        Arrays.stream(config.persistableTypes()).forEach(nt -> {
+            String[] cfg = nt.split("\\=");
+            if (cfg.length != 2 || StringUtils.isEmpty(cfg[0]) || StringUtils.isEmpty(cfg[1])) {
+                log.warn("Could not parse persisted resource type from {}", nt);
+            } else if (!supportedTypes.containsKey(cfg[0])) {
+                log.warn("Ignoring unsupported persistable resource type: {}", cfg[0]);
+            } else if (persistableTypes.containsKey(cfg[0])) {
+                log.warn("Ignoring duplicate persistable resource type: {}", cfg[0]);
+            } else {
+                persistableTypes.put(cfg[0], cfg[1]);
+            }
+        });
+        this.config = config;
+    }
+
+    @Override
+    public @NotNull String getMetaTypePropertyPath(@NotNull String resourceType) {
+        if (!supportedTypes.containsKey(resourceType)) {
+            throw new IllegalArgumentException("Supplied unsupported resource type " + resourceType);
+        } else {
+            return supportedTypes.get(resourceType);
+        }
+    }
+
+    @Override
+    public @NotNull Set<String> getPersistableTypes() {
+        return persistableTypes.keySet();
+    }
+
+    @Override
+    public @NotNull String getRenditionPath(@NotNull String resourceType) {
+        if (!persistableTypes.containsKey(resourceType)) {
+            throw new IllegalArgumentException("Supplied non-persistable resource type " + resourceType);
+        } else {
+            return persistableTypes.get(resourceType);
+        }
+    }
+
+    @Override
+    public @NotNull String getServletErrorSuffix() {
+        return config.errorSuffix();
+    }
+
+    @Override
+    public @NotNull Set<String> getSupportedTypes() {
+        return supportedTypes.keySet();
+    }
+
+    @Override
+    public @NotNull String getServletErrorResourcePath() {
+        return config.errorResourcePath();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsole.java b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsole.java
new file mode 100644
index 0000000..10bfb9b
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsole.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+
+import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import com.google.common.collect.Lists;
+
+import org.apache.felix.webconsole.AbstractWebConsolePlugin;
+import org.apache.felix.webconsole.WebConsoleConstants;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformer;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Simple web console plugin for listing out the registered thumbnail providers
+ * and transformation handler
+ */
+@Component(property = { Constants.SERVICE_DESCRIPTION + "=Web Console Plugin for Apache Sling Thumbnails",
+        Constants.SERVICE_VENDOR + "=The Apache Software Foundation",
+        WebConsoleConstants.PLUGIN_LABEL + "=" + ThumbnailsWebConsole.CONSOLE_LABEL,
+        WebConsoleConstants.PLUGIN_TITLE + "=" + ThumbnailsWebConsole.CONSOLE_TITLE,
+        WebConsoleConstants.CONFIG_PRINTER_MODES + "=always",
+        WebConsoleConstants.PLUGIN_CATEGORY + "=Status" }, service = { Servlet.class })
+public class ThumbnailsWebConsole extends AbstractWebConsolePlugin {
+
+    private static final long serialVersionUID = 4819043498961127418L;
+    public static final String CONSOLE_LABEL = "thumbnails";
+    public static final String CONSOLE_TITLE = "Sling Thumbnails";
+
+    private final Transformer transformer;
+    private final ThumbnailSupport thumbnailSupport;
+
+    @Activate
+    public ThumbnailsWebConsole(@Reference ThumbnailSupport thumbnailSupport, @Reference Transformer transformer) {
+        this.thumbnailSupport = thumbnailSupport;
+        this.transformer = transformer;
+    }
+
+    @Override
+    public String getTitle() {
+        return CONSOLE_TITLE;
+    }
+
+    @Override
+    public String getLabel() {
+        return CONSOLE_LABEL;
+    }
+
+    @Override
+    protected void renderContent(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse)
+            throws IOException {
+        PrintWriter pw = httpServletResponse.getWriter();
+
+        printSeparator(pw, "Supported Resource Types", true);
+        pw.println("[Resource Type] => [MetaType Property Path]");
+        thumbnailSupport.getSupportedTypes()
+                .forEach(st -> pw.println(st + " => " + thumbnailSupport.getMetaTypePropertyPath(st)));
+
+        printSeparator(pw, "Persistable Resource Types", false);
+        pw.println("[Resource Type] => [Rendition Path]");
+        thumbnailSupport.getPersistableTypes()
+                .forEach(pt -> pw.println(pt + " => " + thumbnailSupport.getRenditionPath(pt)));
+
+        printSeparator(pw, "Registered Thumbnail Providers", false);
+        List<ThumbnailProvider> providers = ((TransformerImpl) transformer).getThumbnailProviders();
+        Lists.reverse(providers).forEach(p -> pw.println(p.getClass().getName()));
+
+        printSeparator(pw, "Registered Transformation Providers", false);
+        List<TransformationHandler> handlers = ((TransformerImpl) transformer).getHandlers();
+        handlers.forEach(h -> pw.println(h.getResourceType() + "=" + h.getClass().getCanonicalName()));
+        pw.println("</pre>");
+        pw.println("</div>");
+    }
+
+    private void printSeparator(PrintWriter pw, String title, boolean first) {
+        if (!first) {
+            pw.println("</pre><br/>");
+        }
+        pw.println("<pre>");
+        pw.println(title);
+        pw.println("========================");
+
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/TransformServlet.java b/src/main/java/org/apache/sling/thumbnails/internal/TransformServlet.java
new file mode 100644
index 0000000..17c25e0
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/TransformServlet.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.util.IOUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.request.RequestDispatcherOptions;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.Transformer;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A servlet to transform images using the Thumbnails API. Can be invoked using
+ * the syntax:
+ * 
+ * /content/file/path.jpg.transform/transformation-name.png
+ */
+@Component(immediate = true)
+public class TransformServlet extends SlingAllMethodsServlet {
+
+    private static final Logger log = LoggerFactory.getLogger(TransformServlet.class);
+
+    private static final long serialVersionUID = -1513067546618762171L;
+
+    private final transient RenditionSupport renditionSupport;
+
+    private final transient ServiceRegistration<Servlet> servletRegistration;
+
+    private final transient TransformationServiceUser transformationServiceUser;
+
+    private final transient Transformer transformer;
+
+    private final transient ThumbnailSupport thumbnailSupport;
+
+    private final transient TransformationCache transformationCache;
+
+    @Activate
+    public TransformServlet(@Reference ThumbnailSupport thumbnailSupport, @Reference Transformer transformer,
+            @Reference TransformationServiceUser transformationServiceUser,
+            @Reference TransformationCache transformationCache, @Reference RenditionSupport renditionSupport,
+            BundleContext context) {
+        this.renditionSupport = renditionSupport;
+        this.thumbnailSupport = thumbnailSupport;
+        this.transformer = transformer;
+        this.transformationServiceUser = transformationServiceUser;
+        this.transformationCache = transformationCache;
+
+        log.info("Registering as servlet...");
+        Dictionary<String, Object> properties = new Hashtable<>();
+        properties.put("sling.servlet.methods", new String[] { "GET" });
+        properties.put("sling.servlet.extensions", "transform");
+        properties.put("sling.servlet.resourceTypes", thumbnailSupport.getSupportedTypes());
+
+        this.servletRegistration = context.registerService(Servlet.class, this, properties);
+        log.info("Transform servlet registered...");
+
+    }
+
+    @Deactivate
+    public void deactivate() {
+        if (this.servletRegistration != null) {
+            this.servletRegistration.unregister();
+        }
+    }
+
+    @Override
+    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
+            throws ServletException, IOException {
+        log.trace("doGet");
+
+        String transformationName = StringUtils.substringBeforeLast(request.getRequestPathInfo().getSuffix(), ".");
+        String renditionName = request.getRequestPathInfo().getSuffix();
+        String format = StringUtils.substringAfterLast(request.getRequestPathInfo().getSuffix(), ".");
+        response.setHeader("Content-Disposition", "filename=" + request.getResource().getName());
+
+        log.debug("Transforming resource: {} with transformation: {} to {}", request.getResource(), transformationName,
+                format);
+        try {
+            Resource file = request.getResource();
+            if (renditionSupport.renditionExists(file, renditionName)) {
+                response.setContentType(OutputFileFormat.forRequest(request).getMimeType());
+                IOUtils.copy(renditionSupport.getRenditionContent(file, renditionName), response.getOutputStream());
+            } else {
+                try (ResourceResolver servicResolver = transformationServiceUser.getTransformationServiceUser()) {
+                    performTransformation(request, response, transformationName, renditionName, servicResolver);
+                }
+            }
+        } catch (BadRequestException e) {
+            log.error("Could not render thumbnail due to bad request", e);
+            response.sendError(400, "Could not render thumbnail due to bad request: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("Exception rendering transformed resource", e);
+            response.setStatus(500);
+            RequestDispatcherOptions op = new RequestDispatcherOptions();
+            op.setReplaceSuffix(thumbnailSupport.getServletErrorSuffix());
+            op.setReplaceSelectors("transform");
+            RequestDispatcher disp = request.getRequestDispatcher(thumbnailSupport.getServletErrorResourcePath(), op);
+            disp.forward(request, response);
+        }
+    }
+
+    private void performTransformation(SlingHttpServletRequest request, SlingHttpServletResponse response,
+            String transformationName, String renditionName, ResourceResolver serviceResolver)
+            throws IOException, ExecutionException {
+        Resource file = request.getResource();
+        String originalContentType = response.getContentType();
+        response.setContentType(OutputFileFormat.forRequest(request).getMimeType());
+        Optional<Transformation> transformationOp = transformationCache.getTransformation(serviceResolver,
+                transformationName);
+        if (!transformationOp.isPresent()) {
+            log.error("Unable to find transformation: {}", transformationName);
+            response.setContentType(originalContentType);
+            response.sendError(404, "Unable to find transformation: " + transformationName);
+        } else {
+            Transformation transformation = transformationOp.get();
+            log.debug("Transforming file...");
+            ByteArrayOutputStream baos = transform(request, response, transformation);
+            if (renditionSupport.supportsRenditions(file)) {
+                log.debug("Saving rendition...");
+                renditionSupport.setRendition(file, renditionName, new ByteArrayInputStream(baos.toByteArray()));
+            }
+        }
+    }
+
+    private ByteArrayOutputStream transform(SlingHttpServletRequest request, SlingHttpServletResponse response,
+            Transformation transformation) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        transformer.transform(request.getResource(), transformation, OutputFileFormat.forRequest(request), baos);
+        IOUtils.copy(new ByteArrayInputStream(baos.toByteArray()), response.getOutputStream());
+        return baos;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/TransformationCache.java b/src/main/java/org/apache/sling/thumbnails/internal/TransformationCache.java
new file mode 100644
index 0000000..8f66d1e
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/TransformationCache.java
@@ -0,0 +1,93 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import javax.jcr.query.Query;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.thumbnails.Transformation;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventConstants;
+import org.osgi.service.event.EventHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Component(service = { TransformationCache.class, EventHandler.class }, property = {
+        EventConstants.EVENT_TOPIC + "=org/apache/sling/api/resource/Resource/CHANGED",
+        EventConstants.EVENT_FILTER + "=(&(resourceType=sling/thumbnails/transformation))" })
+public class TransformationCache implements EventHandler {
+
+    private static final Logger log = LoggerFactory.getLogger(TransformationCache.class);
+    private final TransformationServiceUser transformationServiceUser;
+
+    @Activate
+    public TransformationCache(@Reference TransformationServiceUser transformationServiceUser) {
+        this.transformationServiceUser = transformationServiceUser;
+    }
+
+    private final LoadingCache<String, Optional<String>> cache = CacheBuilder.newBuilder()
+            .expireAfterAccess(1, TimeUnit.HOURS).build(new CacheLoader<String, Optional<String>>() {
+                @Override
+                public Optional<String> load(String name) throws LoginException {
+                    try (ResourceResolver resolver = transformationServiceUser.getTransformationServiceUser()) {
+                        return findTransformation(resolver, name);
+                    }
+                }
+
+                private Optional<String> findTransformation(ResourceResolver serviceResolver, String name) {
+                    name = name.substring(1).replace("'", "''");
+                    log.debug("Finding transformations with {}", name);
+                    Iterator<Resource> transformations = serviceResolver.findResources(
+                            "SELECT * FROM [nt:unstructured] WHERE (ISDESCENDANTNODE([/conf]) OR ISDESCENDANTNODE([/libs/conf]) OR ISDESCENDANTNODE([/apps/conf])) AND [sling:resourceType]='sling/thumbnails/transformation' AND [name]='"
+                                    + name + "'",
+                            Query.JCR_SQL2);
+                    if (transformations.hasNext()) {
+                        Resource transformation = transformations.next();
+                        log.debug("Found transformation resource: {}", transformation);
+                        return Optional.of(transformation.getPath());
+                    }
+                    return Optional.empty();
+                }
+            });
+
+    public Optional<Transformation> getTransformation(ResourceResolver resolver, String name)
+            throws ExecutionException {
+        return cache.get(name).map(resolver::getResource).map(r -> r.adaptTo(Transformation.class));
+    }
+
+    @Override
+    public void handleEvent(Event event) {
+        cache.invalidateAll();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/TransformationServiceUser.java b/src/main/java/org/apache/sling/thumbnails/internal/TransformationServiceUser.java
new file mode 100644
index 0000000..d5478b9
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/TransformationServiceUser.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.util.Collections;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Service for retrieving the service user
+ */
+@Component(service = TransformationServiceUser.class)
+public class TransformationServiceUser {
+
+    public static final String SERVICE_USER = "sling-thumbnails";
+
+    private final ResourceResolverFactory resolverFactory;
+
+    @Activate
+    public TransformationServiceUser(@Reference ResourceResolverFactory resolverFactory) {
+        this.resolverFactory = resolverFactory;
+    }
+
+    public ResourceResolver getTransformationServiceUser() throws LoginException {
+        return resolverFactory
+                .getServiceResourceResolver(Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, SERVICE_USER));
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/TransformerImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/TransformerImpl.java
new file mode 100644
index 0000000..2de4740
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/TransformerImpl.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.apache.sling.thumbnails.Transformer;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ReferenceCardinality;
+import org.osgi.service.component.annotations.ReferencePolicyOption;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+@Component(service = Transformer.class)
+public class TransformerImpl implements Transformer {
+
+    private static final Logger log = LoggerFactory.getLogger(TransformerImpl.class);
+
+    private final List<TransformationHandler> handlers;
+
+    private final List<ThumbnailProvider> thumbnailProviders;
+
+    private final ThumbnailSupport thumbnailSupport;
+
+
+    @Activate
+    public TransformerImpl(
+            @Reference(service = ThumbnailProvider.class, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.AT_LEAST_ONE) List<ThumbnailProvider> thumbnailProviders,
+            @Reference ThumbnailSupport thumbnailSupport,
+            @Reference(service = TransformationHandler.class, policyOption = ReferencePolicyOption.GREEDY, cardinality = ReferenceCardinality.AT_LEAST_ONE) List<TransformationHandler> handlers) {
+        this.thumbnailProviders = thumbnailProviders;
+        this.thumbnailSupport = thumbnailSupport;
+        this.handlers = handlers;
+    }
+
+    public List<TransformationHandler> getHandlers() {
+        return handlers;
+    }
+
+    private String getMetaType(Resource resource) {
+        return resource.getValueMap().get(thumbnailSupport.getMetaTypePropertyPath(resource.getResourceType()),
+                String.class);
+    }
+
+    private ThumbnailProvider getThumbnailProvider(Resource resource) throws IOException {
+        String metaType = getMetaType(resource);
+        log.debug("Finding thumbnail provider for resource {} with meta type {} from available providers {}", resource,
+                metaType, thumbnailProviders);
+        return thumbnailProviders.stream().filter(tp -> {
+            log.debug("Checking provider: {}", tp);
+            return tp.applies(resource, metaType);
+        }).findFirst()
+                .orElseThrow(() -> new IOException("Unable to find thumbnail provider for: " + resource.getPath()));
+    }
+
+    /**
+     * @return the thumbnailProviders
+     */
+    public List<ThumbnailProvider> getThumbnailProviders() {
+        return thumbnailProviders;
+    }
+
+    public TransformationHandler getTransformationHandler(String resourceType) {
+        return handlers.stream().filter(h -> resourceType.equals(h.getResourceType())).findFirst().orElse(null);
+    }
+
+    @Override
+    public void transform(Resource resource, Transformation transformation, OutputFileFormat format, OutputStream out)
+            throws IOException {
+        if (!thumbnailSupport.getSupportedTypes().contains(resource.getResourceType())) {
+            throw new BadRequestException("Unsupported resource type: " + resource.getResourceType());
+        }
+        ThumbnailProvider provider = getThumbnailProvider(resource);
+        log.debug("Using thumbnail provider {} for resource {}", provider, resource);
+        try (InputStream thumbnailIs = provider.getThumbnail(resource)) {
+
+            InputStream inputStream = thumbnailIs;
+            for (TransformationHandlerConfig config : transformation.getHandlers()) {
+                log.debug("Handling command: {}", config);
+
+                TransformationHandler handler = getTransformationHandler(config.getHandlerType());
+                if (handler != null) {
+                    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+                    log.debug("Invoking handler {} for command {}", handler.getClass().getCanonicalName(),
+                            config.getHandlerType());
+                    handler.handle(inputStream, outputStream, config);
+                    inputStream = new ByteArrayInputStream(outputStream.toByteArray());
+                } else {
+                    log.info("No handler found for: {}", config.getHandlerType());
+                }
+            }
+            if (!getMetaType(resource).equals(format.getMimeType())) {
+                log.debug("Converting to {}", format);
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+                builder.outputFormat(format.toString());
+                builder.scale(1.0);
+                builder.toOutputStream(baos);
+                inputStream = new ByteArrayInputStream(baos.toByteArray());
+            }
+
+            IOUtils.copy(inputStream, out);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImpl.java
new file mode 100644
index 0000000..71308bd
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImpl.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.models;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.inject.Inject;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.OSGiService;
+import org.apache.sling.models.annotations.injectorspecific.Self;
+import org.apache.sling.thumbnails.RenderedResource;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.jetbrains.annotations.NotNull;
+
+@Model(adaptables = { SlingHttpServletRequest.class }, adapters = RenderedResource.class)
+public class RenderedResourceImpl implements RenderedResource {
+
+    private final List<Transformation> availableTransformations;
+    private final List<Resource> renditions;
+    private final String renditionsPath;
+    private final List<String> supportedRenditions;
+
+    @Inject
+    public RenderedResourceImpl(@Self SlingHttpServletRequest request,
+            @OSGiService ConfigurationResourceResolver configResourceResolver,
+            @OSGiService RenditionSupport renditionSupport, @OSGiService ThumbnailSupport thumbnailSupport) {
+        Resource resource = Optional.ofNullable(request.getResourceResolver().getResource(request.getParameter("src")))
+                .orElse(request.getRequestPathInfo().getSuffixResource());
+
+        Resource contextResource = request.getRequestPathInfo().getSuffixResource();
+
+        if (thumbnailSupport.getPersistableTypes().contains(resource.getResourceType())) {
+            this.renditions = renditionSupport.listRenditions(resource);
+            this.renditionsPath = thumbnailSupport.getRenditionPath(resource.getResourceType());
+        } else {
+            this.renditions = Collections.emptyList();
+            this.renditionsPath = null;
+        }
+
+        Collection<Resource> transformationResources = configResourceResolver.getResourceCollection(contextResource,
+                "files", "transformations");
+        availableTransformations = transformationResources.stream().map(r -> r.adaptTo(Transformation.class))
+                .collect(Collectors.toList());
+        supportedRenditions = availableTransformations.stream().map(Transformation::getName)
+                .collect(Collectors.toList());
+        renditions.stream().filter(r -> !supportedRenditions.contains(StringUtils.substringBefore(r.getName(), ".")))
+                .map(Resource::getName).forEach(supportedRenditions::add);
+    }
+
+    @Override
+    public @NotNull List<Transformation> getAvailableTransformations() {
+        return this.availableTransformations;
+    }
+
+    @Override
+    public List<Resource> getRenditions() {
+        return this.renditions;
+    }
+
+    @Override
+    public String getRenditionsPath() {
+        return this.renditionsPath;
+    }
+
+    @Override
+    public List<String> getSupportedRenditions() {
+        return supportedRenditions;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationHandlerConfigImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationHandlerConfigImpl.java
new file mode 100644
index 0000000..6050a73
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationHandlerConfigImpl.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.models;
+
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.Self;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+
+@Model(adaptables = Resource.class, adapters = TransformationHandlerConfig.class)
+public class TransformationHandlerConfigImpl implements TransformationHandlerConfig {
+
+    private final String handlerType;
+    private final ValueMap properties;
+
+    @Inject
+    public TransformationHandlerConfigImpl(@Self Resource resource) {
+        this.handlerType = resource.getResourceType();
+        this.properties = resource.getValueMap();
+    }
+
+    @JsonCreator
+    public TransformationHandlerConfigImpl(@JsonProperty("handlerType") String handerType,
+            @JsonProperty("properties") Map<String, Object> properties) {
+        this.handlerType = handerType;
+        this.properties = new ValueMapDecorator(properties);
+    }
+
+    @Override
+    public String getHandlerType() {
+        return handlerType;
+    }
+
+    @Override
+    public ValueMap getProperties() {
+        return this.properties;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationImpl.java b/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationImpl.java
new file mode 100644
index 0000000..e437d81
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/models/TransformationImpl.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.models;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.models.annotations.Model;
+import org.apache.sling.models.annotations.injectorspecific.ChildResource;
+import org.apache.sling.models.annotations.injectorspecific.Self;
+import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+
+@Model(adaptables = Resource.class, adapters = Transformation.class)
+public class TransformationImpl implements Transformation {
+
+    private final List<?> handlers;
+    private final String name;
+    private final String path;
+
+    @JsonCreator
+    public TransformationImpl(@JsonProperty("handlers") List<?> handlers) {
+        this.handlers = (List<?>) handlers;
+        this.name = null;
+        this.path = null;
+    }
+
+    @Inject
+    public TransformationImpl(@ChildResource @Named("handlers") List<TransformationHandlerConfig> handlers,
+            @ValueMapValue @Named("name") String name, @Self Resource resource) {
+        this.handlers = handlers;
+        this.name = name;
+        this.path = resource.getPath();
+    }
+
+    @Override
+    public List<TransformationHandlerConfig> getHandlers() {
+        return (List<TransformationHandlerConfig>) handlers;
+    }
+
+    @Override
+    public String getName() {
+        return this.name;
+    }
+
+    @Override
+    public String getPath() {
+        return this.path;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProvider.java b/src/main/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProvider.java
new file mode 100644
index 0000000..f69a7d1
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProvider.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import java.io.InputStream;
+
+import com.google.common.net.MediaType;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * A thumbnail provider for image files.
+ */
+@Component(service = ThumbnailProvider.class, immediate = true)
+public class ImageThumbnailProvider implements ThumbnailProvider {
+
+    @Override
+    public boolean applies(Resource resource, String metaType) {
+        return MediaType.parse(metaType).is(MediaType.ANY_IMAGE_TYPE)
+                && !MediaType.SVG_UTF_8.is(MediaType.parse(metaType));
+    }
+
+    @Override
+    public InputStream getThumbnail(Resource resource) {
+        return resource.adaptTo(InputStream.class);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProvider.java b/src/main/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProvider.java
new file mode 100644
index 0000000..61a12c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProvider.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.ImageIO;
+
+import com.google.common.net.MediaType;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.rendering.ImageType;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * A thumbnail provider for PDF documents.
+ */
+@Component(service = ThumbnailProvider.class, immediate = true)
+public class PdfThumbnailProvider implements ThumbnailProvider {
+
+    @Override
+    public boolean applies(Resource resource, String metaType) {
+        return MediaType.PDF.is(MediaType.parse(metaType));
+    }
+
+    @Override
+    public InputStream getThumbnail(Resource resource) throws IOException {
+        try (PDDocument document = PDDocument.load(resource.adaptTo(InputStream.class))) {
+            PDFRenderer pdfRenderer = new PDFRenderer(document);
+            BufferedImage bim = pdfRenderer.renderImageWithDPI(0, 300, ImageType.RGB);
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            ImageIO.write(bim, "jpeg", os);
+            return new ByteArrayInputStream(os.toByteArray());
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProvider.java b/src/main/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProvider.java
new file mode 100644
index 0000000..e7e3577
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+
+import javax.imageio.ImageIO;
+
+import com.google.common.net.MediaType;
+
+import org.apache.poi.hslf.usermodel.HSLFSlideShow;
+import org.apache.poi.sl.usermodel.Slide;
+import org.apache.poi.sl.usermodel.SlideShow;
+import org.apache.poi.xslf.usermodel.XMLSlideShow;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Provides Thumbnails for Microsoft PPT and PPTX files.
+ */
+@Component(service = ThumbnailProvider.class, immediate = true)
+public class SlideShowThumbnailProvider implements ThumbnailProvider {
+
+    private final DynamicClassLoaderManager classLoaderManager;
+    private final ThumbnailSupport thumbnailSupport;
+
+    @Activate
+    public SlideShowThumbnailProvider(@Reference DynamicClassLoaderManager classLoaderManager,
+            @Reference ThumbnailSupport thumbnailSupport) {
+        this.classLoaderManager = classLoaderManager;
+        this.thumbnailSupport = thumbnailSupport;
+    }
+
+    @Override
+    public boolean applies(Resource resource, String metaType) {
+        MediaType mt = MediaType.parse(metaType);
+        return mt.is(MediaType.MICROSOFT_POWERPOINT) || mt.is(MediaType.OOXML_PRESENTATION);
+    }
+
+    @Override
+    public InputStream getThumbnail(Resource resource) throws IOException {
+        if (classLoaderManager != null) {
+            Thread.currentThread().setContextClassLoader(classLoaderManager.getDynamicClassLoader());
+        }
+
+        SlideShow<?, ?> ppt = null;
+        MediaType mt = MediaType.parse(resource.getValueMap()
+                .get(thumbnailSupport.getMetaTypePropertyPath(resource.getResourceType()), String.class));
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                InputStream is = resource.adaptTo(InputStream.class)) {
+            if (mt.is(MediaType.MICROSOFT_POWERPOINT)) {
+                ppt = new HSLFSlideShow(is);
+            } else {
+                ppt = new XMLSlideShow(is);
+            }
+            Dimension dim = ppt.getPageSize();
+            List<? extends Slide<?, ?>> slides = ppt.getSlides();
+
+            BufferedImage img = new BufferedImage(dim.width, dim.height, BufferedImage.TYPE_INT_RGB);
+            Graphics2D graphics = img.createGraphics();
+            graphics.setPaint(Color.white);
+            graphics.fill(new Rectangle2D.Float(0, 0, dim.width, dim.height));
+
+            if (slides != null && !slides.isEmpty()) {
+                slides.get(0).draw(graphics);
+            }
+
+            ImageIO.write(img, OutputFileFormat.PNG.toString(), baos);
+            return new ByteArrayInputStream(baos.toByteArray());
+        } finally {
+            if (ppt != null) {
+                ppt.close();
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProvider.java b/src/main/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProvider.java
new file mode 100644
index 0000000..bf6c567
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProvider.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import java.awt.Graphics;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.imageio.ImageIO;
+import javax.swing.JEditorPane;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.parser.AutoDetectParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+import org.apache.tika.sax.BodyContentHandler;
+import org.apache.tika.sax.WriteOutContentHandler;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Component;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.SAXException;
+
+@Component(service = ThumbnailProvider.class, property = {
+        Constants.SERVICE_RANKING + "=" + Integer.MIN_VALUE }, immediate = true)
+public class TikaFallbackProvider implements ThumbnailProvider {
+
+    private static final Logger log = LoggerFactory.getLogger(TikaFallbackProvider.class);
+
+    @Override
+    public boolean applies(Resource resource, String metaType) {
+        return true;
+    }
+
+    @Override
+    public InputStream getThumbnail(Resource resource) throws IOException {
+
+        log.info("Extracting content thumbnail from {}", resource.getPath());
+        try {
+
+            log.debug("Extracting file contents");
+            String contents = extractContents(resource);
+
+            log.debug("Creating thumbnail of file contents");
+            int width = 500;
+            int height = 500;
+            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+            Graphics graphics = image.createGraphics();
+            JEditorPane jep = new JEditorPane("text/html", contents);
+            jep.setSize(width, height);
+            jep.print(graphics);
+
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ImageIO.write(image, OutputFileFormat.PNG.toString(), baos);
+            return new ByteArrayInputStream(baos.toByteArray());
+        } catch (SAXException | TikaException e) {
+            throw new IOException("Failed to generate thumbnail from " + resource.getPath(), e);
+        }
+    }
+
+    private String extractContents(Resource resource) throws IOException, TikaException, SAXException {
+        InputStream is = resource.adaptTo(InputStream.class);
+        Parser parser = new AutoDetectParser();
+        WriteOutContentHandler woHandler = new WriteOutContentHandler();
+        BodyContentHandler bHandler = new BodyContentHandler(woHandler);
+
+        Metadata md = new Metadata();
+        ParseContext context = new ParseContext();
+        try {
+            parser.parse(is, bHandler, md, context);
+        } catch (SAXException se) {
+            if (woHandler.isWriteLimitReached(se)) {
+                log.debug("Reached write limit for preview generation");
+            } else {
+                throw se;
+            }
+        }
+        return bHandler.toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandler.java
new file mode 100644
index 0000000..6058320
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandler.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+import net.coobird.thumbnailator.filters.Colorize;
+
+/**
+ * A transformer for resizing an image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class ColorizeHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/colorize";
+    public static final String PN_RED = "red";
+    public static final String PN_GREEN = "green";
+    public static final String PN_BLUE = "blue";
+    public static final String PN_ALPHA = "alpha";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        ValueMap properties = config.getProperties();
+        int red = getColor(properties, PN_RED);
+        int green = getColor(properties, PN_GREEN);
+        int blue = getColor(properties, PN_BLUE);
+        float alpha = (float) config.getProperties().get(PN_ALPHA, 0.0).doubleValue();
+
+        if (alpha < 0 || alpha > 1.0) {
+            throw new BadRequestException("Unable to colorize, bad alpha value " + alpha);
+        }
+
+        builder.addFilter(new Colorize(new Color(red, green, blue), alpha));
+        builder.scale(1.0);
+        builder.toOutputStream(outputStream);
+    }
+
+    protected int getColor(ValueMap properties, String name) {
+        int color = properties.get(name, 0);
+        if (color < 0 || color > 255) {
+            throw new BadRequestException("Unable to colorize, bad " + name + " value " + color);
+        }
+        return color;
+    }
+
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/CropHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/CropHandler.java
new file mode 100644
index 0000000..44cbbd4
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/CropHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+import net.coobird.thumbnailator.geometry.Positions;
+
+/**
+ * A transformation handler to crop images
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class CropHandler implements TransformationHandler {
+
+    public static final String PN_POSITION = "position";
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/crop";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        ValueMap properties = config.getProperties();
+        resize(builder, properties);
+        String positionStr = properties.get(PN_POSITION, "CENTER").toUpperCase();
+        try {
+            Positions pos = Positions.valueOf(positionStr);
+            builder.crop(pos);
+            builder.toOutputStream(outputStream);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Unable to crop due to invalid configuration: \n%s", config.getProperties(),
+                    e);
+        }
+    }
+
+    private static void resize(Builder<? extends InputStream> builder, ValueMap properties) {
+        int width = properties.get(ResizeHandler.PN_WIDTH, -1);
+        int height = properties.get(ResizeHandler.PN_HEIGHT, -1);
+        if (width >= 0 && height >= 0) {
+            builder.size(width, height);
+        } else {
+            throw new BadRequestException("Unable to resize thumbnail due to invalid parameters: \n%s", properties);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/FlipHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/FlipHandler.java
new file mode 100644
index 0000000..09c51bb
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/FlipHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+import net.coobird.thumbnailator.filters.Flip;
+import net.coobird.thumbnailator.filters.ImageFilter;
+
+/**
+ * Fips the image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class FlipHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/flip";
+
+    public static final String PN_DIRECTION = "direction";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+
+        String direction = config.getProperties().get(PN_DIRECTION, "").toUpperCase();
+
+        ImageFilter flipper = null;
+        if ("HORIZONTAL".equals(direction)) {
+            flipper = Flip.HORIZONTAL;
+        } else if ("VERTICAL".equals(direction)) {
+            flipper = Flip.VERTICAL;
+        } else {
+            throw new BadRequestException("Could not flip image with configuration: \n%s", config.getProperties());
+        }
+
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        builder.addFilter(flipper);
+        builder.scale(1.0);
+        builder.toOutputStream(outputStream);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandler.java
new file mode 100644
index 0000000..fa5f5e3
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandler.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+/**
+ * A transformer for rotating an image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class GreyscaleHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/greyscale";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        builder.imageType(BufferedImage.TYPE_BYTE_GRAY);
+        builder.scale(1.0);
+        builder.toOutputStream(outputStream);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandler.java
new file mode 100644
index 0000000..84963aa
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+/**
+ * A transformer for resizing an image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class ResizeHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/resize";
+    public static final String PN_HEIGHT = "height";
+    public static final String PN_WIDTH = "width";
+    public static final String PN_KEEP_ASPECT_RATIO = "keepAspectRatio";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+
+        try {
+            resize(builder, config.getProperties());
+
+            boolean keepAspectRatio = config.getProperties().get(PN_KEEP_ASPECT_RATIO, true);
+            builder.keepAspectRatio(keepAspectRatio);
+
+            builder.toOutputStream(outputStream);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Unable to resize due to invalid configuration: \n%s", config.getProperties(),
+                    e);
+        }
+    }
+
+    private static void resize(Builder<? extends InputStream> builder, ValueMap properties) {
+        int width = properties.get(PN_WIDTH, -1);
+        int height = properties.get(PN_HEIGHT, -1);
+        if (width >= 0 && height >= 0) {
+            builder.size(width, height);
+        } else if (width >= 0) {
+            builder.width(width);
+        } else if (height >= 0) {
+            builder.height(height);
+        } else {
+            throw new BadRequestException("Unable to resize thumbnail due to invalid parameters: \n%s", properties);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/RotateHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/RotateHandler.java
new file mode 100644
index 0000000..4827a0d
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/RotateHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+/**
+ * A transformer for rotating an image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class RotateHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/rotate";
+    public static final String DEGREES = "degrees";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        double degrees = config.getProperties().get(DEGREES, 0.0);
+        builder.rotate(degrees);
+        builder.scale(1.0);
+        builder.toOutputStream(outputStream);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandler.java
new file mode 100644
index 0000000..14423f9
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+/**
+ * A transformer for resizing an image
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class ScaleHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/scale";
+    public static final String PN_BOTH = "both";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        ValueMap properties = config.getProperties();
+        double both = properties.get(PN_BOTH, -1.0);
+        double width = properties.get(ResizeHandler.PN_WIDTH, -1.0);
+        double height = properties.get(ResizeHandler.PN_HEIGHT, -1.0);
+        try {
+            Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+            if (both >= 0) {
+                builder.scale(both);
+            } else if (width >= 0 && height >= 0) {
+                builder.scale(width, height);
+            } else {
+                throw new BadRequestException("Could not scale thumbnail, invalid parameters: \n%s", properties);
+            }
+
+            builder.toOutputStream(outputStream);
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException("Unable to resize due to invalid configuration: \n%s", config.getProperties(),
+                    e);
+        }
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandler.java b/src/main/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandler.java
new file mode 100644
index 0000000..84258df
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandler.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+import net.coobird.thumbnailator.filters.Transparency;
+
+/**
+ * A transformer for making an image transparent
+ */
+@Component(service = TransformationHandler.class, immediate = true)
+public class TransparencyHandler implements TransformationHandler {
+
+    public static final String RESOURCE_TYPE = "sling/thumbnails/transformers/transparency";
+
+    @Override
+    public String getResourceType() {
+        return RESOURCE_TYPE;
+    }
+
+    @Override
+    public void handle(InputStream inputStream, OutputStream outputStream, TransformationHandlerConfig config)
+            throws IOException {
+        Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
+        ValueMap properties = config.getProperties();
+
+        double alpha = properties.get(ColorizeHandler.PN_ALPHA, 0.0);
+
+        if (alpha < 0 || alpha > 1.0) {
+            throw new BadRequestException("Unable to make transparent, bad alpha value " + alpha);
+        }
+
+        builder.addFilter(new Transparency(alpha));
+        builder.scale(1.0);
+
+        builder.toOutputStream(outputStream);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/thumbnails/package-info.java b/src/main/java/org/apache/sling/thumbnails/package-info.java
new file mode 100644
index 0000000..60180b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/thumbnails/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Support for file thumbnail generation and transformation
+ *
+ * @since 1.0.0
+ */
+@org.osgi.annotation.versioning.Version("1.0.0")
+package org.apache.sling.thumbnails;
diff --git a/src/main/resources/OSGI-INF/l10n/bundle.properties b/src/main/resources/OSGI-INF/l10n/bundle.properties
new file mode 100644
index 0000000..ab3c9f5
--- /dev/null
+++ b/src/main/resources/OSGI-INF/l10n/bundle.properties
@@ -0,0 +1,43 @@
+#
+#  Licensed to the Apache Software Foundation (ASF) under one
+#  or more contributor license agreements.  See the NOTICE file
+#  distributed with this work for additional information
+#  regarding copyright ownership.  The ASF licenses this file
+#  to you under the Apache License, Version 2.0 (the
+#  "License"); you may not use this file except in compliance
+#  with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+#  Unless required by applicable law or agreed to in writing,
+#  software distributed under the License is distributed on an
+#  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+#  KIND, either express or implied.  See the License for the
+#  specific language governing permissions and limitations
+#  under the License.
+#
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the Sling SCR plugin
+
+## Transformation Servlet Entries
+thumbnailsupport.name=Apache Sling Thumbnails Support
+thumbnailsupport.description=Service for generating and transforming thumbnails from files
+
+thumbnailsupport.supportedTypes.name=Supported Resource Types
+thumbnailsupport.supportedTypes.description= The resource types supported in generating \
+thumbnails in the format: <RESOURCE_TYPE>\=<SUB_PATH_TO_MIME_TYPE>, e.g.: nt:file\=jcr:content/jcr:mimeType
+
+thumbnailsupport.persistableTypes.name=Persistable Resource Types
+thumbnailsupport.persistableTypes.description= The resource types which support persisting renditions when generating \
+thumbnails in the format: <RESOURCE_TYPE>\=<SUB_PATH_TO_RENDITIONS_CONTAINER>, e.g.: sling:File\=jcr:content/renditions
+
+transformservlet.errorResourcePath.name=Error Resource Path
+transformservlet.errorResourcePath.name.description=The path of the resource to forward \
+if an error occurs transforming the provided resource
+
+transformservlet.errorSuffix.name=Error Suffix
+transformservlet.errorSuffix.name.description=The suffix to forward to \
+if an error occurs transforming the provided resource
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/ContextHelper.java b/src/test/java/org/apache/sling/thumbnails/internal/ContextHelper.java
new file mode 100644
index 0000000..8db19bd
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/ContextHelper.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import java.io.InputStream;
+import java.util.function.Function;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+
+public class ContextHelper {
+
+    public static final void initContext(SlingContext context) {
+
+        context.load().json("/content.json", "/content");
+        context.load().json("/conf.json", "/conf");
+        context.load().binaryResource("/apache.png", "/content/apache/sling-apache-org/index/apache.png/jcr:content");
+        context.load().binaryResource("/sling.pdf", "/content/apache/sling-apache-org/index/sling.pdf/jcr:content");
+        context.load().binaryResource("/Sling.docx", "/content/apache/sling-apache-org/index/Sling.docx/jcr:content");
+        context.load().binaryResource("/Sling.pptx", "/content/apache/sling-apache-org/index/Sling.pptx/jcr:content");
+        context.load().binaryResource("/Sling.ppt", "/content/apache/sling-apache-org/index/Sling.ppt/jcr:content");
+        context.load().binaryResource("/editor.min.css",
+                "/content/apache/sling-apache-org/index/editor.min.css/jcr:content");
+
+        context.registerAdapter(Resource.class, InputStream.class, new Function<Resource, InputStream>() {
+            public InputStream apply(Resource input) {
+                return input.getValueMap().get("jcr:content/jcr:data", InputStream.class);
+            }
+        });
+
+    }
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/DynamicTransformServletTest.java b/src/test/java/org/apache/sling/thumbnails/internal/DynamicTransformServletTest.java
new file mode 100644
index 0000000..237992a
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/DynamicTransformServletTest.java
@@ -0,0 +1,280 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.ServletException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.internal.models.TransformationImpl;
+import org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider;
+import org.apache.sling.thumbnails.internal.providers.PdfThumbnailProvider;
+import org.apache.sling.thumbnails.internal.transformers.CropHandler;
+import org.apache.sling.thumbnails.internal.transformers.ResizeHandler;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class DynamicTransformServletTest {
+
+    private DynamicTransformServlet dts;
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+
+        ContextHelper.initContext(context);
+
+        List<TransformationHandler> th = new ArrayList<>();
+        th.add(new CropHandler());
+        th.add(new ResizeHandler());
+
+        List<ThumbnailProvider> providers = new ArrayList<>();
+        providers.add(new ImageThumbnailProvider());
+        providers.add(new PdfThumbnailProvider());
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.singleton("sling:File"));
+        when(thumbnailSupport.getRenditionPath("sling:File")).thenReturn("jcr:content/renditions");
+        Set<String> supportedTypes = new HashSet<>();
+        supportedTypes.add("nt:file");
+        supportedTypes.add("sling:File");
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(supportedTypes);
+        when(thumbnailSupport.getMetaTypePropertyPath(anyString())).thenReturn("jcr:content/jcr:mimeType");
+
+        TransformationServiceUser tsu = mock(TransformationServiceUser.class);
+        when(tsu.getTransformationServiceUser()).thenReturn(context.resourceResolver());
+
+        RenditionSupportImpl renditionSupport = new RenditionSupportImpl(thumbnailSupport, tsu);
+        TransformerImpl transformer = new TransformerImpl(providers, thumbnailSupport, th);
+        dts = new DynamicTransformServlet(transformer, renditionSupport);
+
+    }
+
+    @Test
+    public void testRequest() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+        assertEquals("image/png", context.response().getContentType());
+
+        assertNotEquals(0, context.response().getOutput().length);
+    }
+
+    @Test
+    public void testInvalidPersist() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().addRequestParameter("renditionName", "/my-rendition.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testPersist() throws IOException, ServletException {
+
+        context.create().resource("/content/slingfile.jpg",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:File"));
+        Map<String, Object> slingFileProperties = new HashMap<>();
+        slingFileProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+        slingFileProperties.put(JcrConstants.JCR_DATA, context.resourceResolver()
+                .getResource("/content/apache/sling-apache-org/index/apache.png").adaptTo(InputStream.class));
+        slingFileProperties.put("jcr:mimeType", "image/jpeg");
+        context.create().resource("/content/slingfile.jpg/jcr:content", slingFileProperties);
+
+        context.request().addRequestParameter("resource", "/content/slingfile.jpg");
+        context.request().addRequestParameter("renditionName", "my-rendition.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+
+        assertNotNull(context.resourceResolver()
+                .getResource("/content/slingfile.jpg/jcr:content/renditions/my-rendition.png"));
+    }
+
+    @Test
+    public void testNoResource() throws IOException, ServletException {
+
+        context.request().addRequestParameter("format", "png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testMissingResource() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource",
+                "/content/apache/sling-apache-org/index/wow-look-at-this-file.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(404, context.response().getStatus());
+    }
+
+    @Test
+    public void testRequestWithResource() throws IOException, ServletException {
+
+        context.create().resource("/home/users/test/transformation");
+
+        List<TransformationHandlerConfig> handlers = new ArrayList<>();
+
+        Map<String, Object> size = new HashMap<>();
+        size.put(ResizeHandler.PN_WIDTH, 200);
+        size.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(ResizeHandler.RESOURCE_TYPE, size));
+
+        Map<String, Object> crop = new HashMap<>();
+        crop.put(CropHandler.PN_POSITION, "center");
+        crop.put(ResizeHandler.PN_WIDTH, 200);
+        crop.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(CropHandler.RESOURCE_TYPE, crop));
+
+        Transformation transformation = new TransformationImpl(handlers);
+
+        context.registerAdapter(Resource.class, Transformation.class, transformation);
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().addRequestParameter("transformationResource", "/home/users/test/transformation");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+        assertEquals("image/png", context.response().getContentType());
+
+        assertNotEquals(0, context.response().getOutput().length);
+    }
+
+    @Test
+    public void testRequestWithInvalidResource() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().addRequestParameter("transformationResource", "/home/users/test/transformation");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testRequestWithFailedAdaption() throws IOException, ServletException {
+
+        context.create().resource("/home/users/test/transformation");
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().addRequestParameter("format", "png");
+        context.request().addRequestParameter("transformationResource", "/home/users/test/transformation");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testNoFormat() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().setContent(
+                "[{\"handlerType\":\"sling/thumbnails/transformers/crop\",\"properties\":{\"position\":\"CENTER\",\"width\":1000,\"height\":1000}}]"
+                        .getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+        assertEquals("image/jpeg", context.response().getContentType());
+
+        assertNotEquals(0, context.response().getOutput().length);
+    }
+
+    @Test
+    public void testNoHandlers() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().setContent("[]".getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+        assertEquals("image/jpeg", context.response().getContentType());
+
+        assertNotEquals(0, context.response().getOutput().length);
+    }
+
+    @Test
+    public void testInvalidJson() throws IOException, ServletException {
+
+        context.request().addRequestParameter("resource", "/content/apache/sling-apache-org/index/apache.png");
+        context.request().setContent("{}".getBytes());
+        dts.doPost(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/RenditionSupportImplTest.java b/src/test/java/org/apache/sling/thumbnails/internal/RenditionSupportImplTest.java
new file mode 100644
index 0000000..6083713
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/RenditionSupportImplTest.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayInputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class RenditionSupportImplTest {
+
+    private RenditionSupport renditionSupport;
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private @NotNull Resource slingFolderResource;
+
+    private @NotNull Resource ntFileresource;
+
+    private @NotNull Resource slingFileResource;
+
+    private TransformationServiceUser tsu;
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+
+        ContextHelper.initContext(context);
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.singleton("sling:File"));
+        when(thumbnailSupport.getRenditionPath("sling:File")).thenReturn("jcr:content/renditions");
+
+        Set<String> supportedTypes = new HashSet<>();
+        supportedTypes.add("sling:File");
+        supportedTypes.add("nt:file");
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(supportedTypes);
+        when(thumbnailSupport.getMetaTypePropertyPath(anyString())).thenReturn("jcr:content/jcr:mimeType");
+
+        tsu = mock(TransformationServiceUser.class);
+        when(tsu.getTransformationServiceUser()).thenReturn(context.resourceResolver());
+
+        renditionSupport = new RenditionSupportImpl(thumbnailSupport, tsu);
+
+        slingFolderResource = context.resourceResolver().getResource("/content");
+        Map<String, Object> ntFileProperties = new HashMap<>();
+        ntFileProperties.put("jcr:primaryType", JcrConstants.NT_FILE);
+        ntFileProperties.put("jcr:content/jcr:primaryType", JcrConstants.NT_RESOURCE);
+        ntFileProperties.put("jcr:content/jcr:data", new byte[] { 1, 0 });
+        ntFileProperties.put("jcr:content/jcr:mimeType", "image/jpeg");
+        ntFileresource = context.create().resource("/content/ntfile.jpg", ntFileProperties);
+
+        slingFileResource = context.create().resource("/content/slingfile.jpg",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:File"));
+        Map<String, Object> slingFileProperties = new HashMap<>();
+        slingFileProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+        slingFileProperties.put(JcrConstants.JCR_DATA, new byte[] { 1, 0 });
+        slingFileProperties.put("jcr:mimeType", "image/jpeg");
+        context.create().resource("/content/slingfile.jpg/jcr:content", slingFileProperties);
+
+    }
+
+    @Test
+    public void testSupportsRenditions() {
+        assertFalse(renditionSupport.supportsRenditions(ntFileresource));
+        assertFalse(renditionSupport.supportsRenditions(slingFolderResource));
+        assertTrue(renditionSupport.supportsRenditions(slingFileResource));
+    }
+
+    @Test
+    public void testRenditionExists() {
+        assertFalse(renditionSupport.renditionExists(ntFileresource, "myrendition.png"));
+
+        ntFileresource = context.create().resource("/content/ntfile.jpg/jcr:content/renditions",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:Folder"));
+
+        Map<String, Object> ntFileProperties = new HashMap<>();
+        ntFileProperties.put("jcr:primaryType", JcrConstants.NT_FILE);
+        ntFileProperties.put("jcr:content/jcr:primaryType", JcrConstants.NT_RESOURCE);
+        ntFileProperties.put("jcr:content/jcr:data", new byte[] { 1, 0 });
+        ntFileProperties.put("jcr:content/jcr:mimeType", "image/png");
+        context.create().resource("/content/slingfile.jpg/jcr:content/renditions/myrendition.png", ntFileProperties);
+
+        assertTrue(renditionSupport.renditionExists(slingFileResource, "myrendition.png"));
+        assertFalse(renditionSupport.renditionExists(slingFileResource, "myrendition.jpg"));
+        assertFalse(renditionSupport.renditionExists(slingFileResource, "myrendition2.ong"));
+    }
+
+    @Test
+    public void testCreateRendition() throws PersistenceException {
+        assertFalse(renditionSupport.renditionExists(slingFileResource, "myrendition.png"));
+        assertNull(renditionSupport.getRenditionContent(slingFileResource, "myrendition.png"));
+        renditionSupport.setRendition(slingFileResource, "myrendition.png",
+                new ByteArrayInputStream(new byte[] { 0, 1 }));
+        assertTrue(renditionSupport.renditionExists(slingFileResource, "myrendition.png"));
+        assertNotNull(renditionSupport.getRenditionContent(slingFileResource, "myrendition.png"));
+    }
+
+    @Test
+    public void testListRenditions() throws PersistenceException {
+        renditionSupport.setRendition(slingFileResource, "myrendition.png",
+                new ByteArrayInputStream(new byte[] { 0, 1 }));
+        renditionSupport.setRendition(slingFileResource, "myrendition.jpeg",
+                new ByteArrayInputStream(new byte[] { 0, 1 }));
+        context.create().resource(slingFileResource.getPath() + "/jcr:content/renditions/jcr:content");
+        assertNotNull(renditionSupport.listRenditions(slingFileResource));
+        assertEquals(2, renditionSupport.listRenditions(slingFileResource).size());
+        assertNotNull(renditionSupport.listRenditions(ntFileresource));
+    }
+
+    @Test(expected = PersistenceException.class)
+    public void testLoginFailure() throws PersistenceException, LoginException {
+
+        when(tsu.getTransformationServiceUser()).thenThrow(new LoginException("I'm sorry, I can't do that Dave"));
+        renditionSupport.setRendition(slingFileResource, "myrendition.png",
+                new ByteArrayInputStream(new byte[] { 0, 1 }));
+    }
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImplTest.java b/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImplTest.java
new file mode 100644
index 0000000..4d68f65
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailSupportImplTest.java
@@ -0,0 +1,130 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.annotation.Annotation;
+
+import org.junit.Test;
+
+public class ThumbnailSupportImplTest {
+
+    @Test
+    public void testValidConfig() {
+        ThumbnailSupportImpl support = new ThumbnailSupportImpl(new ThumbnailSupportConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return null;
+            }
+
+            @Override
+            public String[] supportedTypes() {
+                return new String[] { "nt:file=jcr:content/jcr:mimeType", "willbe:skipped",
+                        "sling:File=jcr:content/jcr:mimeType2" };
+            }
+
+            @Override
+            public String[] persistableTypes() {
+                return new String[] { "sling:File=jcr:content/renditions", "willbe:skipped" };
+            }
+
+            @Override
+            public String errorResourcePath() {
+                return "/content/error";
+            }
+
+            @Override
+            public String errorSuffix() {
+                return "errorsuffix";
+            }
+
+        });
+
+        assertEquals(2, support.getSupportedTypes().size());
+        assertTrue(support.getSupportedTypes().contains("nt:file"));
+        assertTrue(support.getSupportedTypes().contains("sling:File"));
+
+        try {
+            support.getMetaTypePropertyPath("nt:folder");
+            fail();
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        assertEquals("jcr:content/jcr:mimeType", support.getMetaTypePropertyPath("nt:file"));
+        assertEquals("jcr:content/jcr:mimeType2", support.getMetaTypePropertyPath("sling:File"));
+
+        assertEquals(1, support.getPersistableTypes().size());
+        assertTrue(support.getPersistableTypes().contains("sling:File"));
+        assertEquals("jcr:content/renditions", support.getRenditionPath("sling:File"));
+
+        try {
+            support.getRenditionPath("nt:file");
+            fail();
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+
+        assertEquals("/content/error", support.getServletErrorResourcePath());
+        assertEquals("errorsuffix", support.getServletErrorSuffix());
+    }
+
+    @Test
+    public void testHandleDuplicatesAndUnsupported() {
+        ThumbnailSupportImpl support = new ThumbnailSupportImpl(new ThumbnailSupportConfig() {
+
+            @Override
+            public Class<? extends Annotation> annotationType() {
+                return null;
+            }
+
+            @Override
+            public String[] supportedTypes() {
+                return new String[] { "nt:file=jcr:content/jcr:mimeType", "nt:file=jcr:content/jcr:mimeType2" };
+            }
+
+            @Override
+            public String[] persistableTypes() {
+                return new String[] { "nt:file=SOMEWHERE", "nt:file=RAINBOW", "sling:File=jcr:content/renditions",
+                        "sling:File=NOWHERE" };
+            }
+
+            @Override
+            public String errorResourcePath() {
+                return "/content/error";
+            }
+
+            @Override
+            public String errorSuffix() {
+                return "errorsuffix";
+            }
+
+        });
+
+        assertEquals(1, support.getSupportedTypes().size());
+        assertEquals("jcr:content/jcr:mimeType", support.getMetaTypePropertyPath("nt:file"));
+
+        assertEquals(1, support.getPersistableTypes().size());
+        assertTrue(support.getPersistableTypes().contains("nt:file"));
+        assertEquals("SOMEWHERE", support.getRenditionPath("nt:file"));
+
+    }
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsoleTest.java b/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsoleTest.java
new file mode 100644
index 0000000..da5d515
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/ThumbnailsWebConsoleTest.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.servlet.ServletException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider;
+import org.apache.sling.thumbnails.internal.providers.PdfThumbnailProvider;
+import org.apache.sling.thumbnails.internal.transformers.CropHandler;
+import org.apache.sling.thumbnails.internal.transformers.ResizeHandler;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class ThumbnailsWebConsoleTest {
+
+    private ThumbnailsWebConsole wc;
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+
+        List<TransformationHandler> th = new ArrayList<>();
+        th.add(new CropHandler());
+        th.add(new ResizeHandler());
+
+        List<ThumbnailProvider> providers = new ArrayList<>();
+        providers.add(new ImageThumbnailProvider());
+        providers.add(new PdfThumbnailProvider());
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.emptySet());
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(Collections.singleton("nt:file"));
+        when(thumbnailSupport.getMetaTypePropertyPath("nt:file")).thenReturn("jcr:content/jcr:mimeType");
+
+        TransformerImpl transformer = new TransformerImpl(providers, thumbnailSupport, th);
+        wc = new ThumbnailsWebConsole(thumbnailSupport, transformer);
+
+    }
+
+    @Test
+    public void testWebConsole() throws IOException, ServletException {
+
+        String expected = IOUtils.toString(
+                ThumbnailsWebConsoleTest.class.getClassLoader().getResourceAsStream("web-console.txt"),
+                StandardCharsets.UTF_8);
+        wc.renderContent(context.request(), context.response());
+
+        assertEquals(expected, context.response().getOutputAsString());
+    }
+
+    @Test
+    public void testTitle() throws IOException, ServletException {
+
+        assertEquals("Sling Thumbnails", wc.getTitle());
+    }
+
+    @Test
+    public void testLabel() throws IOException, ServletException {
+        assertEquals("thumbnails", wc.getLabel());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/TransformServletTest.java b/src/test/java/org/apache/sling/thumbnails/internal/TransformServletTest.java
new file mode 100644
index 0000000..3ba6781
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/TransformServletTest.java
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletException;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.servlethelpers.MockRequestDispatcherFactory;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.internal.models.TransformationImpl;
+import org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider;
+import org.apache.sling.thumbnails.internal.providers.PdfThumbnailProvider;
+import org.apache.sling.thumbnails.internal.transformers.CropHandler;
+import org.apache.sling.thumbnails.internal.transformers.ResizeHandler;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+
+public class TransformServletTest {
+
+    private TransformServlet ts;
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private Resource resource;
+
+    private RequestDispatcher dispatcher;
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+        ContextHelper.initContext(context);
+
+        ResourceResolverFactory factory = Mockito.mock(ResourceResolverFactory.class);
+        ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+
+        List<TransformationHandlerConfig> handlers = new ArrayList<>();
+        Map<String, Object> size = new HashMap<>();
+        size.put(ResizeHandler.PN_WIDTH, 200);
+        size.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(ResizeHandler.RESOURCE_TYPE, size));
+
+        Map<String, Object> crop = new HashMap<>();
+        crop.put(CropHandler.PN_POSITION, "center");
+        crop.put(ResizeHandler.PN_WIDTH, 200);
+        crop.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(CropHandler.RESOURCE_TYPE, crop));
+
+        TransformationImpl transformation = new TransformationImpl(handlers);
+
+        resource = Mockito.mock(Resource.class);
+        Mockito.when(resource.getPath()).thenReturn("/conf");
+        Mockito.when(resource.adaptTo(Mockito.any())).thenReturn(transformation);
+        Mockito.when(resolver.findResources(Mockito.anyString(), Mockito.anyString())).thenAnswer((ans) -> {
+            List<Resource> resources = new ArrayList<>();
+            if (ans.getArgument(0, String.class).contains("test")) {
+                resources.add(resource);
+            }
+            return resources.iterator();
+        });
+        when(resolver.getResource("/conf")).thenReturn(resource);
+
+        Mockito.when(factory.getServiceResourceResolver(Mockito.any())).thenReturn(resolver);
+        TransformationServiceUser tsu = new TransformationServiceUser(factory);
+
+        List<TransformationHandler> th = new ArrayList<>();
+        th.add(new CropHandler());
+        th.add(new ResizeHandler());
+
+        List<ThumbnailProvider> providers = new ArrayList<>();
+        providers.add(new ImageThumbnailProvider());
+        providers.add(new PdfThumbnailProvider());
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.singleton("sling:File"));
+        when(thumbnailSupport.getRenditionPath("sling:File")).thenReturn("jcr:content/renditions");
+        Set<String> supportedTypes = new HashSet<>();
+        supportedTypes.add("sling:File");
+        supportedTypes.add("nt:file");
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(supportedTypes);
+        when(thumbnailSupport.getMetaTypePropertyPath(anyString())).thenReturn("jcr:content/jcr:mimeType");
+        when(thumbnailSupport.getServletErrorResourcePath()).thenReturn("/content");
+
+        TransformerImpl transformer = new TransformerImpl(providers, thumbnailSupport, th);
+
+        ResourceResolverFactory contextFactory = Mockito.mock(ResourceResolverFactory.class);
+        Mockito.when(contextFactory.getServiceResourceResolver(Mockito.any())).thenReturn(context.resourceResolver());
+        TransformationServiceUser contextTsu = new TransformationServiceUser(contextFactory);
+        RenditionSupportImpl renditionSupport = new RenditionSupportImpl(thumbnailSupport, contextTsu);
+
+        ts = new TransformServlet(thumbnailSupport, transformer, tsu, new TransformationCache(tsu), renditionSupport,
+                mock(BundleContext.class));
+
+        MockRequestDispatcherFactory dispatcherFactory = mock(MockRequestDispatcherFactory.class);
+        dispatcher = mock(RequestDispatcher.class);
+        when(dispatcherFactory.getRequestDispatcher(anyString(), any())).thenReturn(dispatcher);
+        context.request().setRequestDispatcherFactory(dispatcherFactory);
+
+    }
+
+    @Test
+    public void testValid() throws IOException, ServletException {
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        context.requestPathInfo().setSuffix("/test.png");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertNotNull(context.response().getOutput());
+    }
+
+    @Test
+    public void testPersistence() throws IOException, ServletException {
+
+        context.create().resource("/content/slingfile.jpg",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:File"));
+        Map<String, Object> slingFileProperties = new HashMap<>();
+        slingFileProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+        slingFileProperties.put(JcrConstants.JCR_DATA,
+                IOUtils.toByteArray(this.getClass().getClassLoader().getResourceAsStream("apache.png")));
+        slingFileProperties.put("jcr:mimeType", "image/jpeg");
+        context.create().resource("/content/slingfile.jpg/jcr:content", slingFileProperties);
+
+        context.currentResource("/content/slingfile.jpg");
+        context.requestPathInfo().setSuffix("/test.png");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertNotNull(context.response().getOutput());
+
+        assertNotNull(context.resourceResolver().getResource("/content/slingfile.jpg/jcr:content/renditions/test.png"));
+    }
+
+    @Test
+    public void testUnsupportedOutput() throws IOException, ServletException {
+
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        context.requestPathInfo().setSuffix("/test.webp");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testUnexpectedException() throws IOException, ServletException {
+
+        Resource throwy = mock(Resource.class);
+        when(throwy.getResourceType()).thenThrow(new RuntimeException());
+        context.currentResource(throwy);
+        context.requestPathInfo().setSuffix("/test.jpg");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertEquals(500, context.response().getStatus());
+
+        verify(dispatcher).forward(any(), any());
+    }
+
+    @Test
+    public void testInvalidConfig() throws ServletException, IOException {
+        List<TransformationHandlerConfig> handlers = new ArrayList<>();
+        Map<String, Object> crop = new HashMap<>();
+        crop.put(CropHandler.PN_POSITION, "center");
+        crop.put(ResizeHandler.PN_WIDTH, -1);
+        crop.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(CropHandler.RESOURCE_TYPE, crop));
+
+        TransformationImpl transformation = new TransformationImpl(handlers);
+
+        Mockito.when(resource.adaptTo(Mockito.any())).thenReturn(transformation);
+
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        context.requestPathInfo().setSuffix("/test.png");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertEquals(400, context.response().getStatus());
+    }
+
+    @Test
+    public void testInvalid() throws IOException, ServletException {
+
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        context.requestPathInfo().setSuffix("/te.png");
+        context.requestPathInfo().setExtension("transform");
+
+        ts.doGet(context.request(), context.response());
+
+        assertEquals(404, context.response().getStatus());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/TransformerImplTest.java b/src/test/java/org/apache/sling/thumbnails/internal/TransformerImplTest.java
new file mode 100644
index 0000000..d164586
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/TransformerImplTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal;
+
+import static org.junit.Assert.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.OutputFileFormat;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.apache.sling.thumbnails.Transformer;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.thumbnails.extension.TransformationHandler;
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.internal.models.TransformationImpl;
+import org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider;
+import org.apache.sling.thumbnails.internal.providers.PdfThumbnailProvider;
+import org.apache.sling.thumbnails.internal.transformers.CropHandler;
+import org.apache.sling.thumbnails.internal.transformers.ResizeHandler;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class TransformerImplTest {
+    @Rule
+    public final SlingContext context = new SlingContext();
+    private Transformer transformer;
+
+    @Before
+    public void init() {
+        ContextHelper.initContext(context);
+
+        List<TransformationHandler> handlers = new ArrayList<>();
+        handlers.add(new CropHandler());
+        handlers.add(new ResizeHandler());
+
+        List<ThumbnailProvider> providers = new ArrayList<>();
+        providers.add(new ImageThumbnailProvider());
+        providers.add(new PdfThumbnailProvider());
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.emptySet());
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(Collections.singleton("nt:file"));
+        when(thumbnailSupport.getMetaTypePropertyPath("nt:file")).thenReturn("jcr:content/jcr:mimeType");
+        when(thumbnailSupport.getServletErrorSuffix()).thenReturn("error");
+        when(thumbnailSupport.getServletErrorResourcePath()).thenReturn("/content/sling/error");
+
+        transformer = new TransformerImpl(providers, thumbnailSupport, handlers);
+
+    }
+
+    @Test
+    public void testImageThumbnail() throws IOException {
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        List<TransformationHandlerConfig> handlers = new ArrayList<>();
+
+        Map<String, Object> size = new HashMap<>();
+        size.put(ResizeHandler.PN_WIDTH, 200);
+        size.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(ResizeHandler.RESOURCE_TYPE, size));
+
+        Map<String, Object> crop = new HashMap<>();
+        crop.put(CropHandler.PN_POSITION, "center");
+        crop.put(ResizeHandler.PN_WIDTH, 200);
+        crop.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(CropHandler.RESOURCE_TYPE, crop));
+
+        TransformationImpl transformation = new TransformationImpl(handlers, "test", mock(Resource.class));
+        transformer.transform(context.currentResource(), transformation, OutputFileFormat.PNG, baos);
+        assertNotNull(baos);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testNotFile() throws IOException {
+        context.currentResource("/content/apache/sling-apache-org/index");
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        List<TransformationHandlerConfigImpl> handlers = new ArrayList<>();
+
+        Map<String, Object> size = new HashMap<>();
+        size.put(ResizeHandler.PN_WIDTH, 200);
+        size.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(ResizeHandler.RESOURCE_TYPE, size));
+
+        Map<String, Object> crop = new HashMap<>();
+        crop.put(CropHandler.PN_POSITION, "center");
+        crop.put(ResizeHandler.PN_WIDTH, 200);
+        crop.put(ResizeHandler.PN_HEIGHT, 200);
+        handlers.add(new TransformationHandlerConfigImpl(CropHandler.RESOURCE_TYPE, crop));
+
+        TransformationImpl transformation = new TransformationImpl(handlers);
+        transformer.transform(context.currentResource(), transformation, OutputFileFormat.PNG, baos);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImplTest.java b/src/test/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImplTest.java
new file mode 100644
index 0000000..2788d20
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/models/RenderedResourceImplTest.java
@@ -0,0 +1,211 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.models;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.AdditionalMatchers.not;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.caconfig.resource.ConfigurationResourceResolver;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.testing.mock.sling.servlet.MockRequestPathInfo;
+import org.apache.sling.thumbnails.RenderedResource;
+import org.apache.sling.thumbnails.RenditionSupport;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.apache.sling.thumbnails.internal.RenditionSupportImpl;
+import org.apache.sling.thumbnails.internal.TransformationServiceUser;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class RenderedResourceImplTest {
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private @NotNull Resource ntFileresource;
+
+    private @NotNull Resource slingFileResource;
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+
+        ContextHelper.initContext(context);
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.singleton("sling:File"));
+        when(thumbnailSupport.getRenditionPath("sling:File")).thenReturn("jcr:content/renditions");
+        when(thumbnailSupport.getRenditionPath(not(eq("sling:File"))))
+                .thenThrow(new IllegalArgumentException("Supplied non-persistable resource type!"));
+
+        Set<String> supportedTypes = new HashSet<>();
+        supportedTypes.add("sling:File");
+        supportedTypes.add("nt:file");
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(supportedTypes);
+        when(thumbnailSupport.getMetaTypePropertyPath(anyString())).thenReturn("jcr:content/jcr:mimeType");
+
+        context.registerService(ThumbnailSupport.class, thumbnailSupport);
+
+        TransformationServiceUser tsu = mock(TransformationServiceUser.class);
+        when(tsu.getTransformationServiceUser()).thenReturn(context.resourceResolver());
+
+        RenditionSupport renditionSupport = new RenditionSupportImpl(thumbnailSupport, tsu);
+        context.registerService(RenditionSupport.class, renditionSupport);
+
+        ConfigurationResourceResolver configurationResourceResolver = mock(ConfigurationResourceResolver.class);
+        Resource configResource = mock(Resource.class);
+        Transformation transformation = mock(Transformation.class);
+        when(transformation.getName()).thenReturn("test");
+        when(configResource.adaptTo(Transformation.class)).thenReturn(transformation);
+        when(configurationResourceResolver.getResourceCollection(any(), eq("files"), eq("transformations")))
+                .thenReturn(Collections.singleton(configResource));
+        context.registerService(ConfigurationResourceResolver.class, configurationResourceResolver);
+
+        Map<String, Object> ntFileProperties = new HashMap<>();
+        ntFileProperties.put("jcr:primaryType", JcrConstants.NT_FILE);
+        ntFileProperties.put("jcr:content/jcr:primaryType", JcrConstants.NT_RESOURCE);
+        ntFileProperties.put("jcr:content/jcr:data", new byte[] { 1, 0 });
+        ntFileProperties.put("jcr:content/jcr:mimeType", "image/jpeg");
+        ntFileresource = context.create().resource("/content/ntfile.jpg", ntFileProperties);
+
+        slingFileResource = context.create().resource("/content/slingfile.jpg",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:File"));
+        Map<String, Object> slingFileProperties = new HashMap<>();
+        slingFileProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
+        slingFileProperties.put(JcrConstants.JCR_DATA, new byte[] { 1, 0 });
+        slingFileProperties.put("jcr:mimeType", "image/jpeg");
+        context.create().resource("/content/slingfile.jpg/jcr:content", slingFileProperties);
+        context.create().resource("/content/slingfile.jpg/jcr:content/renditions",
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "sling:Folder"));
+
+        context.addModelsForClasses(RenderedResourceImpl.class);
+
+        ((MockRequestPathInfo) context.request().getRequestPathInfo()).setSuffix(slingFileResource.getPath());
+
+    }
+
+    @Test
+    public void testGetRenditions() {
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        assertNotNull(rendered);
+        assertNotNull(rendered.getRenditions());
+        assertEquals(0, rendered.getRenditions().size());
+        assertEquals("jcr:content/renditions", rendered.getRenditionsPath());
+
+    }
+
+    private void addRendition(String filePath, String renditionName) {
+
+        Map<String, Object> renditionProperties = new HashMap<>();
+        renditionProperties.put("jcr:primaryType", JcrConstants.NT_FILE);
+        renditionProperties.put("jcr:content/jcr:primaryType", JcrConstants.NT_RESOURCE);
+        renditionProperties.put("jcr:content/jcr:data", new byte[] { 1, 0 });
+        renditionProperties.put("jcr:content/jcr:mimeType", "image/png");
+        context.create().resource(filePath + "/jcr:content/renditions/" + renditionName, renditionProperties);
+    }
+
+    @Test
+    public void testRenditionExists() {
+        addRendition("/content/slingfile.jpg", "test.png");
+        addRendition("/content/slingfile.jpg", "test2.png");
+
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        assertEquals(2, rendered.getRenditions().size());
+    }
+
+    @Test
+    public void testInvalidResource() throws PersistenceException {
+        ((MockRequestPathInfo) context.request().getRequestPathInfo()).setSuffix(ntFileresource.getPath());
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        assertNotNull(rendered);
+        assertNull(rendered.getRenditionsPath());
+        assertEquals(0, rendered.getRenditions().size());
+    }
+
+    @Test
+    public void testSupportedRenditions() {
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        List<String> supportedRenditions = rendered.getSupportedRenditions();
+        assertNotNull(supportedRenditions);
+        assertEquals(1, supportedRenditions.size());
+        assertEquals("test", supportedRenditions.get(0));
+    }
+
+    @Test
+    public void testSupportedRenditionsMerge() {
+        addRendition("/content/slingfile.jpg", "test.png");
+        addRendition("/content/slingfile.jpg", "test2.png");
+        addRendition("/content/slingfile.jpg", "test2.jpeg");
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        List<String> supportedRenditions = rendered.getSupportedRenditions();
+        assertNotNull(supportedRenditions);
+        assertEquals(3, supportedRenditions.size());
+        assertEquals("test", supportedRenditions.get(0));
+        assertEquals("test2.png", supportedRenditions.get(1));
+        assertEquals("test2.jpeg", supportedRenditions.get(2));
+    }
+
+    @Test
+    public void testSlingHttpServletRequest() {
+        addRendition("/content/slingfile.jpg", "test.png");
+        addRendition("/content/slingfile.jpg", "test2.png");
+        addRendition("/content/slingfile.jpg", "test2.jpeg");
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        context.request().addRequestParameter("src", slingFileResource.getPath());
+        List<String> supportedRenditions = rendered.getSupportedRenditions();
+        assertNotNull(supportedRenditions);
+        assertEquals(3, supportedRenditions.size());
+        assertEquals("test", supportedRenditions.get(0));
+        assertEquals("test2.png", supportedRenditions.get(1));
+        assertEquals("test2.jpeg", supportedRenditions.get(2));
+    }
+
+    @Test
+    public void testSlingHttpServletRequestNoSrc() {
+        addRendition("/content/slingfile.jpg", "test.png");
+        addRendition("/content/slingfile.jpg", "test2.png");
+        addRendition("/content/slingfile.jpg", "test2.jpeg");
+        ((MockRequestPathInfo) context.request().getRequestPathInfo()).setSuffix(slingFileResource.getPath());
+        RenderedResource rendered = context.request().adaptTo(RenderedResource.class);
+        List<String> supportedRenditions = rendered.getSupportedRenditions();
+        assertNotNull(supportedRenditions);
+        assertEquals(3, supportedRenditions.size());
+        assertEquals("test", supportedRenditions.get(0));
+        assertEquals("test2.png", supportedRenditions.get(1));
+        assertEquals("test2.jpeg", supportedRenditions.get(2));
+    }
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/models/TransformationImplTest.java b/src/test/java/org/apache/sling/thumbnails/internal/models/TransformationImplTest.java
new file mode 100644
index 0000000..7032acd
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/models/TransformationImplTest.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.models;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.jcr.LoginException;
+
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.Transformation;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.apache.sling.thumbnails.internal.transformers.RotateHandler;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class TransformationImplTest {
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    @Before
+    public void init() throws IllegalAccessException, LoginException {
+        ContextHelper.initContext(context);
+        context.addModelsForPackage("org.apache.sling.thumbnails.internal.models");
+    }
+
+    @Test
+    public void testModel() {
+        Transformation transformation = context.resourceResolver()
+                .getResource("/conf/global/files/transformations/sling-cms-thumbnail").adaptTo(Transformation.class);
+        assertNotNull(transformation);
+        assertEquals("sling-cms-thumbnail", transformation.getName());
+        assertEquals("/conf/global/files/transformations/sling-cms-thumbnail", transformation.getPath());
+
+        assertEquals(1, transformation.getHandlers().size());
+
+        assertEquals("sling/thumbnails/transformers/crop", transformation.getHandlers().get(0).getHandlerType());
+    }
+
+    @Test
+    public void testJson() {
+
+        List<TransformationHandlerConfig> config = Collections.singletonList(new TransformationHandlerConfigImpl(
+                "sling/thumbnails/transformers/rotate", Collections.singletonMap(RotateHandler.DEGREES, 90)));
+
+        Transformation transformation = new TransformationImpl(config);
+
+        assertNotNull(transformation);
+        assertNull(transformation.getName());
+        assertNull(transformation.getPath());
+
+        assertEquals(1, transformation.getHandlers().size());
+        assertTrue(transformation.getHandlers().get(0) instanceof TransformationHandlerConfig);
+        assertEquals("sling/thumbnails/transformers/rotate", transformation.getHandlers().get(0).getHandlerType());
+
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProviderTest.java b/src/test/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProviderTest.java
new file mode 100644
index 0000000..8584e9e
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/providers/ImageThumbnailProviderTest.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ImageThumbnailProviderTest {
+
+    private static final Logger log = LoggerFactory.getLogger(ImageThumbnailProviderTest.class);
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private Resource imageFile;
+    private Resource pdfFile;
+
+    @Before
+    public void init() {
+        ContextHelper.initContext(context);
+
+        imageFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/apache.png");
+
+        pdfFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/sling.pdf");
+    }
+
+    @Test
+    public void testContentTypes() throws IOException {
+        log.info("testContentTypes");
+        ImageThumbnailProvider itp = new ImageThumbnailProvider();
+
+        assertTrue(itp.applies(imageFile, "image/png"));
+        assertFalse(itp.applies(pdfFile, "application/pdf"));
+    }
+
+    @Test
+    public void testImageThumbnailProvider() throws IOException {
+        log.info("testImageThumbnailProvider");
+        ImageThumbnailProvider itp = new ImageThumbnailProvider();
+
+        assertNotNull(itp.getThumbnail(imageFile));
+        assertArrayEquals(IOUtils.toByteArray(getClass().getClassLoader().getResourceAsStream("apache.png")),
+                IOUtils.toByteArray(itp.getThumbnail(imageFile)));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProviderTest.java b/src/test/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProviderTest.java
new file mode 100644
index 0000000..5a72967
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/providers/PdfThumbnailProviderTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class PdfThumbnailProviderTest {
+
+    private static final Logger log = LoggerFactory.getLogger(PdfThumbnailProviderTest.class);
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private Resource imageFile;
+    private Resource pdfFile;
+
+    @Before
+    public void init() {
+        ContextHelper.initContext(context);
+        imageFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/apache.png");
+        pdfFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/sling.pdf");
+    }
+
+    @Test
+    public void testContentTypes() throws IOException {
+        log.info("testContentTypes");
+        PdfThumbnailProvider ptp = new PdfThumbnailProvider();
+        assertFalse(ptp.applies(imageFile, "image/png"));
+        assertTrue(ptp.applies(pdfFile, "application/pdf"));
+    }
+
+    @Test
+    public void testPDFThumbnailProvider() throws IOException {
+        log.info("testPDFThumbnailProvider");
+        PdfThumbnailProvider ptp = new PdfThumbnailProvider();
+        assertNotNull(ptp.getThumbnail(pdfFile));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProviderTest.java b/src/test/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProviderTest.java
new file mode 100644
index 0000000..4ea5c99
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/providers/SlideShowThumbnailProviderTest.java
@@ -0,0 +1,84 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.Collections;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.thumbnails.extension.ThumbnailProvider;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.ThumbnailSupport;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SlideShowThumbnailProviderTest {
+
+    private static final Logger log = LoggerFactory.getLogger(SlideShowThumbnailProviderTest.class);
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private Resource docxFile;
+    private Resource pptFile;
+    private Resource pptxFile;
+
+    private ThumbnailProvider provider;
+
+    @Before
+    public void init() {
+        ContextHelper.initContext(context);
+        docxFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/Sling.docx");
+        pptxFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/Sling.pptx");
+        pptFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/Sling.ppt");
+
+        ThumbnailSupport thumbnailSupport = mock(ThumbnailSupport.class);
+        when(thumbnailSupport.getPersistableTypes()).thenReturn(Collections.emptySet());
+        when(thumbnailSupport.getSupportedTypes()).thenReturn(Collections.singleton("sling:File"));
+        when(thumbnailSupport.getMetaTypePropertyPath("sling:File")).thenReturn("jcr:content/jcr:mimeType");
+
+        provider = new SlideShowThumbnailProvider(null, thumbnailSupport);
+    }
+
+    @Test
+    public void testApplies() throws IOException {
+        log.info("testApplies");
+        assertTrue(provider.applies(pptxFile,
+                "application/vnd.openxmlformats-officedocument.presentationml.presentation"));
+        assertTrue(provider.applies(pptFile, "application/vnd.ms-powerpoint"));
+        assertFalse(
+                provider.applies(docxFile, "application/vnd.openxmlformats-officedocument.wordprocessingml.document"));
+    }
+
+    @Test
+    public void testGetThumbnail() throws IOException {
+        log.info("testGetThumbnail");
+        assertNotNull(provider.getThumbnail(pptxFile));
+        assertNotNull(provider.getThumbnail(pptFile));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProviderTest.java b/src/test/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProviderTest.java
new file mode 100644
index 0000000..350bed9
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/providers/TikaFallbackProviderTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.providers;
+
+import static org.junit.Assert.assertNotNull;
+
+import java.io.IOException;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.apache.sling.thumbnails.internal.ContextHelper;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TikaFallbackProviderTest {
+
+    private static final Logger log = LoggerFactory.getLogger(TikaFallbackProviderTest.class);
+
+    @Rule
+    public final SlingContext context = new SlingContext();
+
+    private Resource docxFile;
+
+    private Resource largeFile;
+
+    @Before
+    public void init() {
+        ContextHelper.initContext(context);
+        docxFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/Sling.docx");
+        largeFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/editor.min.css");
+    }
+
+    @Test
+    public void testTikaProvider() throws IOException {
+        log.info("testTikaProvider");
+        TikaFallbackProvider tfp = new TikaFallbackProvider();
+        assertNotNull(tfp.getThumbnail(docxFile));
+    }
+
+
+    @Test
+    public void testLargeFile() throws IOException {
+        log.info("testLargeFile");
+        TikaFallbackProvider tfp = new TikaFallbackProvider();
+        assertNotNull(tfp.getThumbnail(largeFile));
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandlerTest.java
new file mode 100644
index 0000000..0b4459a
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ColorizeHandlerTest.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ColorizeHandlerTest {
+
+    private ByteArrayOutputStream outputStream;
+    private InputStream inputStream;
+    private ColorizeHandler colorizer;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        colorizer = new ColorizeHandler();
+    }
+
+    @Test
+    public void testResourceType() throws IOException {
+        assertEquals("sling/thumbnails/transformers/colorize", colorizer.getResourceType());
+    }
+
+    @Test
+    public void testColorize() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_BLUE, 12);
+        properties.put(ColorizeHandler.PN_RED, 12);
+        properties.put(ColorizeHandler.PN_GREEN, 12);
+        properties.put(ColorizeHandler.PN_ALPHA, .8);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        colorizer.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testNoAlpha() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_BLUE, 12);
+        properties.put(ColorizeHandler.PN_RED, 12);
+        properties.put(ColorizeHandler.PN_GREEN, 12);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        colorizer.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testMissingColor() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_BLUE, 12);
+        properties.put(ColorizeHandler.PN_GREEN, 12);
+        properties.put(ColorizeHandler.PN_ALPHA, .8);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        colorizer.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidColor() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_BLUE, 12);
+        properties.put(ColorizeHandler.PN_RED, 256);
+        properties.put(ColorizeHandler.PN_GREEN, 12);
+        properties.put(ColorizeHandler.PN_ALPHA, .8);
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        colorizer.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidAlpha() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_BLUE, 12);
+        properties.put(ColorizeHandler.PN_RED, 12);
+        properties.put(ColorizeHandler.PN_GREEN, 12);
+        properties.put(ColorizeHandler.PN_ALPHA, -.8);
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        colorizer.handle(inputStream, outputStream, config);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/CropHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/CropHandlerTest.java
new file mode 100644
index 0000000..97c66ec
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/CropHandlerTest.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CropHandlerTest {
+
+    private ByteArrayOutputStream outputStream;
+    private InputStream inputStream;
+    private CropHandler cropper;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        cropper = new CropHandler();
+    }
+
+    @Test
+    public void testCrop() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CropHandler.PN_POSITION, "CENTER");
+        properties.put(ResizeHandler.PN_HEIGHT, 200);
+        properties.put(ResizeHandler.PN_WIDTH, 200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        cropper.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testCropLower() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CropHandler.PN_POSITION, "center");
+        properties.put(ResizeHandler.PN_HEIGHT, 200);
+        properties.put(ResizeHandler.PN_WIDTH, 200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        cropper.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidPosition() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(CropHandler.PN_POSITION, "centerz");
+        properties.put(ResizeHandler.PN_HEIGHT, 200);
+        properties.put(ResizeHandler.PN_WIDTH, 200);
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        cropper.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testMissingWidthHeight() throws IOException {
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf",
+                Collections.singletonMap(CropHandler.PN_POSITION, "center"));
+        cropper.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidHuge() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, Integer.MAX_VALUE);
+        properties.put(ResizeHandler.PN_HEIGHT, Integer.MAX_VALUE);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        cropper.handle(inputStream, outputStream, config);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/FlipHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/FlipHandlerTest.java
new file mode 100644
index 0000000..f433bf3
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/FlipHandlerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class FlipHandlerTest {
+
+    private InputStream inputStream;
+    private ByteArrayOutputStream outputStream;
+    private FlipHandler flop;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        flop = new FlipHandler();
+    }
+
+    @Test
+    public void testFlipHorizontal() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(FlipHandler.PN_DIRECTION, "horizontal");
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        flop.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testFlipVertical() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(FlipHandler.PN_DIRECTION, "VERTICAL");
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        flop.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidDirection() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(FlipHandler.PN_DIRECTION, "asdf");
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        flop.handle(inputStream, outputStream, config);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandlerTest.java
new file mode 100644
index 0000000..5bcb5b6
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/GreyscaleHandlerTest.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class GreyscaleHandlerTest {
+
+    private ByteArrayOutputStream outputStream;
+    private InputStream inputStream;
+    private GreyscaleHandler decolorizer;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        decolorizer = new GreyscaleHandler();
+    }
+
+    @Test
+    public void testResourceType() throws IOException {
+        assertEquals("sling/thumbnails/transformers/greyscale", decolorizer.getResourceType());
+    }
+
+    @Test
+    public void testColorize() throws IOException {
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", Collections.emptyMap());
+        decolorizer.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandlerTest.java
new file mode 100644
index 0000000..b59399f
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ResizeHandlerTest.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ResizeHandlerTest {
+
+    private InputStream inputStream;
+    private ByteArrayOutputStream outputStream;
+    private ResizeHandler sizer;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        sizer = new ResizeHandler();
+    }
+
+    @Test
+    public void testResize() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, 200);
+        properties.put(ResizeHandler.PN_HEIGHT, 200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        sizer.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testInvalidWidth() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, "K");
+        properties.put(ResizeHandler.PN_HEIGHT, 200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        sizer.handle(inputStream, outputStream, config);
+        assertNotNull(outputStream.toByteArray());
+
+    }
+
+    @Test
+    public void testInvalidHeight() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, 200);
+        properties.put(ResizeHandler.PN_HEIGHT, "h");
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        sizer.handle(inputStream, outputStream, config);
+
+        assertNotNull(outputStream.toByteArray());
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidHuge() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, Integer.MAX_VALUE);
+        properties.put(ResizeHandler.PN_HEIGHT, Integer.MAX_VALUE);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        sizer.handle(inputStream, outputStream, config);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/RotateHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/RotateHandlerTest.java
new file mode 100644
index 0000000..ef40e67
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/RotateHandlerTest.java
@@ -0,0 +1,92 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class RotateHandlerTest {
+
+    private InputStream inputStream;
+    private ByteArrayOutputStream outputStream;
+    private RotateHandler spinner;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        spinner = new RotateHandler();
+    }
+
+    @Test
+    public void testResourceType() throws IOException {
+        assertEquals("sling/thumbnails/transformers/rotate", spinner.getResourceType());
+    }
+
+    @Test
+    public void testRotate() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(RotateHandler.DEGREES, 200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        spinner.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testNoDegrees() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        spinner.handle(inputStream, outputStream, config);
+        assertNotNull(outputStream.toByteArray());
+    }
+
+    @Test
+    public void testNegativeDegrees() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(RotateHandler.DEGREES, -200);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        spinner.handle(inputStream, outputStream, config);
+        assertNotNull(outputStream.toByteArray());
+    }
+
+    @Test
+    public void testAbsurdlyHighDegrees() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(RotateHandler.DEGREES, Integer.MAX_VALUE);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        spinner.handle(inputStream, outputStream, config);
+        assertNotNull(outputStream.toByteArray());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandlerTest.java
new file mode 100644
index 0000000..daf51c8
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/ScaleHandlerTest.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ScaleHandlerTest {
+
+    private InputStream inputStream;
+    private ByteArrayOutputStream outputStream;
+    private ScaleHandler scaler;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        scaler = new ScaleHandler();
+    }
+
+    @Test
+    public void testResourceType() throws IOException {
+        assertEquals("sling/thumbnails/transformers/scale", scaler.getResourceType());
+    }
+
+    @Test
+    public void testScaleBoth() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ScaleHandler.PN_BOTH, 2);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        scaler.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testScaleWidthAndHeight() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, 2);
+        properties.put(ResizeHandler.PN_HEIGHT, 1);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        scaler.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidScale() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_WIDTH, 2);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        scaler.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidScaleWidth() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ResizeHandler.PN_HEIGHT, 2);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        scaler.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidHugeScale() throws IOException {
+
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ScaleHandler.PN_BOTH, Integer.MAX_VALUE);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        scaler.handle(inputStream, outputStream, config);
+    }
+}
diff --git a/src/test/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandlerTest.java b/src/test/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandlerTest.java
new file mode 100644
index 0000000..a737c75
--- /dev/null
+++ b/src/test/java/org/apache/sling/thumbnails/internal/transformers/TransparencyHandlerTest.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.thumbnails.internal.transformers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.thumbnails.internal.models.TransformationHandlerConfigImpl;
+import org.apache.sling.thumbnails.BadRequestException;
+import org.apache.sling.thumbnails.TransformationHandlerConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TransparencyHandlerTest {
+
+    private ByteArrayOutputStream outputStream;
+    private InputStream inputStream;
+    private TransparencyHandler transparent;
+
+    @Before
+    public void init() {
+        inputStream = getClass().getClassLoader().getResourceAsStream("apache.png");
+        outputStream = new ByteArrayOutputStream();
+        transparent = new TransparencyHandler();
+    }
+
+    @Test
+    public void testResourceType() throws IOException {
+        assertEquals("sling/thumbnails/transformers/transparency", transparent.getResourceType());
+    }
+
+    @Test
+    public void testTransparency() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_ALPHA, .8);
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        transparent.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test
+    public void testNoAlpha() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        transparent.handle(inputStream, outputStream, config);
+        assertNotEquals(0, outputStream.toByteArray().length);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidAlpha() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_ALPHA, -.8);
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        transparent.handle(inputStream, outputStream, config);
+    }
+
+    @Test(expected = BadRequestException.class)
+    public void testInvalidLargeAlpha() throws IOException {
+        Map<String, Object> properties = new HashMap<>();
+        properties.put(ColorizeHandler.PN_ALPHA, 2.0);
+        TransformationHandlerConfig config = new TransformationHandlerConfigImpl("/conf", properties);
+        transparent.handle(inputStream, outputStream, config);
+    }
+
+}
diff --git a/src/test/resources/Sling.docx b/src/test/resources/Sling.docx
new file mode 100644
index 0000000..73fba2a
Binary files /dev/null and b/src/test/resources/Sling.docx differ
diff --git a/src/test/resources/Sling.ppt b/src/test/resources/Sling.ppt
new file mode 100644
index 0000000..0e9a6d3
Binary files /dev/null and b/src/test/resources/Sling.ppt differ
diff --git a/src/test/resources/Sling.pptx b/src/test/resources/Sling.pptx
new file mode 100644
index 0000000..8ed0a34
Binary files /dev/null and b/src/test/resources/Sling.pptx differ
diff --git a/src/test/resources/apache.png b/src/test/resources/apache.png
new file mode 100644
index 0000000..fc3f667
Binary files /dev/null and b/src/test/resources/apache.png differ
diff --git a/src/test/resources/conf.json b/src/test/resources/conf.json
new file mode 100644
index 0000000..6f595c1
--- /dev/null
+++ b/src/test/resources/conf.json
@@ -0,0 +1,355 @@
+{
+  "jcr:primaryType": "sling:Folder",
+  "jcr:mixinTypes": ["rep:AccessControllable"],
+  "jcr:createdBy": "admin",
+  "jcr:created": "Tue Jul 20 2021 19:34:02 GMT-0400",
+  "global": {
+    "jcr:primaryType": "sling:OrderedFolder",
+    "jcr:createdBy": "sling-jcr-content-loader",
+    "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+    "jcr:content": {
+      "jcr:primaryType": "nt:unstructured",
+      "jcr:title": "Global"
+    },
+    "files": {
+      "jcr:primaryType": "sling:OrderedFolder",
+      "jcr:createdBy": "sling-jcr-content-loader",
+      "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+      "jcr:content": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:title": "File Configurations"
+      },
+      "editors": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "File Editors",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/fileeditor",
+        "default": {
+          "jcr:primaryType": "nt:unstructured",
+          "jcr:title": "Default File Editor",
+          "sling:resourceType": "sling-cms/components/caconfig/fileeditor/config",
+          "fields": {
+            "jcr:primaryType": "nt:unstructured",
+            "title": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": false,
+              "name": "jcr:content/jcr:title",
+              "type": "text",
+              "label": "Title",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            },
+            "jcrmimeType": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": true,
+              "name": "jcr:content/jcr:mimeType",
+              "type": "text",
+              "label": "MIME Type",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            },
+            "licensing": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": false,
+              "name": "jcr:content/licensing",
+              "type": "text",
+              "label": "Licensing",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            },
+            "taxonomy": {
+              "jcr:primaryType": "nt:unstructured",
+              "name": "jcr:content/sling:taxonomy",
+              "label": "Taxonomy",
+              "sling:resourceType": "sling-cms/components/editor/fields/taxonomy"
+            },
+            "taxonomyTypeHint": {
+              "jcr:primaryType": "nt:unstructured",
+              "name": "jcr:content/sling:taxonomy@TypeHint",
+              "value": "String[]",
+              "sling:resourceType": "sling-cms/components/editor/fields/hidden"
+            },
+            "published": {
+              "jcr:primaryType": "nt:unstructured",
+              "name": "jcr:content/published",
+              "label": "Published",
+              "sling:resourceType": "sling-cms/components/editor/fields/publication"
+            },
+            "filemetadata": {
+              "jcr:primaryType": "nt:unstructured",
+              "name": "jcr:content/filemetadata",
+              "label": "File Metadata",
+              "sling:resourceType": "sling-cms/components/editor/fields/filemetadata"
+            }
+          }
+        }
+      },
+      "transformations": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "Transformations",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/transformations",
+        "sling-cms-thumbnail": {
+          "jcr:primaryType": "nt:unstructured",
+          "name": "sling-cms-thumbnail",
+          "sling:resourceType": "sling/thumbnails/transformation",
+          "handlers": {
+            "jcr:primaryType": "nt:unstructured",
+            "crop": {
+              "jcr:primaryType": "nt:unstructured",
+              "height": "480",
+              "width": "600",
+              "position": "CENTER",
+              "sling:resourceType": "sling/thumbnails/transformers/crop"
+            }
+          }
+        },
+        "sling-cms-thumbnail128": {
+          "jcr:primaryType": "nt:unstructured",
+          "name": "sling-cms-thumbnail128",
+          "sling:resourceType": "sling/thumbnails/transformation",
+          "handlers": {
+            "jcr:primaryType": "nt:unstructured",
+            "crop": {
+              "jcr:primaryType": "nt:unstructured",
+              "height": "128",
+              "width": "128",
+              "position": "CENTER",
+              "sling:resourceType": "sling/thumbnails/transformers/crop"
+            }
+          }
+        },
+        "sling-cms-thumbnail32": {
+          "jcr:primaryType": "nt:unstructured",
+          "name": "sling-cms-thumbnail32",
+          "sling:resourceType": "sling/thumbnails/transformation",
+          "handlers": {
+            "jcr:primaryType": "nt:unstructured",
+            "crop": {
+              "jcr:primaryType": "nt:unstructured",
+              "height": "32",
+              "width": "32",
+              "position": "CENTER",
+              "sling:resourceType": "sling/thumbnails/transformers/crop"
+            }
+          }
+        }
+      }
+    },
+    "site": {
+      "jcr:primaryType": "sling:OrderedFolder",
+      "jcr:createdBy": "sling-jcr-content-loader",
+      "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+      "jcr:content": {
+        "jcr:primaryType": "nt:unstructured",
+        "jcr:title": "Default Site Configuration"
+      },
+      "rewrite": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "Rewriter",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "attributes": ["action", "href", "src"],
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/rewriter",
+        "doctype": "<!DOCTYPE html>"
+      },
+      "policies": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "Default Policy",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/policies",
+        "policy": {
+          "jcr:primaryType": "nt:unstructured",
+          "jcr:title": "Bootstrap CSS Policy",
+          "availableComponentTypes": ["General"],
+          "sling:resourceType": "sling-cms/components/caconfig/policy",
+          "componentConfigurations": {
+            "jcr:primaryType": "nt:unstructured",
+            "component": {
+              "jcr:primaryType": "nt:unstructured",
+              "fieldConfigGroups": "Form Field,General",
+              "checkInputClass": "form-check-input",
+              "fieldGroupClass": "form-group",
+              "checkLabelClass": "form-check-label",
+              "fieldRequiredClass": "text-danger",
+              "type": "reference/components/forms/form",
+              "submitClass": "btn btn-primary",
+              "fieldClass": "form-control",
+              "actionConfigGroups": "Form Action",
+              "providerConfigGroups": "Form Value Provider",
+              "sling:resourceType": "sling-cms/components/caconfig/component",
+              "checkFieldClass": "form-check",
+              "formClass": "form",
+              "alertClass": "alert alert-dark"
+            },
+            "component_1310080869": {
+              "jcr:primaryType": "nt:unstructured",
+              "ctaClasses": ["Primary=btn-primary"],
+              "type": "reference/components/general/cta",
+              "sling:resourceType": "sling-cms/components/caconfig/component"
+            },
+            "component_867209498": {
+              "jcr:primaryType": "nt:unstructured",
+              "type": "reference/components/general/columncontrol",
+              "rowClass": "row",
+              "containerclass": "container",
+              "columns": [
+                "100%=col-md-12",
+                "50% 50%=col-6,col-6",
+                "33% 33% 33%=col-4,col-4,col-4"
+              ],
+              "sling:resourceType": "sling-cms/components/caconfig/component"
+            },
+            "component_1644835788": {
+              "jcr:primaryType": "nt:unstructured",
+              "iframeClass": "frame",
+              "type": "reference/components/general/iframe",
+              "sling:resourceType": "sling-cms/components/caconfig/component",
+              "wrapperClasses": ["embed-responsive"]
+            },
+            "component_1264147350": {
+              "jcr:primaryType": "nt:unstructured",
+              "type": "reference/components/general/image",
+              "sling:resourceType": "sling-cms/components/caconfig/component",
+              "imageClasses": ["Responsive=img-responsive"]
+            },
+            "component_2817746": {
+              "jcr:primaryType": "nt:unstructured",
+              "pageItemClass": "page-item",
+              "pageLinkClass": "page-link",
+              "type": "reference/components/general/list",
+              "paginationClass": "pagination",
+              "sling:resourceType": "sling-cms/components/caconfig/component"
+            },
+            "component_55724978": {
+              "jcr:primaryType": "nt:unstructured",
+              "pageItemClass": "page-item",
+              "pageLinkClass": "page-link",
+              "searchClass": "search",
+              "resultClass": "search-result",
+              "type": "reference/components/general/search",
+              "paginationClass": "pagination",
+              "resultHeaderClass": "search-header",
+              "sling:resourceType": "sling-cms/components/caconfig/component"
+            },
+            "component_928101893": {
+              "jcr:primaryType": "nt:unstructured",
+              "buttonClass": "btn btn-primary",
+              "type": "reference/components/general/searchform",
+              "sling:resourceType": "sling-cms/components/caconfig/component",
+              "inputClass": "input",
+              "formClass": "form"
+            },
+            "component_1816434016": {
+              "jcr:primaryType": "nt:unstructured",
+              "tagPage": "",
+              "listClass": "tag-list",
+              "type": "reference/components/general/tags",
+              "listTag": "div",
+              "sling:resourceType": "sling-cms/components/caconfig/component",
+              "itemTag": "div",
+              "itemClass": "tag-item"
+            }
+          }
+        }
+      },
+      "settings": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "Site Settings",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "taxonomyroot": "/etc/taxonomy",
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/sitesettings"
+      },
+      "templates": {
+        "jcr:primaryType": "sling:Config",
+        "jcr:createdBy": "sling-jcr-content-loader",
+        "jcr:title": "Templates",
+        "jcr:lastModifiedBy": "sling-jcr-content-loader",
+        "jcr:created": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "jcr:lastModified": "Tue Jul 20 2021 19:34:03 GMT-0400",
+        "sling:resourceType": "sling-cms/components/caconfig/templates",
+        "base-page": {
+          "jcr:primaryType": "nt:unstructured",
+          "jcr:title": "Base Page",
+          "template": "{\r\n  \"jcr:primaryType\": \"sling:Page\",\r\n  \"jcr:content\": {\r\n    \"jcr:primaryType\": \"nt:unstructured\",\r\n    \"jcr:title\": \"{{title}}\",\r\n    \"sling:template\": \"/conf/global/site/templates/base-page\",\r\n    \"sling:resourceType\": \"reference/components/pages/base\",\r\n    \"published\": false\r\n  }\r\n}",
+          "allowedPaths": ["/content/apache/sling-apache-org.*"],
+          "sling:resourceType": "sling-cms/components/caconfig/template",
+          "fields": {
+            "jcr:primaryType": "nt:unstructured",
+            "text": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": true,
+              "name": "title",
+              "type": "text",
+              "label": "Title",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            },
+            "text_1147023191": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": true,
+              "name": ":name",
+              "type": "text",
+              "label": "Name",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            }
+          },
+          "policies": {
+            "jcr:primaryType": "nt:unstructured",
+            "policymapping": {
+              "jcr:primaryType": "nt:unstructured",
+              "sling:resourceType": "sling-cms/components/caconfig/policymapping",
+              "pathPattern": ".*",
+              "policyPath": "/conf/global/site/policies/policy"
+            }
+          }
+        },
+        "fragment": {
+          "jcr:primaryType": "nt:unstructured",
+          "jcr:title": "Fragment",
+          "template": "{\r\n  \"jcr:primaryType\": \"sling:Page\",\r\n  \"jcr:content\": {\r\n    \"jcr:primaryType\": \"nt:unstructured\",\r\n    \"jcr:title\": \"{{title}}\",\r\n    \"sling:template\": \"/conf/global/site/templates/fragment\",\r\n    \"sling:resourceType\": \"sling-cms/components/pages/fragment\",\r\n    \"published\": false\r\n  }\r\n}",
+          "allowedPaths": ["/content.*"],
+          "sling:resourceType": "sling-cms/components/caconfig/template",
+          "fields": {
+            "jcr:primaryType": "nt:unstructured",
+            "text": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": true,
+              "name": "title",
+              "type": "text",
+              "label": "Title",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            },
+            "text_1147023191": {
+              "jcr:primaryType": "nt:unstructured",
+              "required": true,
+              "name": ":name",
+              "type": "text",
+              "label": "Name",
+              "sling:resourceType": "sling-cms/components/editor/fields/text"
+            }
+          },
+          "policies": {
+            "jcr:primaryType": "nt:unstructured",
+            "policymapping": {
+              "jcr:primaryType": "nt:unstructured",
+              "sling:resourceType": "sling-cms/components/caconfig/policymapping",
+              "pathPattern": ".*",
+              "policyPath": "/conf/global/site/policies/policy"
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/resources/content.json b/src/test/resources/content.json
new file mode 100644
index 0000000..3591ef7
--- /dev/null
+++ b/src/test/resources/content.json
@@ -0,0 +1,115 @@
+{
+    "jcr:primaryType": "sling:OrderedFolder",
+    "jcr:mixinTypes": [
+        "rep:AccessControllable"
+    ],
+    "jcr:createdBy": "admin",
+    "jcr:created": "Wed May 15 2019 12:39:57 GMT-0400",
+    "rep:policy": {
+        "jcr:primaryType": "rep:ACL",
+        "allow": {
+            "jcr:primaryType": "rep:GrantACE",
+            "rep:principalName": "authors",
+            "rep:privileges": [
+                "jcr:versionManagement",
+                "rep:write"
+            ]
+        },
+        "allow1": {
+            "jcr:primaryType": "rep:GrantACE",
+            "rep:principalName": "sling-cms-metadata",
+            "rep:privileges": [
+                "jcr:versionManagement",
+                "rep:write"
+            ]
+        },
+        "allow2": {
+            "jcr:primaryType": "rep:GrantACE",
+            "rep:principalName": "sling-cms-versionmgr",
+            "rep:privileges": [
+                "jcr:versionManagement",
+                "rep:write"
+            ]
+        },
+        "allow3": {
+            "jcr:primaryType": "rep:GrantACE",
+            "rep:principalName": "everyone",
+            "rep:privileges": [
+                "jcr:read"
+            ]
+        }
+    },
+    "apache": {
+        "jcr:primaryType": "sling:OrderedFolder",
+        "jcr:createdBy": "admin",
+        "sling:configRef": "/conf/global",
+        "jcr:created": "Wed May 15 2019 12:40:00 GMT-0400",
+        "jcr:content": {
+            "jcr:primaryType": "nt:unstructured",
+            "jcr:title": "Apache Software Foundation"
+        },
+        "sling-apache-org": {
+            "jcr:primaryType": "sling:Site",
+            "jcr:createdBy": "admin",
+            "jcr:title": "Apache Sling",
+            "jcr:language": "en",
+            "sling:url": "https://sling.apache.org",
+            "jcr:created": "Wed May 15 2019 12:40:00 GMT-0400",
+            "index": {
+                "jcr:primaryType": "sling:Page",
+                "jcr:mixinTypes": [
+                    "mix:versionable"
+                ],
+                "jcr:createdBy": "admin",
+                "jcr:versionHistory": "87458e80-83b8-46ee-a5d8-3e39c2f07c10",
+                "jcr:predecessors": [],
+                "jcr:created": "Wed May 15 2019 12:40:00 GMT-0400",
+                "jcr:baseVersion": "9ddd2472-9a0e-4fcb-8a2d-72f0b3f40d61",
+                "jcr:isCheckedOut": false,
+                "jcr:uuid": "fd5c6000-b3b9-44a2-88a0-1c8e13d7c1a7",
+                "jcr:content": {
+                    "jcr:primaryType": "nt:unstructured",
+                    "jcr:title": "Apache Sling - Bringing Back the Fun!",
+                    "jcr:lastModifiedBy": "admin",
+                    "sling:template": "/conf/global/site/templates/base-page",
+                    "sling:taxonomy": "/etc/taxonomy/reference/community",
+                    "jcr:lastModified": "Wed May 15 2019 14:05:46 GMT-0400",
+                    "sling:resourceType": "reference/components/pages/base",
+                    "published": true,
+                    "hideInSitemap": false,
+                    "container": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "richtext": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "text": "<p>Apache Sling(TM) is a framework for RESTful web-applications based on an extensible content tree.</p>\r\n<p>In a nutshell, Sling maps HTTP request URLs to content resources based on the request's path, extension and selectors. Using convention over configuration, requests are processed by scripts and servlets, dynamically selected based on the current resource. This fosters meaningful URLs and resource driven request processing, while the modular n [...]
+                            "sling:resourceType": "sling-cms/components/general/richtext"
+                        }
+                    },
+                    "menu": {
+                        "jcr:primaryType": "nt:unstructured",
+                        "richtext": {
+                            "jcr:primaryType": "nt:unstructured",
+                            "text": "<p>\r\n                <strong><a href=\"#\">Documentation</a></strong><br>\r\n                <a href=\"#\">Getting Started</a><br>\r\n                <a href=\"#\">The Sling Engine</a><br>\r\n                <a href=\"#\">Development</a><br>\r\n                <a href=\"#\">Bundles</a><br>\r\n                <a href=\"#\">Tutorials &amp; How-Tos</a><br>\r\n                <a href=\"http://sling.apache.org/components/\">Maven Plugins</a><br>\r\n      [...]
+                            "sling:resourceType": "sling-cms/components/general/richtext"
+                        }
+                    }
+                },
+                "apache.png": {
+                    "jcr:primaryType": "nt:file"
+                },
+                "sling.pdf": {
+                    "jcr:primaryType": "sling:File"
+                },
+                "Sling.docx": {
+                    "jcr:primaryType": "sling:File"
+                },
+                "Sling.pptx": {
+                    "jcr:primaryType": "sling:File"
+                },
+                "Sling.ppt": {
+                    "jcr:primaryType": "sling:File"
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/editor.min.css b/src/test/resources/editor.min.css
new file mode 100644
index 0000000..8d051e5
--- /dev/null
+++ b/src/test/resources/editor.min.css
@@ -0,0 +1,2854 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+.sling-cms-editor {
+  /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+  /*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+  /*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */
+  font-family: "Open Sans",sans-serif; }
+
+@font-face {
+  .sling-cms-editor {
+    font-family: 'Open Sans';
+    src: url("../fonts/OpenSans-Light-webfont.woff") format("woff");
+    font-weight: 300;
+    font-style: normal;
+    font-display: fallback; } }
+
+@font-face {
+  .sling-cms-editor {
+    font-family: 'Open Sans';
+    src: url("../fonts/OpenSans-Regular-webfont.woff") format("woff");
+    font-weight: 400;
+    font-style: normal;
+    font-display: fallback; } }
+
+@font-face {
+  .sling-cms-editor {
+    font-family: 'Open Sans';
+    src: url("../fonts/OpenSans-Semibold-webfont.woff") format("woff");
+    font-weight: 600;
+    font-style: normal;
+    font-display: fallback; } }
+
+@font-face {
+  .sling-cms-editor {
+    font-family: 'Open Sans';
+    src: url("../fonts/OpenSans-Bold-webfont.woff") format("woff");
+    font-weight: 700;
+    font-style: normal;
+    font-display: fallback; } }
+
+@keyframes spinAround {
+  from {
+    transform: rotate(0deg); }
+  to {
+    transform: rotate(359deg); } }
+  .sling-cms-editor .modal-close, .sling-cms-editor .is-unselectable, .sling-cms-editor .button, .sling-cms-editor .file {
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none; }
+  .sling-cms-editor .select:not(.is-multiple):not(.is-loading)::after {
+    border: 3px solid transparent;
+    border-radius: 2px;
+    border-right: 0;
+    border-top: 0;
+    content: " ";
+    display: block;
+    height: 0.625em;
+    margin-top: -0.4375em;
+    pointer-events: none;
+    position: absolute;
+    top: 50%;
+    transform: rotate(-45deg);
+    transform-origin: center;
+    width: 0.625em; }
+  .sling-cms-editor .box:not(:last-child), .sling-cms-editor .level:not(:last-child) {
+    margin-bottom: 1.5rem; }
+  .sling-cms-editor .modal-close {
+    -moz-appearance: none;
+    -webkit-appearance: none;
+    background-color: rgba(10, 10, 10, 0.2);
+    border: none;
+    border-radius: 290486px;
+    cursor: pointer;
+    pointer-events: auto;
+    display: inline-block;
+    flex-grow: 0;
+    flex-shrink: 0;
+    font-size: 0;
+    height: 20px;
+    max-height: 20px;
+    max-width: 20px;
+    min-height: 20px;
+    min-width: 20px;
+    outline: none;
+    position: relative;
+    vertical-align: top;
+    width: 20px; }
+    .sling-cms-editor .modal-close::before, .sling-cms-editor .modal-close::after {
+      background-color: white;
+      content: "";
+      display: block;
+      left: 50%;
+      position: absolute;
+      top: 50%;
+      transform: translateX(-50%) translateY(-50%) rotate(45deg);
+      transform-origin: center center; }
+    .sling-cms-editor .modal-close::before {
+      height: 2px;
+      width: 50%; }
+    .sling-cms-editor .modal-close::after {
+      height: 50%;
+      width: 2px; }
+    .sling-cms-editor .modal-close:hover, .sling-cms-editor .modal-close:focus {
+      background-color: rgba(10, 10, 10, 0.3); }
+    .sling-cms-editor .modal-close:active {
+      background-color: rgba(10, 10, 10, 0.4); }
+    .sling-cms-editor .is-small.modal-close {
+      height: 16px;
+      max-height: 16px;
+      max-width: 16px;
+      min-height: 16px;
+      min-width: 16px;
+      width: 16px; }
+    .sling-cms-editor .is-medium.modal-close {
+      height: 24px;
+      max-height: 24px;
+      max-width: 24px;
+      min-height: 24px;
+      min-width: 24px;
+      width: 24px; }
+    .sling-cms-editor .is-large.modal-close {
+      height: 32px;
+      max-height: 32px;
+      max-width: 32px;
+      min-height: 32px;
+      min-width: 32px;
+      width: 32px; }
+  .sling-cms-editor .button.is-loading::after, .sling-cms-editor .select.is-loading::after, .sling-cms-editor .control.is-loading::after {
+    animation: spinAround 500ms infinite linear;
+    border: 2px solid #dbdbdb;
+    border-radius: 290486px;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    content: "";
+    display: block;
+    height: 1em;
+    position: relative;
+    width: 1em; }
+  .sling-cms-editor .is-overlay, .sling-cms-editor .modal, .sling-cms-editor .modal-background {
+    bottom: 0;
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 0; }
+  .sling-cms-editor .button, .sling-cms-editor .input, .sling-cms-editor .textarea, .sling-cms-editor .select select, .sling-cms-editor .file-cta,
+  .sling-cms-editor .file-name {
+    -moz-appearance: none;
+    -webkit-appearance: none;
+    align-items: center;
+    border: 1px solid transparent;
+    border-radius: 4px;
+    box-shadow: none;
+    display: inline-flex;
+    font-size: 1rem;
+    height: 2.5em;
+    justify-content: flex-start;
+    line-height: 1.5;
+    padding-bottom: calc(0.5em - 1px);
+    padding-left: calc(0.75em - 1px);
+    padding-right: calc(0.75em - 1px);
+    padding-top: calc(0.5em - 1px);
+    position: relative;
+    vertical-align: top; }
+    .sling-cms-editor .button:focus, .sling-cms-editor .input:focus, .sling-cms-editor .textarea:focus, .sling-cms-editor .select select:focus, .sling-cms-editor .file-cta:focus,
+    .sling-cms-editor .file-name:focus, .sling-cms-editor .is-focused.button, .sling-cms-editor .is-focused.input, .sling-cms-editor .is-focused.textarea, .sling-cms-editor .select select.is-focused, .sling-cms-editor .is-focused.file-cta,
+    .sling-cms-editor .is-focused.file-name, .sling-cms-editor .button:active, .sling-cms-editor .input:active, .sling-cms-editor .textarea:active, .sling-cms-editor .select select:active, .sling-cms-editor .file-cta:active,
+    .sling-cms-editor .file-name:active, .sling-cms-editor .is-active.button, .sling-cms-editor .is-active.input, .sling-cms-editor .is-active.textarea, .sling-cms-editor .select select.is-active, .sling-cms-editor .is-active.file-cta,
+    .sling-cms-editor .is-active.file-name {
+      outline: none; }
+    .sling-cms-editor .button[disabled], .sling-cms-editor .input[disabled], .sling-cms-editor .textarea[disabled], .sling-cms-editor .select select[disabled], .sling-cms-editor .file-cta[disabled],
+    .sling-cms-editor .file-name[disabled],
+    fieldset[disabled] .sling-cms-editor .button,
+    fieldset[disabled] .sling-cms-editor .input,
+    fieldset[disabled] .sling-cms-editor .textarea,
+    fieldset[disabled] .sling-cms-editor .select select,
+    fieldset[disabled] .sling-cms-editor .file-cta,
+    fieldset[disabled] .sling-cms-editor .file-name {
+      cursor: not-allowed; }
+  .sling-cms-editor html,
+  .sling-cms-editor body,
+  .sling-cms-editor p,
+  .sling-cms-editor ol,
+  .sling-cms-editor ul,
+  .sling-cms-editor li,
+  .sling-cms-editor dl,
+  .sling-cms-editor dt,
+  .sling-cms-editor dd,
+  .sling-cms-editor blockquote,
+  .sling-cms-editor figure,
+  .sling-cms-editor fieldset,
+  .sling-cms-editor legend,
+  .sling-cms-editor textarea,
+  .sling-cms-editor pre,
+  .sling-cms-editor iframe,
+  .sling-cms-editor hr,
+  .sling-cms-editor h1,
+  .sling-cms-editor h2,
+  .sling-cms-editor h3,
+  .sling-cms-editor h4,
+  .sling-cms-editor h5,
+  .sling-cms-editor h6 {
+    margin: 0;
+    padding: 0; }
+  .sling-cms-editor h1,
+  .sling-cms-editor h2,
+  .sling-cms-editor h3,
+  .sling-cms-editor h4,
+  .sling-cms-editor h5,
+  .sling-cms-editor h6 {
+    font-size: 100%;
+    font-weight: normal; }
+  .sling-cms-editor ul {
+    list-style: none; }
+  .sling-cms-editor button,
+  .sling-cms-editor input,
+  .sling-cms-editor select,
+  .sling-cms-editor textarea {
+    margin: 0; }
+  .sling-cms-editor html {
+    box-sizing: border-box; }
+  .sling-cms-editor *, .sling-cms-editor *::before, .sling-cms-editor *::after {
+    box-sizing: inherit; }
+  .sling-cms-editor img,
+  .sling-cms-editor video {
+    height: auto;
+    max-width: 100%; }
+  .sling-cms-editor iframe {
+    border: 0; }
+  .sling-cms-editor table {
+    border-collapse: collapse;
+    border-spacing: 0; }
+  .sling-cms-editor td,
+  .sling-cms-editor th {
+    padding: 0; }
+    .sling-cms-editor td:not([align]),
+    .sling-cms-editor th:not([align]) {
+      text-align: left; }
+  .sling-cms-editor html {
+    background-color: white;
+    font-size: 16px;
+    -moz-osx-font-smoothing: grayscale;
+    -webkit-font-smoothing: antialiased;
+    min-width: 300px;
+    overflow-x: hidden;
+    overflow-y: scroll;
+    text-rendering: optimizeLegibility;
+    text-size-adjust: 100%; }
+  .sling-cms-editor article,
+  .sling-cms-editor aside,
+  .sling-cms-editor figure,
+  .sling-cms-editor footer,
+  .sling-cms-editor header,
+  .sling-cms-editor hgroup,
+  .sling-cms-editor section {
+    display: block; }
+  .sling-cms-editor body,
+  .sling-cms-editor button,
+  .sling-cms-editor input,
+  .sling-cms-editor select,
+  .sling-cms-editor textarea {
+    font-family: "Open Sans", sans-serif; }
+  .sling-cms-editor code,
+  .sling-cms-editor pre {
+    -moz-osx-font-smoothing: auto;
+    -webkit-font-smoothing: auto;
+    font-family: monospace; }
+  .sling-cms-editor body {
+    color: #4a4a4a;
+    font-size: 1em;
+    font-weight: 400;
+    line-height: 1.5; }
+  .sling-cms-editor a {
+    color: #282661;
+    cursor: pointer;
+    text-decoration: none; }
+    .sling-cms-editor a strong {
+      color: currentColor; }
+    .sling-cms-editor a:hover {
+      color: #363636; }
+  .sling-cms-editor code {
+    background-color: whitesmoke;
+    color: #D22128;
+    font-size: 0.875em;
+    font-weight: normal;
+    padding: 0.25em 0.5em 0.25em; }
+  .sling-cms-editor hr {
+    background-color: whitesmoke;
+    border: none;
+    display: block;
+    height: 2px;
+    margin: 1.5rem 0; }
+  .sling-cms-editor img {
+    height: auto;
+    max-width: 100%; }
+  .sling-cms-editor input[type="checkbox"],
+  .sling-cms-editor input[type="radio"] {
+    vertical-align: baseline; }
+  .sling-cms-editor small {
+    font-size: 0.875em; }
+  .sling-cms-editor span {
+    font-style: inherit;
+    font-weight: inherit; }
+  .sling-cms-editor strong {
+    color: #363636;
+    font-weight: 700; }
+  .sling-cms-editor fieldset {
+    border: none; }
+  .sling-cms-editor pre {
+    -webkit-overflow-scrolling: touch;
+    background-color: whitesmoke;
+    color: #4a4a4a;
+    font-size: 0.875em;
+    overflow-x: auto;
+    padding: 1.25rem 1.5rem;
+    white-space: pre;
+    word-wrap: normal; }
+    .sling-cms-editor pre code {
+      background-color: transparent;
+      color: currentColor;
+      font-size: 1em;
+      padding: 0; }
+  .sling-cms-editor table td,
+  .sling-cms-editor table th {
+    vertical-align: top; }
+    .sling-cms-editor table td:not([align]),
+    .sling-cms-editor table th:not([align]) {
+      text-align: left; }
+  .sling-cms-editor table th {
+    color: #363636; }
+  .sling-cms-editor .is-clearfix::after {
+    clear: both;
+    content: " ";
+    display: table; }
+  .sling-cms-editor .is-pulled-left {
+    float: left !important; }
+  .sling-cms-editor .is-pulled-right {
+    float: right !important; }
+  .sling-cms-editor .is-clipped {
+    overflow: hidden !important; }
+  .sling-cms-editor .is-size-1 {
+    font-size: 3rem !important; }
+  .sling-cms-editor .is-size-2 {
+    font-size: 2.5rem !important; }
+  .sling-cms-editor .is-size-3 {
+    font-size: 2rem !important; }
+  .sling-cms-editor .is-size-4 {
+    font-size: 1.5rem !important; }
+  .sling-cms-editor .is-size-5 {
+    font-size: 1.25rem !important; }
+  .sling-cms-editor .is-size-6 {
+    font-size: 1rem !important; }
+  .sling-cms-editor .is-size-7 {
+    font-size: 0.75rem !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-size-1-mobile {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-mobile {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-mobile {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-mobile {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-mobile {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-mobile {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-mobile {
+      font-size: 0.75rem !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-size-1-tablet {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-tablet {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-tablet {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-tablet {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-tablet {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-tablet {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-tablet {
+      font-size: 0.75rem !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-size-1-touch {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-touch {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-touch {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-touch {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-touch {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-touch {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-touch {
+      font-size: 0.75rem !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-size-1-desktop {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-desktop {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-desktop {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-desktop {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-desktop {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-desktop {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-desktop {
+      font-size: 0.75rem !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-size-1-widescreen {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-widescreen {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-widescreen {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-widescreen {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-widescreen {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-widescreen {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-widescreen {
+      font-size: 0.75rem !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-size-1-fullhd {
+      font-size: 3rem !important; }
+    .sling-cms-editor .is-size-2-fullhd {
+      font-size: 2.5rem !important; }
+    .sling-cms-editor .is-size-3-fullhd {
+      font-size: 2rem !important; }
+    .sling-cms-editor .is-size-4-fullhd {
+      font-size: 1.5rem !important; }
+    .sling-cms-editor .is-size-5-fullhd {
+      font-size: 1.25rem !important; }
+    .sling-cms-editor .is-size-6-fullhd {
+      font-size: 1rem !important; }
+    .sling-cms-editor .is-size-7-fullhd {
+      font-size: 0.75rem !important; } }
+  .sling-cms-editor .has-text-centered {
+    text-align: center !important; }
+  .sling-cms-editor .has-text-justified {
+    text-align: justify !important; }
+  .sling-cms-editor .has-text-left {
+    text-align: left !important; }
+  .sling-cms-editor .has-text-right {
+    text-align: right !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .has-text-centered-mobile {
+      text-align: center !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .has-text-centered-tablet {
+      text-align: center !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .has-text-centered-tablet-only {
+      text-align: center !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .has-text-centered-touch {
+      text-align: center !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .has-text-centered-desktop {
+      text-align: center !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .has-text-centered-desktop-only {
+      text-align: center !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .has-text-centered-widescreen {
+      text-align: center !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .has-text-centered-widescreen-only {
+      text-align: center !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .has-text-centered-fullhd {
+      text-align: center !important; } }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .has-text-justified-mobile {
+      text-align: justify !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .has-text-justified-tablet {
+      text-align: justify !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .has-text-justified-tablet-only {
+      text-align: justify !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .has-text-justified-touch {
+      text-align: justify !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .has-text-justified-desktop {
+      text-align: justify !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .has-text-justified-desktop-only {
+      text-align: justify !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .has-text-justified-widescreen {
+      text-align: justify !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .has-text-justified-widescreen-only {
+      text-align: justify !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .has-text-justified-fullhd {
+      text-align: justify !important; } }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .has-text-left-mobile {
+      text-align: left !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .has-text-left-tablet {
+      text-align: left !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .has-text-left-tablet-only {
+      text-align: left !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .has-text-left-touch {
+      text-align: left !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .has-text-left-desktop {
+      text-align: left !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .has-text-left-desktop-only {
+      text-align: left !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .has-text-left-widescreen {
+      text-align: left !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .has-text-left-widescreen-only {
+      text-align: left !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .has-text-left-fullhd {
+      text-align: left !important; } }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .has-text-right-mobile {
+      text-align: right !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .has-text-right-tablet {
+      text-align: right !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .has-text-right-tablet-only {
+      text-align: right !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .has-text-right-touch {
+      text-align: right !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .has-text-right-desktop {
+      text-align: right !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .has-text-right-desktop-only {
+      text-align: right !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .has-text-right-widescreen {
+      text-align: right !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .has-text-right-widescreen-only {
+      text-align: right !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .has-text-right-fullhd {
+      text-align: right !important; } }
+  .sling-cms-editor .is-capitalized {
+    text-transform: capitalize !important; }
+  .sling-cms-editor .is-lowercase {
+    text-transform: lowercase !important; }
+  .sling-cms-editor .is-uppercase {
+    text-transform: uppercase !important; }
+  .sling-cms-editor .is-italic {
+    font-style: italic !important; }
+  .sling-cms-editor .has-text-white {
+    color: white !important; }
+  .sling-cms-editor a.has-text-white:hover, .sling-cms-editor a.has-text-white:focus {
+    color: #e6e6e6 !important; }
+  .sling-cms-editor .has-background-white {
+    background-color: white !important; }
+  .sling-cms-editor .has-text-black {
+    color: #0a0a0a !important; }
+  .sling-cms-editor a.has-text-black:hover, .sling-cms-editor a.has-text-black:focus {
+    color: black !important; }
+  .sling-cms-editor .has-background-black {
+    background-color: #0a0a0a !important; }
+  .sling-cms-editor .has-text-light {
+    color: whitesmoke !important; }
+  .sling-cms-editor a.has-text-light:hover, .sling-cms-editor a.has-text-light:focus {
+    color: #dbdbdb !important; }
+  .sling-cms-editor .has-background-light {
+    background-color: whitesmoke !important; }
+  .sling-cms-editor .has-text-dark {
+    color: #363636 !important; }
+  .sling-cms-editor a.has-text-dark:hover, .sling-cms-editor a.has-text-dark:focus {
+    color: #1c1c1c !important; }
+  .sling-cms-editor .has-background-dark {
+    background-color: #363636 !important; }
+  .sling-cms-editor .has-text-primary {
+    color: #9E2165 !important; }
+  .sling-cms-editor a.has-text-primary:hover, .sling-cms-editor a.has-text-primary:focus {
+    color: #74184a !important; }
+  .sling-cms-editor .has-background-primary {
+    background-color: #9E2165 !important; }
+  .sling-cms-editor .has-text-link {
+    color: #282661 !important; }
+  .sling-cms-editor a.has-text-link:hover, .sling-cms-editor a.has-text-link:focus {
+    color: #19183c !important; }
+  .sling-cms-editor .has-background-link {
+    background-color: #282661 !important; }
+  .sling-cms-editor .has-text-info {
+    color: #3298dc !important; }
+  .sling-cms-editor a.has-text-info:hover, .sling-cms-editor a.has-text-info:focus {
+    color: #207dbc !important; }
+  .sling-cms-editor .has-background-info {
+    background-color: #3298dc !important; }
+  .sling-cms-editor .has-text-success {
+    color: #48c774 !important; }
+  .sling-cms-editor a.has-text-success:hover, .sling-cms-editor a.has-text-success:focus {
+    color: #34a85c !important; }
+  .sling-cms-editor .has-background-success {
+    background-color: #48c774 !important; }
+  .sling-cms-editor .has-text-warning {
+    color: #EA7826 !important; }
+  .sling-cms-editor a.has-text-warning:hover, .sling-cms-editor a.has-text-warning:focus {
+    color: #ca6014 !important; }
+  .sling-cms-editor .has-background-warning {
+    background-color: #EA7826 !important; }
+  .sling-cms-editor .has-text-danger {
+    color: #CB2138 !important; }
+  .sling-cms-editor a.has-text-danger:hover, .sling-cms-editor a.has-text-danger:focus {
+    color: #9f1a2c !important; }
+  .sling-cms-editor .has-background-danger {
+    background-color: #CB2138 !important; }
+  .sling-cms-editor .has-text-black-bis {
+    color: #121212 !important; }
+  .sling-cms-editor .has-background-black-bis {
+    background-color: #121212 !important; }
+  .sling-cms-editor .has-text-black-ter {
+    color: #242424 !important; }
+  .sling-cms-editor .has-background-black-ter {
+    background-color: #242424 !important; }
+  .sling-cms-editor .has-text-grey-darker {
+    color: #363636 !important; }
+  .sling-cms-editor .has-background-grey-darker {
+    background-color: #363636 !important; }
+  .sling-cms-editor .has-text-grey-dark {
+    color: #4a4a4a !important; }
+  .sling-cms-editor .has-background-grey-dark {
+    background-color: #4a4a4a !important; }
+  .sling-cms-editor .has-text-grey {
+    color: #7a7a7a !important; }
+  .sling-cms-editor .has-background-grey {
+    background-color: #7a7a7a !important; }
+  .sling-cms-editor .has-text-grey-light {
+    color: #b5b5b5 !important; }
+  .sling-cms-editor .has-background-grey-light {
+    background-color: #b5b5b5 !important; }
+  .sling-cms-editor .has-text-grey-lighter {
+    color: #dbdbdb !important; }
+  .sling-cms-editor .has-background-grey-lighter {
+    background-color: #dbdbdb !important; }
+  .sling-cms-editor .has-text-white-ter {
+    color: whitesmoke !important; }
+  .sling-cms-editor .has-background-white-ter {
+    background-color: whitesmoke !important; }
+  .sling-cms-editor .has-text-white-bis {
+    color: #fafafa !important; }
+  .sling-cms-editor .has-background-white-bis {
+    background-color: #fafafa !important; }
+  .sling-cms-editor .has-text-weight-light {
+    font-weight: 300 !important; }
+  .sling-cms-editor .has-text-weight-normal {
+    font-weight: 400 !important; }
+  .sling-cms-editor .has-text-weight-medium {
+    font-weight: 500 !important; }
+  .sling-cms-editor .has-text-weight-semibold {
+    font-weight: 600 !important; }
+  .sling-cms-editor .has-text-weight-bold {
+    font-weight: 700 !important; }
+  .sling-cms-editor .is-family-primary {
+    font-family: "Open Sans", sans-serif !important; }
+  .sling-cms-editor .is-family-secondary {
+    font-family: "Open Sans", sans-serif !important; }
+  .sling-cms-editor .is-family-sans-serif {
+    font-family: "Open Sans", sans-serif !important; }
+  .sling-cms-editor .is-family-monospace {
+    font-family: monospace !important; }
+  .sling-cms-editor .is-family-code {
+    font-family: monospace !important; }
+  .sling-cms-editor .is-block {
+    display: block !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-block-mobile {
+      display: block !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-block-tablet {
+      display: block !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-block-tablet-only {
+      display: block !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-block-touch {
+      display: block !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-block-desktop {
+      display: block !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-block-desktop-only {
+      display: block !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-block-widescreen {
+      display: block !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-block-widescreen-only {
+      display: block !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-block-fullhd {
+      display: block !important; } }
+  .sling-cms-editor .is-flex {
+    display: flex !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-flex-mobile {
+      display: flex !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-flex-tablet {
+      display: flex !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-flex-tablet-only {
+      display: flex !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-flex-touch {
+      display: flex !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-flex-desktop {
+      display: flex !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-flex-desktop-only {
+      display: flex !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-flex-widescreen {
+      display: flex !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-flex-widescreen-only {
+      display: flex !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-flex-fullhd {
+      display: flex !important; } }
+  .sling-cms-editor .is-inline {
+    display: inline !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-inline-mobile {
+      display: inline !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-inline-tablet {
+      display: inline !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-tablet-only {
+      display: inline !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-touch {
+      display: inline !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-inline-desktop {
+      display: inline !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-inline-desktop-only {
+      display: inline !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-inline-widescreen {
+      display: inline !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-inline-widescreen-only {
+      display: inline !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-inline-fullhd {
+      display: inline !important; } }
+  .sling-cms-editor .is-inline-block {
+    display: inline-block !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-inline-block-mobile {
+      display: inline-block !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-inline-block-tablet {
+      display: inline-block !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-block-tablet-only {
+      display: inline-block !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-block-touch {
+      display: inline-block !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-inline-block-desktop {
+      display: inline-block !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-inline-block-desktop-only {
+      display: inline-block !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-inline-block-widescreen {
+      display: inline-block !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-inline-block-widescreen-only {
+      display: inline-block !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-inline-block-fullhd {
+      display: inline-block !important; } }
+  .sling-cms-editor .is-inline-flex {
+    display: inline-flex !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-inline-flex-mobile {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-inline-flex-tablet {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-flex-tablet-only {
+      display: inline-flex !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-inline-flex-touch {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-inline-flex-desktop {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-inline-flex-desktop-only {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-inline-flex-widescreen {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-inline-flex-widescreen-only {
+      display: inline-flex !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-inline-flex-fullhd {
+      display: inline-flex !important; } }
+  .sling-cms-editor .is-hidden {
+    display: none !important; }
+  .sling-cms-editor .is-sr-only {
+    border: none !important;
+    clip: rect(0, 0, 0, 0) !important;
+    height: 0.01em !important;
+    overflow: hidden !important;
+    padding: 0 !important;
+    position: absolute !important;
+    white-space: nowrap !important;
+    width: 0.01em !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-hidden-mobile {
+      display: none !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-hidden-tablet {
+      display: none !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-hidden-tablet-only {
+      display: none !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-hidden-touch {
+      display: none !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-hidden-desktop {
+      display: none !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-hidden-desktop-only {
+      display: none !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-hidden-widescreen {
+      display: none !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-hidden-widescreen-only {
+      display: none !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-hidden-fullhd {
+      display: none !important; } }
+  .sling-cms-editor .is-invisible {
+    visibility: hidden !important; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .is-invisible-mobile {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .is-invisible-tablet {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 769px) and (max-width: 1023px) {
+    .sling-cms-editor .is-invisible-tablet-only {
+      visibility: hidden !important; } }
+  @media screen and (max-width: 1023px) {
+    .sling-cms-editor .is-invisible-touch {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 1024px) {
+    .sling-cms-editor .is-invisible-desktop {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 1024px) and (max-width: 1215px) {
+    .sling-cms-editor .is-invisible-desktop-only {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 1216px) {
+    .sling-cms-editor .is-invisible-widescreen {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 1216px) and (max-width: 1407px) {
+    .sling-cms-editor .is-invisible-widescreen-only {
+      visibility: hidden !important; } }
+  @media screen and (min-width: 1408px) {
+    .sling-cms-editor .is-invisible-fullhd {
+      visibility: hidden !important; } }
+  .sling-cms-editor .is-marginless {
+    margin: 0 !important; }
+  .sling-cms-editor .is-paddingless {
+    padding: 0 !important; }
+  .sling-cms-editor .is-radiusless {
+    border-radius: 0 !important; }
+  .sling-cms-editor .is-shadowless {
+    box-shadow: none !important; }
+  .sling-cms-editor .is-relative {
+    position: relative !important; }
+  .sling-cms-editor .box {
+    background-color: white;
+    border-radius: 6px;
+    box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0px 0 1px rgba(10, 10, 10, 0.02);
+    color: #4a4a4a;
+    display: block;
+    padding: 1.25rem; }
+  .sling-cms-editor a.box:hover, .sling-cms-editor a.box:focus {
+    box-shadow: 0 0.5em 1em -0.125em rgba(10, 10, 10, 0.1), 0 0 0 1px #282661; }
+  .sling-cms-editor a.box:active {
+    box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.2), 0 0 0 1px #282661; }
+  .sling-cms-editor .button {
+    background-color: white;
+    border-color: #dbdbdb;
+    border-width: 1px;
+    color: #363636;
+    cursor: pointer;
+    justify-content: center;
+    padding-bottom: calc(0.5em - 1px);
+    padding-left: 1em;
+    padding-right: 1em;
+    padding-top: calc(0.5em - 1px);
+    text-align: center;
+    white-space: nowrap; }
+    .sling-cms-editor .button strong {
+      color: inherit; }
+    .sling-cms-editor .button .icon, .sling-cms-editor .button .icon.is-small, .sling-cms-editor .button .icon.is-medium, .sling-cms-editor .button .icon.is-large {
+      height: 1.5em;
+      width: 1.5em; }
+    .sling-cms-editor .button .icon:first-child:not(:last-child) {
+      margin-left: calc(-0.5em - 1px);
+      margin-right: 0.25em; }
+    .sling-cms-editor .button .icon:last-child:not(:first-child) {
+      margin-left: 0.25em;
+      margin-right: calc(-0.5em - 1px); }
+    .sling-cms-editor .button .icon:first-child:last-child {
+      margin-left: calc(-0.5em - 1px);
+      margin-right: calc(-0.5em - 1px); }
+    .sling-cms-editor .button:hover, .sling-cms-editor .button.is-hovered {
+      border-color: #b5b5b5;
+      color: #363636; }
+    .sling-cms-editor .button:focus, .sling-cms-editor .button.is-focused {
+      border-color: #3273dc;
+      color: #363636; }
+      .sling-cms-editor .button:focus:not(:active), .sling-cms-editor .button.is-focused:not(:active) {
+        box-shadow: 0 0 0 0.125em rgba(40, 38, 97, 0.25); }
+    .sling-cms-editor .button:active, .sling-cms-editor .button.is-active {
+      border-color: #4a4a4a;
+      color: #363636; }
+    .sling-cms-editor .button.is-text {
+      background-color: transparent;
+      border-color: transparent;
+      color: #4a4a4a;
+      text-decoration: underline; }
+      .sling-cms-editor .button.is-text:hover, .sling-cms-editor .button.is-text.is-hovered, .sling-cms-editor .button.is-text:focus, .sling-cms-editor .button.is-text.is-focused {
+        background-color: whitesmoke;
+        color: #363636; }
+      .sling-cms-editor .button.is-text:active, .sling-cms-editor .button.is-text.is-active {
+        background-color: #e8e8e8;
+        color: #363636; }
+      .sling-cms-editor .button.is-text[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-text {
+        background-color: transparent;
+        border-color: transparent;
+        box-shadow: none; }
+    .sling-cms-editor .button.is-white {
+      background-color: white;
+      border-color: transparent;
+      color: #0a0a0a; }
+      .sling-cms-editor .button.is-white:hover, .sling-cms-editor .button.is-white.is-hovered {
+        background-color: #f9f9f9;
+        border-color: transparent;
+        color: #0a0a0a; }
+      .sling-cms-editor .button.is-white:focus, .sling-cms-editor .button.is-white.is-focused {
+        border-color: transparent;
+        color: #0a0a0a; }
+        .sling-cms-editor .button.is-white:focus:not(:active), .sling-cms-editor .button.is-white.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); }
+      .sling-cms-editor .button.is-white:active, .sling-cms-editor .button.is-white.is-active {
+        background-color: #f2f2f2;
+        border-color: transparent;
+        color: #0a0a0a; }
+      .sling-cms-editor .button.is-white[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-white {
+        background-color: white;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-white.is-inverted {
+        background-color: #0a0a0a;
+        color: white; }
+        .sling-cms-editor .button.is-white.is-inverted:hover, .sling-cms-editor .button.is-white.is-inverted.is-hovered {
+          background-color: black; }
+        .sling-cms-editor .button.is-white.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-white.is-inverted {
+          background-color: #0a0a0a;
+          border-color: transparent;
+          box-shadow: none;
+          color: white; }
+      .sling-cms-editor .button.is-white.is-loading::after {
+        border-color: transparent transparent #0a0a0a #0a0a0a !important; }
+      .sling-cms-editor .button.is-white.is-outlined {
+        background-color: transparent;
+        border-color: white;
+        color: white; }
+        .sling-cms-editor .button.is-white.is-outlined:hover, .sling-cms-editor .button.is-white.is-outlined.is-hovered, .sling-cms-editor .button.is-white.is-outlined:focus, .sling-cms-editor .button.is-white.is-outlined.is-focused {
+          background-color: white;
+          border-color: white;
+          color: #0a0a0a; }
+        .sling-cms-editor .button.is-white.is-outlined.is-loading::after {
+          border-color: transparent transparent white white !important; }
+        .sling-cms-editor .button.is-white.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-white.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-white.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-white.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #0a0a0a #0a0a0a !important; }
+        .sling-cms-editor .button.is-white.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-white.is-outlined {
+          background-color: transparent;
+          border-color: white;
+          box-shadow: none;
+          color: white; }
+      .sling-cms-editor .button.is-white.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #0a0a0a;
+        color: #0a0a0a; }
+        .sling-cms-editor .button.is-white.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-white.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-focused {
+          background-color: #0a0a0a;
+          color: white; }
+        .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-white.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent white white !important; }
+        .sling-cms-editor .button.is-white.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-white.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #0a0a0a;
+          box-shadow: none;
+          color: #0a0a0a; }
+    .sling-cms-editor .button.is-black {
+      background-color: #0a0a0a;
+      border-color: transparent;
+      color: white; }
+      .sling-cms-editor .button.is-black:hover, .sling-cms-editor .button.is-black.is-hovered {
+        background-color: #040404;
+        border-color: transparent;
+        color: white; }
+      .sling-cms-editor .button.is-black:focus, .sling-cms-editor .button.is-black.is-focused {
+        border-color: transparent;
+        color: white; }
+        .sling-cms-editor .button.is-black:focus:not(:active), .sling-cms-editor .button.is-black.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); }
+      .sling-cms-editor .button.is-black:active, .sling-cms-editor .button.is-black.is-active {
+        background-color: black;
+        border-color: transparent;
+        color: white; }
+      .sling-cms-editor .button.is-black[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-black {
+        background-color: #0a0a0a;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-black.is-inverted {
+        background-color: white;
+        color: #0a0a0a; }
+        .sling-cms-editor .button.is-black.is-inverted:hover, .sling-cms-editor .button.is-black.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-black.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-black.is-inverted {
+          background-color: white;
+          border-color: transparent;
+          box-shadow: none;
+          color: #0a0a0a; }
+      .sling-cms-editor .button.is-black.is-loading::after {
+        border-color: transparent transparent white white !important; }
+      .sling-cms-editor .button.is-black.is-outlined {
+        background-color: transparent;
+        border-color: #0a0a0a;
+        color: #0a0a0a; }
+        .sling-cms-editor .button.is-black.is-outlined:hover, .sling-cms-editor .button.is-black.is-outlined.is-hovered, .sling-cms-editor .button.is-black.is-outlined:focus, .sling-cms-editor .button.is-black.is-outlined.is-focused {
+          background-color: #0a0a0a;
+          border-color: #0a0a0a;
+          color: white; }
+        .sling-cms-editor .button.is-black.is-outlined.is-loading::after {
+          border-color: transparent transparent #0a0a0a #0a0a0a !important; }
+        .sling-cms-editor .button.is-black.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-black.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-black.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-black.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent white white !important; }
+        .sling-cms-editor .button.is-black.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-black.is-outlined {
+          background-color: transparent;
+          border-color: #0a0a0a;
+          box-shadow: none;
+          color: #0a0a0a; }
+      .sling-cms-editor .button.is-black.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: white;
+        color: white; }
+        .sling-cms-editor .button.is-black.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-black.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-focused {
+          background-color: white;
+          color: #0a0a0a; }
+        .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-black.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #0a0a0a #0a0a0a !important; }
+        .sling-cms-editor .button.is-black.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-black.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: white;
+          box-shadow: none;
+          color: white; }
+    .sling-cms-editor .button.is-light {
+      background-color: whitesmoke;
+      border-color: transparent;
+      color: rgba(0, 0, 0, 0.7); }
+      .sling-cms-editor .button.is-light:hover, .sling-cms-editor .button.is-light.is-hovered {
+        background-color: #eeeeee;
+        border-color: transparent;
+        color: rgba(0, 0, 0, 0.7); }
+      .sling-cms-editor .button.is-light:focus, .sling-cms-editor .button.is-light.is-focused {
+        border-color: transparent;
+        color: rgba(0, 0, 0, 0.7); }
+        .sling-cms-editor .button.is-light:focus:not(:active), .sling-cms-editor .button.is-light.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); }
+      .sling-cms-editor .button.is-light:active, .sling-cms-editor .button.is-light.is-active {
+        background-color: #e8e8e8;
+        border-color: transparent;
+        color: rgba(0, 0, 0, 0.7); }
+      .sling-cms-editor .button.is-light[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-light {
+        background-color: whitesmoke;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-light.is-inverted {
+        background-color: rgba(0, 0, 0, 0.7);
+        color: whitesmoke; }
+        .sling-cms-editor .button.is-light.is-inverted:hover, .sling-cms-editor .button.is-light.is-inverted.is-hovered {
+          background-color: rgba(0, 0, 0, 0.7); }
+        .sling-cms-editor .button.is-light.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-light.is-inverted {
+          background-color: rgba(0, 0, 0, 0.7);
+          border-color: transparent;
+          box-shadow: none;
+          color: whitesmoke; }
+      .sling-cms-editor .button.is-light.is-loading::after {
+        border-color: transparent transparent rgba(0, 0, 0, 0.7) rgba(0, 0, 0, 0.7) !important; }
+      .sling-cms-editor .button.is-light.is-outlined {
+        background-color: transparent;
+        border-color: whitesmoke;
+        color: whitesmoke; }
+        .sling-cms-editor .button.is-light.is-outlined:hover, .sling-cms-editor .button.is-light.is-outlined.is-hovered, .sling-cms-editor .button.is-light.is-outlined:focus, .sling-cms-editor .button.is-light.is-outlined.is-focused {
+          background-color: whitesmoke;
+          border-color: whitesmoke;
+          color: rgba(0, 0, 0, 0.7); }
+        .sling-cms-editor .button.is-light.is-outlined.is-loading::after {
+          border-color: transparent transparent whitesmoke whitesmoke !important; }
+        .sling-cms-editor .button.is-light.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-light.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-light.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-light.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent rgba(0, 0, 0, 0.7) rgba(0, 0, 0, 0.7) !important; }
+        .sling-cms-editor .button.is-light.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-light.is-outlined {
+          background-color: transparent;
+          border-color: whitesmoke;
+          box-shadow: none;
+          color: whitesmoke; }
+      .sling-cms-editor .button.is-light.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: rgba(0, 0, 0, 0.7);
+        color: rgba(0, 0, 0, 0.7); }
+        .sling-cms-editor .button.is-light.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-light.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-focused {
+          background-color: rgba(0, 0, 0, 0.7);
+          color: whitesmoke; }
+        .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-light.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent whitesmoke whitesmoke !important; }
+        .sling-cms-editor .button.is-light.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-light.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: rgba(0, 0, 0, 0.7);
+          box-shadow: none;
+          color: rgba(0, 0, 0, 0.7); }
+    .sling-cms-editor .button.is-dark {
+      background-color: #363636;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-dark:hover, .sling-cms-editor .button.is-dark.is-hovered {
+        background-color: #2f2f2f;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-dark:focus, .sling-cms-editor .button.is-dark.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-dark:focus:not(:active), .sling-cms-editor .button.is-dark.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); }
+      .sling-cms-editor .button.is-dark:active, .sling-cms-editor .button.is-dark.is-active {
+        background-color: #292929;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-dark[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-dark {
+        background-color: #363636;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-dark.is-inverted {
+        background-color: #fff;
+        color: #363636; }
+        .sling-cms-editor .button.is-dark.is-inverted:hover, .sling-cms-editor .button.is-dark.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-dark.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-dark.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #363636; }
+      .sling-cms-editor .button.is-dark.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-dark.is-outlined {
+        background-color: transparent;
+        border-color: #363636;
+        color: #363636; }
+        .sling-cms-editor .button.is-dark.is-outlined:hover, .sling-cms-editor .button.is-dark.is-outlined.is-hovered, .sling-cms-editor .button.is-dark.is-outlined:focus, .sling-cms-editor .button.is-dark.is-outlined.is-focused {
+          background-color: #363636;
+          border-color: #363636;
+          color: #fff; }
+        .sling-cms-editor .button.is-dark.is-outlined.is-loading::after {
+          border-color: transparent transparent #363636 #363636 !important; }
+        .sling-cms-editor .button.is-dark.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-dark.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-dark.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-dark.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-dark.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-dark.is-outlined {
+          background-color: transparent;
+          border-color: #363636;
+          box-shadow: none;
+          color: #363636; }
+      .sling-cms-editor .button.is-dark.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-dark.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-dark.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #363636; }
+        .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-dark.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #363636 #363636 !important; }
+        .sling-cms-editor .button.is-dark.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-dark.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+    .sling-cms-editor .button.is-primary {
+      background-color: #9E2165;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-primary:hover, .sling-cms-editor .button.is-primary.is-hovered {
+        background-color: #931f5e;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-primary:focus, .sling-cms-editor .button.is-primary.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-primary:focus:not(:active), .sling-cms-editor .button.is-primary.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(158, 33, 101, 0.25); }
+      .sling-cms-editor .button.is-primary:active, .sling-cms-editor .button.is-primary.is-active {
+        background-color: #891d58;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-primary[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-primary {
+        background-color: #9E2165;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-primary.is-inverted {
+        background-color: #fff;
+        color: #9E2165; }
+        .sling-cms-editor .button.is-primary.is-inverted:hover, .sling-cms-editor .button.is-primary.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-primary.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-primary.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #9E2165; }
+      .sling-cms-editor .button.is-primary.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-primary.is-outlined {
+        background-color: transparent;
+        border-color: #9E2165;
+        color: #9E2165; }
+        .sling-cms-editor .button.is-primary.is-outlined:hover, .sling-cms-editor .button.is-primary.is-outlined.is-hovered, .sling-cms-editor .button.is-primary.is-outlined:focus, .sling-cms-editor .button.is-primary.is-outlined.is-focused {
+          background-color: #9E2165;
+          border-color: #9E2165;
+          color: #fff; }
+        .sling-cms-editor .button.is-primary.is-outlined.is-loading::after {
+          border-color: transparent transparent #9E2165 #9E2165 !important; }
+        .sling-cms-editor .button.is-primary.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-primary.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-primary.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-primary.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-primary.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-primary.is-outlined {
+          background-color: transparent;
+          border-color: #9E2165;
+          box-shadow: none;
+          color: #9E2165; }
+      .sling-cms-editor .button.is-primary.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-primary.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-primary.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #9E2165; }
+        .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-primary.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #9E2165 #9E2165 !important; }
+        .sling-cms-editor .button.is-primary.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-primary.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-primary.is-light {
+        background-color: #fbeef5;
+        color: #d43089; }
+        .sling-cms-editor .button.is-primary.is-light:hover, .sling-cms-editor .button.is-primary.is-light.is-hovered {
+          background-color: #f9e4ef;
+          border-color: transparent;
+          color: #d43089; }
+        .sling-cms-editor .button.is-primary.is-light:active, .sling-cms-editor .button.is-primary.is-light.is-active {
+          background-color: #f7d9e9;
+          border-color: transparent;
+          color: #d43089; }
+    .sling-cms-editor .button.is-link {
+      background-color: #282661;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-link:hover, .sling-cms-editor .button.is-link.is-hovered {
+        background-color: #242258;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-link:focus, .sling-cms-editor .button.is-link.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-link:focus:not(:active), .sling-cms-editor .button.is-link.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(40, 38, 97, 0.25); }
+      .sling-cms-editor .button.is-link:active, .sling-cms-editor .button.is-link.is-active {
+        background-color: #201f4f;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-link[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-link {
+        background-color: #282661;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-link.is-inverted {
+        background-color: #fff;
+        color: #282661; }
+        .sling-cms-editor .button.is-link.is-inverted:hover, .sling-cms-editor .button.is-link.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-link.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-link.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #282661; }
+      .sling-cms-editor .button.is-link.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-link.is-outlined {
+        background-color: transparent;
+        border-color: #282661;
+        color: #282661; }
+        .sling-cms-editor .button.is-link.is-outlined:hover, .sling-cms-editor .button.is-link.is-outlined.is-hovered, .sling-cms-editor .button.is-link.is-outlined:focus, .sling-cms-editor .button.is-link.is-outlined.is-focused {
+          background-color: #282661;
+          border-color: #282661;
+          color: #fff; }
+        .sling-cms-editor .button.is-link.is-outlined.is-loading::after {
+          border-color: transparent transparent #282661 #282661 !important; }
+        .sling-cms-editor .button.is-link.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-link.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-link.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-link.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-link.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-link.is-outlined {
+          background-color: transparent;
+          border-color: #282661;
+          box-shadow: none;
+          color: #282661; }
+      .sling-cms-editor .button.is-link.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-link.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-link.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #282661; }
+        .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-link.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #282661 #282661 !important; }
+        .sling-cms-editor .button.is-link.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-link.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-link.is-light {
+        background-color: #f1f0f9;
+        color: #5e5abe; }
+        .sling-cms-editor .button.is-link.is-light:hover, .sling-cms-editor .button.is-link.is-light.is-hovered {
+          background-color: #e8e7f6;
+          border-color: transparent;
+          color: #5e5abe; }
+        .sling-cms-editor .button.is-link.is-light:active, .sling-cms-editor .button.is-link.is-light.is-active {
+          background-color: #dfdef2;
+          border-color: transparent;
+          color: #5e5abe; }
+    .sling-cms-editor .button.is-info {
+      background-color: #3298dc;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-info:hover, .sling-cms-editor .button.is-info.is-hovered {
+        background-color: #2793da;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-info:focus, .sling-cms-editor .button.is-info.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-info:focus:not(:active), .sling-cms-editor .button.is-info.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(50, 152, 220, 0.25); }
+      .sling-cms-editor .button.is-info:active, .sling-cms-editor .button.is-info.is-active {
+        background-color: #238cd1;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-info[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-info {
+        background-color: #3298dc;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-info.is-inverted {
+        background-color: #fff;
+        color: #3298dc; }
+        .sling-cms-editor .button.is-info.is-inverted:hover, .sling-cms-editor .button.is-info.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-info.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-info.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #3298dc; }
+      .sling-cms-editor .button.is-info.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-info.is-outlined {
+        background-color: transparent;
+        border-color: #3298dc;
+        color: #3298dc; }
+        .sling-cms-editor .button.is-info.is-outlined:hover, .sling-cms-editor .button.is-info.is-outlined.is-hovered, .sling-cms-editor .button.is-info.is-outlined:focus, .sling-cms-editor .button.is-info.is-outlined.is-focused {
+          background-color: #3298dc;
+          border-color: #3298dc;
+          color: #fff; }
+        .sling-cms-editor .button.is-info.is-outlined.is-loading::after {
+          border-color: transparent transparent #3298dc #3298dc !important; }
+        .sling-cms-editor .button.is-info.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-info.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-info.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-info.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-info.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-info.is-outlined {
+          background-color: transparent;
+          border-color: #3298dc;
+          box-shadow: none;
+          color: #3298dc; }
+      .sling-cms-editor .button.is-info.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-info.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-info.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #3298dc; }
+        .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-info.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #3298dc #3298dc !important; }
+        .sling-cms-editor .button.is-info.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-info.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-info.is-light {
+        background-color: #eef6fc;
+        color: #1d72aa; }
+        .sling-cms-editor .button.is-info.is-light:hover, .sling-cms-editor .button.is-info.is-light.is-hovered {
+          background-color: #e3f1fa;
+          border-color: transparent;
+          color: #1d72aa; }
+        .sling-cms-editor .button.is-info.is-light:active, .sling-cms-editor .button.is-info.is-light.is-active {
+          background-color: #d8ebf8;
+          border-color: transparent;
+          color: #1d72aa; }
+    .sling-cms-editor .button.is-success {
+      background-color: #48c774;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-success:hover, .sling-cms-editor .button.is-success.is-hovered {
+        background-color: #3ec46d;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-success:focus, .sling-cms-editor .button.is-success.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-success:focus:not(:active), .sling-cms-editor .button.is-success.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(72, 199, 116, 0.25); }
+      .sling-cms-editor .button.is-success:active, .sling-cms-editor .button.is-success.is-active {
+        background-color: #3abb67;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-success[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-success {
+        background-color: #48c774;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-success.is-inverted {
+        background-color: #fff;
+        color: #48c774; }
+        .sling-cms-editor .button.is-success.is-inverted:hover, .sling-cms-editor .button.is-success.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-success.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-success.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #48c774; }
+      .sling-cms-editor .button.is-success.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-success.is-outlined {
+        background-color: transparent;
+        border-color: #48c774;
+        color: #48c774; }
+        .sling-cms-editor .button.is-success.is-outlined:hover, .sling-cms-editor .button.is-success.is-outlined.is-hovered, .sling-cms-editor .button.is-success.is-outlined:focus, .sling-cms-editor .button.is-success.is-outlined.is-focused {
+          background-color: #48c774;
+          border-color: #48c774;
+          color: #fff; }
+        .sling-cms-editor .button.is-success.is-outlined.is-loading::after {
+          border-color: transparent transparent #48c774 #48c774 !important; }
+        .sling-cms-editor .button.is-success.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-success.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-success.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-success.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-success.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-success.is-outlined {
+          background-color: transparent;
+          border-color: #48c774;
+          box-shadow: none;
+          color: #48c774; }
+      .sling-cms-editor .button.is-success.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-success.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-success.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #48c774; }
+        .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-success.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #48c774 #48c774 !important; }
+        .sling-cms-editor .button.is-success.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-success.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-success.is-light {
+        background-color: #effaf3;
+        color: #257942; }
+        .sling-cms-editor .button.is-success.is-light:hover, .sling-cms-editor .button.is-success.is-light.is-hovered {
+          background-color: #e6f7ec;
+          border-color: transparent;
+          color: #257942; }
+        .sling-cms-editor .button.is-success.is-light:active, .sling-cms-editor .button.is-success.is-light.is-active {
+          background-color: #dcf4e4;
+          border-color: transparent;
+          color: #257942; }
+    .sling-cms-editor .button.is-warning {
+      background-color: #EA7826;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-warning:hover, .sling-cms-editor .button.is-warning.is-hovered {
+        background-color: #e9711a;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-warning:focus, .sling-cms-editor .button.is-warning.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-warning:focus:not(:active), .sling-cms-editor .button.is-warning.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(234, 120, 38, 0.25); }
+      .sling-cms-editor .button.is-warning:active, .sling-cms-editor .button.is-warning.is-active {
+        background-color: #e16b16;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-warning[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-warning {
+        background-color: #EA7826;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-warning.is-inverted {
+        background-color: #fff;
+        color: #EA7826; }
+        .sling-cms-editor .button.is-warning.is-inverted:hover, .sling-cms-editor .button.is-warning.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-warning.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-warning.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #EA7826; }
+      .sling-cms-editor .button.is-warning.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-warning.is-outlined {
+        background-color: transparent;
+        border-color: #EA7826;
+        color: #EA7826; }
+        .sling-cms-editor .button.is-warning.is-outlined:hover, .sling-cms-editor .button.is-warning.is-outlined.is-hovered, .sling-cms-editor .button.is-warning.is-outlined:focus, .sling-cms-editor .button.is-warning.is-outlined.is-focused {
+          background-color: #EA7826;
+          border-color: #EA7826;
+          color: #fff; }
+        .sling-cms-editor .button.is-warning.is-outlined.is-loading::after {
+          border-color: transparent transparent #EA7826 #EA7826 !important; }
+        .sling-cms-editor .button.is-warning.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-warning.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-warning.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-warning.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-warning.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-warning.is-outlined {
+          background-color: transparent;
+          border-color: #EA7826;
+          box-shadow: none;
+          color: #EA7826; }
+      .sling-cms-editor .button.is-warning.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-warning.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-warning.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #EA7826; }
+        .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-warning.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #EA7826 #EA7826 !important; }
+        .sling-cms-editor .button.is-warning.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-warning.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-warning.is-light {
+        background-color: #fdf3ec;
+        color: #b15411; }
+        .sling-cms-editor .button.is-warning.is-light:hover, .sling-cms-editor .button.is-warning.is-light.is-hovered {
+          background-color: #fcece1;
+          border-color: transparent;
+          color: #b15411; }
+        .sling-cms-editor .button.is-warning.is-light:active, .sling-cms-editor .button.is-warning.is-light.is-active {
+          background-color: #fbe5d5;
+          border-color: transparent;
+          color: #b15411; }
+    .sling-cms-editor .button.is-danger {
+      background-color: #CB2138;
+      border-color: transparent;
+      color: #fff; }
+      .sling-cms-editor .button.is-danger:hover, .sling-cms-editor .button.is-danger.is-hovered {
+        background-color: #c01f35;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-danger:focus, .sling-cms-editor .button.is-danger.is-focused {
+        border-color: transparent;
+        color: #fff; }
+        .sling-cms-editor .button.is-danger:focus:not(:active), .sling-cms-editor .button.is-danger.is-focused:not(:active) {
+          box-shadow: 0 0 0 0.125em rgba(203, 33, 56, 0.25); }
+      .sling-cms-editor .button.is-danger:active, .sling-cms-editor .button.is-danger.is-active {
+        background-color: #b51d32;
+        border-color: transparent;
+        color: #fff; }
+      .sling-cms-editor .button.is-danger[disabled],
+      fieldset[disabled] .sling-cms-editor .button.is-danger {
+        background-color: #CB2138;
+        border-color: transparent;
+        box-shadow: none; }
+      .sling-cms-editor .button.is-danger.is-inverted {
+        background-color: #fff;
+        color: #CB2138; }
+        .sling-cms-editor .button.is-danger.is-inverted:hover, .sling-cms-editor .button.is-danger.is-inverted.is-hovered {
+          background-color: #f2f2f2; }
+        .sling-cms-editor .button.is-danger.is-inverted[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-danger.is-inverted {
+          background-color: #fff;
+          border-color: transparent;
+          box-shadow: none;
+          color: #CB2138; }
+      .sling-cms-editor .button.is-danger.is-loading::after {
+        border-color: transparent transparent #fff #fff !important; }
+      .sling-cms-editor .button.is-danger.is-outlined {
+        background-color: transparent;
+        border-color: #CB2138;
+        color: #CB2138; }
+        .sling-cms-editor .button.is-danger.is-outlined:hover, .sling-cms-editor .button.is-danger.is-outlined.is-hovered, .sling-cms-editor .button.is-danger.is-outlined:focus, .sling-cms-editor .button.is-danger.is-outlined.is-focused {
+          background-color: #CB2138;
+          border-color: #CB2138;
+          color: #fff; }
+        .sling-cms-editor .button.is-danger.is-outlined.is-loading::after {
+          border-color: transparent transparent #CB2138 #CB2138 !important; }
+        .sling-cms-editor .button.is-danger.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-danger.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-danger.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-danger.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #fff #fff !important; }
+        .sling-cms-editor .button.is-danger.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-danger.is-outlined {
+          background-color: transparent;
+          border-color: #CB2138;
+          box-shadow: none;
+          color: #CB2138; }
+      .sling-cms-editor .button.is-danger.is-inverted.is-outlined {
+        background-color: transparent;
+        border-color: #fff;
+        color: #fff; }
+        .sling-cms-editor .button.is-danger.is-inverted.is-outlined:hover, .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-hovered, .sling-cms-editor .button.is-danger.is-inverted.is-outlined:focus, .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-focused {
+          background-color: #fff;
+          color: #CB2138; }
+        .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-loading:hover::after, .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after, .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-loading:focus::after, .sling-cms-editor .button.is-danger.is-inverted.is-outlined.is-loading.is-focused::after {
+          border-color: transparent transparent #CB2138 #CB2138 !important; }
+        .sling-cms-editor .button.is-danger.is-inverted.is-outlined[disabled],
+        fieldset[disabled] .sling-cms-editor .button.is-danger.is-inverted.is-outlined {
+          background-color: transparent;
+          border-color: #fff;
+          box-shadow: none;
+          color: #fff; }
+      .sling-cms-editor .button.is-danger.is-light {
+        background-color: #fcedef;
+        color: #d3223a; }
+        .sling-cms-editor .button.is-danger.is-light:hover, .sling-cms-editor .button.is-danger.is-light.is-hovered {
+          background-color: #fae2e6;
+          border-color: transparent;
+          color: #d3223a; }
+        .sling-cms-editor .button.is-danger.is-light:active, .sling-cms-editor .button.is-danger.is-light.is-active {
+          background-color: #f9d8dc;
+          border-color: transparent;
+          color: #d3223a; }
+    .sling-cms-editor .button.is-small {
+      border-radius: 2px;
+      font-size: 0.75rem; }
+    .sling-cms-editor .button.is-normal {
+      font-size: 1rem; }
+    .sling-cms-editor .button.is-medium {
+      font-size: 1.25rem; }
+    .sling-cms-editor .button.is-large {
+      font-size: 1.5rem; }
+    .sling-cms-editor .button[disabled],
+    fieldset[disabled] .sling-cms-editor .button {
+      background-color: white;
+      border-color: #dbdbdb;
+      box-shadow: none;
+      opacity: 0.5; }
+    .sling-cms-editor .button.is-fullwidth {
+      display: flex;
+      width: 100%; }
+    .sling-cms-editor .button.is-loading {
+      color: transparent !important;
+      pointer-events: none; }
+      .sling-cms-editor .button.is-loading::after {
+        position: absolute;
+        left: calc(50% - (1em / 2));
+        top: calc(50% - (1em / 2));
+        position: absolute !important; }
+    .sling-cms-editor .button.is-static {
+      background-color: whitesmoke;
+      border-color: #dbdbdb;
+      color: #7a7a7a;
+      box-shadow: none;
+      pointer-events: none; }
+    .sling-cms-editor .button.is-rounded {
+      border-radius: 290486px;
+      padding-left: calc(1em + 0.25em);
+      padding-right: calc(1em + 0.25em); }
+  .sling-cms-editor .buttons {
+    align-items: center;
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: flex-start; }
+    .sling-cms-editor .buttons .button {
+      margin-bottom: 0.5rem; }
+      .sling-cms-editor .buttons .button:not(:last-child):not(.is-fullwidth) {
+        margin-right: 0.5rem; }
+    .sling-cms-editor .buttons:last-child {
+      margin-bottom: -0.5rem; }
+    .sling-cms-editor .buttons:not(:last-child) {
+      margin-bottom: 1rem; }
+    .sling-cms-editor .buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large) {
+      border-radius: 2px;
+      font-size: 0.75rem; }
+    .sling-cms-editor .buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large) {
+      font-size: 1.25rem; }
+    .sling-cms-editor .buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium) {
+      font-size: 1.5rem; }
+    .sling-cms-editor .buttons.has-addons .button:not(:first-child) {
+      border-bottom-left-radius: 0;
+      border-top-left-radius: 0; }
+    .sling-cms-editor .buttons.has-addons .button:not(:last-child) {
+      border-bottom-right-radius: 0;
+      border-top-right-radius: 0;
+      margin-right: -1px; }
+    .sling-cms-editor .buttons.has-addons .button:last-child {
+      margin-right: 0; }
+    .sling-cms-editor .buttons.has-addons .button:hover, .sling-cms-editor .buttons.has-addons .button.is-hovered {
+      z-index: 2; }
+    .sling-cms-editor .buttons.has-addons .button:focus, .sling-cms-editor .buttons.has-addons .button.is-focused, .sling-cms-editor .buttons.has-addons .button:active, .sling-cms-editor .buttons.has-addons .button.is-active, .sling-cms-editor .buttons.has-addons .button.is-selected {
+      z-index: 3; }
+      .sling-cms-editor .buttons.has-addons .button:focus:hover, .sling-cms-editor .buttons.has-addons .button.is-focused:hover, .sling-cms-editor .buttons.has-addons .button:active:hover, .sling-cms-editor .buttons.has-addons .button.is-active:hover, .sling-cms-editor .buttons.has-addons .button.is-selected:hover {
+        z-index: 4; }
+    .sling-cms-editor .buttons.has-addons .button.is-expanded {
+      flex-grow: 1;
+      flex-shrink: 1; }
+    .sling-cms-editor .buttons.is-centered {
+      justify-content: center; }
+      .sling-cms-editor .buttons.is-centered:not(.has-addons) .button:not(.is-fullwidth) {
+        margin-left: 0.25rem;
+        margin-right: 0.25rem; }
+    .sling-cms-editor .buttons.is-right {
+      justify-content: flex-end; }
+      .sling-cms-editor .buttons.is-right:not(.has-addons) .button:not(.is-fullwidth) {
+        margin-left: 0.25rem;
+        margin-right: 0.25rem; }
+  .sling-cms-editor .input, .sling-cms-editor .textarea, .sling-cms-editor .select select {
+    background-color: white;
+    border-color: #dbdbdb;
+    border-radius: 4px;
+    color: #363636; }
+    .sling-cms-editor .input::-moz-placeholder, .sling-cms-editor .textarea::-moz-placeholder, .sling-cms-editor .select select::-moz-placeholder {
+      color: rgba(54, 54, 54, 0.3); }
+    .sling-cms-editor .input::-webkit-input-placeholder, .sling-cms-editor .textarea::-webkit-input-placeholder, .sling-cms-editor .select select::-webkit-input-placeholder {
+      color: rgba(54, 54, 54, 0.3); }
+    .sling-cms-editor .input:-moz-placeholder, .sling-cms-editor .textarea:-moz-placeholder, .sling-cms-editor .select select:-moz-placeholder {
+      color: rgba(54, 54, 54, 0.3); }
+    .sling-cms-editor .input:-ms-input-placeholder, .sling-cms-editor .textarea:-ms-input-placeholder, .sling-cms-editor .select select:-ms-input-placeholder {
+      color: rgba(54, 54, 54, 0.3); }
+    .sling-cms-editor .input:hover, .sling-cms-editor .textarea:hover, .sling-cms-editor .select select:hover, .sling-cms-editor .is-hovered.input, .sling-cms-editor .is-hovered.textarea, .sling-cms-editor .select select.is-hovered {
+      border-color: #b5b5b5; }
+    .sling-cms-editor .input:focus, .sling-cms-editor .textarea:focus, .sling-cms-editor .select select:focus, .sling-cms-editor .is-focused.input, .sling-cms-editor .is-focused.textarea, .sling-cms-editor .select select.is-focused, .sling-cms-editor .input:active, .sling-cms-editor .textarea:active, .sling-cms-editor .select select:active, .sling-cms-editor .is-active.input, .sling-cms-editor .is-active.textarea, .sling-cms-editor .select select.is-active {
+      border-color: #282661;
+      box-shadow: 0 0 0 0.125em rgba(40, 38, 97, 0.25); }
+    .sling-cms-editor .input[disabled], .sling-cms-editor .textarea[disabled], .sling-cms-editor .select select[disabled],
+    fieldset[disabled] .sling-cms-editor .input,
+    fieldset[disabled] .sling-cms-editor .textarea,
+    fieldset[disabled] .sling-cms-editor .select select {
+      background-color: whitesmoke;
+      border-color: whitesmoke;
+      box-shadow: none;
+      color: #7a7a7a; }
+      .sling-cms-editor .input[disabled]::-moz-placeholder, .sling-cms-editor .textarea[disabled]::-moz-placeholder, .sling-cms-editor .select select[disabled]::-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .input::-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .textarea::-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .select select::-moz-placeholder {
+        color: rgba(122, 122, 122, 0.3); }
+      .sling-cms-editor .input[disabled]::-webkit-input-placeholder, .sling-cms-editor .textarea[disabled]::-webkit-input-placeholder, .sling-cms-editor .select select[disabled]::-webkit-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .input::-webkit-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .textarea::-webkit-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .select select::-webkit-input-placeholder {
+        color: rgba(122, 122, 122, 0.3); }
+      .sling-cms-editor .input[disabled]:-moz-placeholder, .sling-cms-editor .textarea[disabled]:-moz-placeholder, .sling-cms-editor .select select[disabled]:-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .input:-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .textarea:-moz-placeholder,
+      fieldset[disabled] .sling-cms-editor .select select:-moz-placeholder {
+        color: rgba(122, 122, 122, 0.3); }
+      .sling-cms-editor .input[disabled]:-ms-input-placeholder, .sling-cms-editor .textarea[disabled]:-ms-input-placeholder, .sling-cms-editor .select select[disabled]:-ms-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .input:-ms-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .textarea:-ms-input-placeholder,
+      fieldset[disabled] .sling-cms-editor .select select:-ms-input-placeholder {
+        color: rgba(122, 122, 122, 0.3); }
+  .sling-cms-editor .input, .sling-cms-editor .textarea {
+    box-shadow: inset 0 0.0625em 0.125em rgba(10, 10, 10, 0.05);
+    max-width: 100%;
+    width: 100%; }
+    .sling-cms-editor .input[readonly], .sling-cms-editor .textarea[readonly] {
+      box-shadow: none; }
+    .sling-cms-editor .is-white.input, .sling-cms-editor .is-white.textarea {
+      border-color: white; }
+      .sling-cms-editor .is-white.input:focus, .sling-cms-editor .is-white.textarea:focus, .sling-cms-editor .is-white.is-focused.input, .sling-cms-editor .is-white.is-focused.textarea, .sling-cms-editor .is-white.input:active, .sling-cms-editor .is-white.textarea:active, .sling-cms-editor .is-white.is-active.input, .sling-cms-editor .is-white.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); }
+    .sling-cms-editor .is-black.input, .sling-cms-editor .is-black.textarea {
+      border-color: #0a0a0a; }
+      .sling-cms-editor .is-black.input:focus, .sling-cms-editor .is-black.textarea:focus, .sling-cms-editor .is-black.is-focused.input, .sling-cms-editor .is-black.is-focused.textarea, .sling-cms-editor .is-black.input:active, .sling-cms-editor .is-black.textarea:active, .sling-cms-editor .is-black.is-active.input, .sling-cms-editor .is-black.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); }
+    .sling-cms-editor .is-light.input, .sling-cms-editor .is-light.textarea {
+      border-color: whitesmoke; }
+      .sling-cms-editor .is-light.input:focus, .sling-cms-editor .is-light.textarea:focus, .sling-cms-editor .is-light.is-focused.input, .sling-cms-editor .is-light.is-focused.textarea, .sling-cms-editor .is-light.input:active, .sling-cms-editor .is-light.textarea:active, .sling-cms-editor .is-light.is-active.input, .sling-cms-editor .is-light.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); }
+    .sling-cms-editor .is-dark.input, .sling-cms-editor .is-dark.textarea {
+      border-color: #363636; }
+      .sling-cms-editor .is-dark.input:focus, .sling-cms-editor .is-dark.textarea:focus, .sling-cms-editor .is-dark.is-focused.input, .sling-cms-editor .is-dark.is-focused.textarea, .sling-cms-editor .is-dark.input:active, .sling-cms-editor .is-dark.textarea:active, .sling-cms-editor .is-dark.is-active.input, .sling-cms-editor .is-dark.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); }
+    .sling-cms-editor .is-primary.input, .sling-cms-editor .is-primary.textarea {
+      border-color: #9E2165; }
+      .sling-cms-editor .is-primary.input:focus, .sling-cms-editor .is-primary.textarea:focus, .sling-cms-editor .is-primary.is-focused.input, .sling-cms-editor .is-primary.is-focused.textarea, .sling-cms-editor .is-primary.input:active, .sling-cms-editor .is-primary.textarea:active, .sling-cms-editor .is-primary.is-active.input, .sling-cms-editor .is-primary.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(158, 33, 101, 0.25); }
+    .sling-cms-editor .is-link.input, .sling-cms-editor .is-link.textarea {
+      border-color: #282661; }
+      .sling-cms-editor .is-link.input:focus, .sling-cms-editor .is-link.textarea:focus, .sling-cms-editor .is-link.is-focused.input, .sling-cms-editor .is-link.is-focused.textarea, .sling-cms-editor .is-link.input:active, .sling-cms-editor .is-link.textarea:active, .sling-cms-editor .is-link.is-active.input, .sling-cms-editor .is-link.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(40, 38, 97, 0.25); }
+    .sling-cms-editor .is-info.input, .sling-cms-editor .is-info.textarea {
+      border-color: #3298dc; }
+      .sling-cms-editor .is-info.input:focus, .sling-cms-editor .is-info.textarea:focus, .sling-cms-editor .is-info.is-focused.input, .sling-cms-editor .is-info.is-focused.textarea, .sling-cms-editor .is-info.input:active, .sling-cms-editor .is-info.textarea:active, .sling-cms-editor .is-info.is-active.input, .sling-cms-editor .is-info.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(50, 152, 220, 0.25); }
+    .sling-cms-editor .is-success.input, .sling-cms-editor .is-success.textarea {
+      border-color: #48c774; }
+      .sling-cms-editor .is-success.input:focus, .sling-cms-editor .is-success.textarea:focus, .sling-cms-editor .is-success.is-focused.input, .sling-cms-editor .is-success.is-focused.textarea, .sling-cms-editor .is-success.input:active, .sling-cms-editor .is-success.textarea:active, .sling-cms-editor .is-success.is-active.input, .sling-cms-editor .is-success.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(72, 199, 116, 0.25); }
+    .sling-cms-editor .is-warning.input, .sling-cms-editor .is-warning.textarea {
+      border-color: #EA7826; }
+      .sling-cms-editor .is-warning.input:focus, .sling-cms-editor .is-warning.textarea:focus, .sling-cms-editor .is-warning.is-focused.input, .sling-cms-editor .is-warning.is-focused.textarea, .sling-cms-editor .is-warning.input:active, .sling-cms-editor .is-warning.textarea:active, .sling-cms-editor .is-warning.is-active.input, .sling-cms-editor .is-warning.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(234, 120, 38, 0.25); }
+    .sling-cms-editor .is-danger.input, .sling-cms-editor .is-danger.textarea {
+      border-color: #CB2138; }
+      .sling-cms-editor .is-danger.input:focus, .sling-cms-editor .is-danger.textarea:focus, .sling-cms-editor .is-danger.is-focused.input, .sling-cms-editor .is-danger.is-focused.textarea, .sling-cms-editor .is-danger.input:active, .sling-cms-editor .is-danger.textarea:active, .sling-cms-editor .is-danger.is-active.input, .sling-cms-editor .is-danger.is-active.textarea {
+        box-shadow: 0 0 0 0.125em rgba(203, 33, 56, 0.25); }
+    .sling-cms-editor .is-small.input, .sling-cms-editor .is-small.textarea {
+      border-radius: 2px;
+      font-size: 0.75rem; }
+    .sling-cms-editor .is-medium.input, .sling-cms-editor .is-medium.textarea {
+      font-size: 1.25rem; }
+    .sling-cms-editor .is-large.input, .sling-cms-editor .is-large.textarea {
+      font-size: 1.5rem; }
+    .sling-cms-editor .is-fullwidth.input, .sling-cms-editor .is-fullwidth.textarea {
+      display: block;
+      width: 100%; }
+    .sling-cms-editor .is-inline.input, .sling-cms-editor .is-inline.textarea {
+      display: inline;
+      width: auto; }
+  .sling-cms-editor .input.is-rounded {
+    border-radius: 290486px;
+    padding-left: calc(calc(0.75em - 1px) + 0.375em);
+    padding-right: calc(calc(0.75em - 1px) + 0.375em); }
+  .sling-cms-editor .input.is-static {
+    background-color: transparent;
+    border-color: transparent;
+    box-shadow: none;
+    padding-left: 0;
+    padding-right: 0; }
+  .sling-cms-editor .textarea {
+    display: block;
+    max-width: 100%;
+    min-width: 100%;
+    padding: calc(0.75em - 1px);
+    resize: vertical; }
+    .sling-cms-editor .textarea:not([rows]) {
+      max-height: 40em;
+      min-height: 8em; }
+    .sling-cms-editor .textarea[rows] {
+      height: initial; }
+    .sling-cms-editor .textarea.has-fixed-size {
+      resize: none; }
+  .sling-cms-editor .checkbox, .sling-cms-editor .radio {
+    cursor: pointer;
+    display: inline-block;
+    line-height: 1.25;
+    position: relative; }
+    .sling-cms-editor .checkbox input, .sling-cms-editor .radio input {
+      cursor: pointer; }
+    .sling-cms-editor .checkbox:hover, .sling-cms-editor .radio:hover {
+      color: #363636; }
+    .sling-cms-editor .checkbox[disabled], .sling-cms-editor .radio[disabled],
+    fieldset[disabled] .sling-cms-editor .checkbox,
+    fieldset[disabled] .sling-cms-editor .radio {
+      color: #7a7a7a;
+      cursor: not-allowed; }
+  .sling-cms-editor .radio + .radio {
+    margin-left: 0.5em; }
+  .sling-cms-editor .select {
+    display: inline-block;
+    max-width: 100%;
+    position: relative;
+    vertical-align: top; }
+    .sling-cms-editor .select:not(.is-multiple) {
+      height: 2.5em; }
+    .sling-cms-editor .select:not(.is-multiple):not(.is-loading)::after {
+      border-color: #282661;
+      right: 1.125em;
+      z-index: 4; }
+    .sling-cms-editor .select.is-rounded select {
+      border-radius: 290486px;
+      padding-left: 1em; }
+    .sling-cms-editor .select select {
+      cursor: pointer;
+      display: block;
+      font-size: 1em;
+      max-width: 100%;
+      outline: none; }
+      .sling-cms-editor .select select::-ms-expand {
+        display: none; }
+      .sling-cms-editor .select select[disabled]:hover,
+      fieldset[disabled] .sling-cms-editor .select select:hover {
+        border-color: whitesmoke; }
+      .sling-cms-editor .select select:not([multiple]) {
+        padding-right: 2.5em; }
+      .sling-cms-editor .select select[multiple] {
+        height: auto;
+        padding: 0; }
+        .sling-cms-editor .select select[multiple] option {
+          padding: 0.5em 1em; }
+    .sling-cms-editor .select:not(.is-multiple):not(.is-loading):hover::after {
+      border-color: #363636; }
+    .sling-cms-editor .select.is-white:not(:hover)::after {
+      border-color: white; }
+    .sling-cms-editor .select.is-white select {
+      border-color: white; }
+      .sling-cms-editor .select.is-white select:hover, .sling-cms-editor .select.is-white select.is-hovered {
+        border-color: #f2f2f2; }
+      .sling-cms-editor .select.is-white select:focus, .sling-cms-editor .select.is-white select.is-focused, .sling-cms-editor .select.is-white select:active, .sling-cms-editor .select.is-white select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(255, 255, 255, 0.25); }
+    .sling-cms-editor .select.is-black:not(:hover)::after {
+      border-color: #0a0a0a; }
+    .sling-cms-editor .select.is-black select {
+      border-color: #0a0a0a; }
+      .sling-cms-editor .select.is-black select:hover, .sling-cms-editor .select.is-black select.is-hovered {
+        border-color: black; }
+      .sling-cms-editor .select.is-black select:focus, .sling-cms-editor .select.is-black select.is-focused, .sling-cms-editor .select.is-black select:active, .sling-cms-editor .select.is-black select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(10, 10, 10, 0.25); }
+    .sling-cms-editor .select.is-light:not(:hover)::after {
+      border-color: whitesmoke; }
+    .sling-cms-editor .select.is-light select {
+      border-color: whitesmoke; }
+      .sling-cms-editor .select.is-light select:hover, .sling-cms-editor .select.is-light select.is-hovered {
+        border-color: #e8e8e8; }
+      .sling-cms-editor .select.is-light select:focus, .sling-cms-editor .select.is-light select.is-focused, .sling-cms-editor .select.is-light select:active, .sling-cms-editor .select.is-light select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(245, 245, 245, 0.25); }
+    .sling-cms-editor .select.is-dark:not(:hover)::after {
+      border-color: #363636; }
+    .sling-cms-editor .select.is-dark select {
+      border-color: #363636; }
+      .sling-cms-editor .select.is-dark select:hover, .sling-cms-editor .select.is-dark select.is-hovered {
+        border-color: #292929; }
+      .sling-cms-editor .select.is-dark select:focus, .sling-cms-editor .select.is-dark select.is-focused, .sling-cms-editor .select.is-dark select:active, .sling-cms-editor .select.is-dark select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(54, 54, 54, 0.25); }
+    .sling-cms-editor .select.is-primary:not(:hover)::after {
+      border-color: #9E2165; }
+    .sling-cms-editor .select.is-primary select {
+      border-color: #9E2165; }
+      .sling-cms-editor .select.is-primary select:hover, .sling-cms-editor .select.is-primary select.is-hovered {
+        border-color: #891d58; }
+      .sling-cms-editor .select.is-primary select:focus, .sling-cms-editor .select.is-primary select.is-focused, .sling-cms-editor .select.is-primary select:active, .sling-cms-editor .select.is-primary select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(158, 33, 101, 0.25); }
+    .sling-cms-editor .select.is-link:not(:hover)::after {
+      border-color: #282661; }
+    .sling-cms-editor .select.is-link select {
+      border-color: #282661; }
+      .sling-cms-editor .select.is-link select:hover, .sling-cms-editor .select.is-link select.is-hovered {
+        border-color: #201f4f; }
+      .sling-cms-editor .select.is-link select:focus, .sling-cms-editor .select.is-link select.is-focused, .sling-cms-editor .select.is-link select:active, .sling-cms-editor .select.is-link select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(40, 38, 97, 0.25); }
+    .sling-cms-editor .select.is-info:not(:hover)::after {
+      border-color: #3298dc; }
+    .sling-cms-editor .select.is-info select {
+      border-color: #3298dc; }
+      .sling-cms-editor .select.is-info select:hover, .sling-cms-editor .select.is-info select.is-hovered {
+        border-color: #238cd1; }
+      .sling-cms-editor .select.is-info select:focus, .sling-cms-editor .select.is-info select.is-focused, .sling-cms-editor .select.is-info select:active, .sling-cms-editor .select.is-info select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(50, 152, 220, 0.25); }
+    .sling-cms-editor .select.is-success:not(:hover)::after {
+      border-color: #48c774; }
+    .sling-cms-editor .select.is-success select {
+      border-color: #48c774; }
+      .sling-cms-editor .select.is-success select:hover, .sling-cms-editor .select.is-success select.is-hovered {
+        border-color: #3abb67; }
+      .sling-cms-editor .select.is-success select:focus, .sling-cms-editor .select.is-success select.is-focused, .sling-cms-editor .select.is-success select:active, .sling-cms-editor .select.is-success select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(72, 199, 116, 0.25); }
+    .sling-cms-editor .select.is-warning:not(:hover)::after {
+      border-color: #EA7826; }
+    .sling-cms-editor .select.is-warning select {
+      border-color: #EA7826; }
+      .sling-cms-editor .select.is-warning select:hover, .sling-cms-editor .select.is-warning select.is-hovered {
+        border-color: #e16b16; }
+      .sling-cms-editor .select.is-warning select:focus, .sling-cms-editor .select.is-warning select.is-focused, .sling-cms-editor .select.is-warning select:active, .sling-cms-editor .select.is-warning select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(234, 120, 38, 0.25); }
+    .sling-cms-editor .select.is-danger:not(:hover)::after {
+      border-color: #CB2138; }
+    .sling-cms-editor .select.is-danger select {
+      border-color: #CB2138; }
+      .sling-cms-editor .select.is-danger select:hover, .sling-cms-editor .select.is-danger select.is-hovered {
+        border-color: #b51d32; }
+      .sling-cms-editor .select.is-danger select:focus, .sling-cms-editor .select.is-danger select.is-focused, .sling-cms-editor .select.is-danger select:active, .sling-cms-editor .select.is-danger select.is-active {
+        box-shadow: 0 0 0 0.125em rgba(203, 33, 56, 0.25); }
+    .sling-cms-editor .select.is-small {
+      border-radius: 2px;
+      font-size: 0.75rem; }
+    .sling-cms-editor .select.is-medium {
+      font-size: 1.25rem; }
+    .sling-cms-editor .select.is-large {
+      font-size: 1.5rem; }
+    .sling-cms-editor .select.is-disabled::after {
+      border-color: #7a7a7a; }
+    .sling-cms-editor .select.is-fullwidth {
+      width: 100%; }
+      .sling-cms-editor .select.is-fullwidth select {
+        width: 100%; }
+    .sling-cms-editor .select.is-loading::after {
+      margin-top: 0;
+      position: absolute;
+      right: 0.625em;
+      top: 0.625em;
+      transform: none; }
+    .sling-cms-editor .select.is-loading.is-small:after {
+      font-size: 0.75rem; }
+    .sling-cms-editor .select.is-loading.is-medium:after {
+      font-size: 1.25rem; }
+    .sling-cms-editor .select.is-loading.is-large:after {
+      font-size: 1.5rem; }
+  .sling-cms-editor .file {
+    align-items: stretch;
+    display: flex;
+    justify-content: flex-start;
+    position: relative; }
+    .sling-cms-editor .file.is-white .file-cta {
+      background-color: white;
+      border-color: transparent;
+      color: #0a0a0a; }
+    .sling-cms-editor .file.is-white:hover .file-cta, .sling-cms-editor .file.is-white.is-hovered .file-cta {
+      background-color: #f9f9f9;
+      border-color: transparent;
+      color: #0a0a0a; }
+    .sling-cms-editor .file.is-white:focus .file-cta, .sling-cms-editor .file.is-white.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(255, 255, 255, 0.25);
+      color: #0a0a0a; }
+    .sling-cms-editor .file.is-white:active .file-cta, .sling-cms-editor .file.is-white.is-active .file-cta {
+      background-color: #f2f2f2;
+      border-color: transparent;
+      color: #0a0a0a; }
+    .sling-cms-editor .file.is-black .file-cta {
+      background-color: #0a0a0a;
+      border-color: transparent;
+      color: white; }
+    .sling-cms-editor .file.is-black:hover .file-cta, .sling-cms-editor .file.is-black.is-hovered .file-cta {
+      background-color: #040404;
+      border-color: transparent;
+      color: white; }
+    .sling-cms-editor .file.is-black:focus .file-cta, .sling-cms-editor .file.is-black.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(10, 10, 10, 0.25);
+      color: white; }
+    .sling-cms-editor .file.is-black:active .file-cta, .sling-cms-editor .file.is-black.is-active .file-cta {
+      background-color: black;
+      border-color: transparent;
+      color: white; }
+    .sling-cms-editor .file.is-light .file-cta {
+      background-color: whitesmoke;
+      border-color: transparent;
+      color: rgba(0, 0, 0, 0.7); }
+    .sling-cms-editor .file.is-light:hover .file-cta, .sling-cms-editor .file.is-light.is-hovered .file-cta {
+      background-color: #eeeeee;
+      border-color: transparent;
+      color: rgba(0, 0, 0, 0.7); }
+    .sling-cms-editor .file.is-light:focus .file-cta, .sling-cms-editor .file.is-light.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(245, 245, 245, 0.25);
+      color: rgba(0, 0, 0, 0.7); }
+    .sling-cms-editor .file.is-light:active .file-cta, .sling-cms-editor .file.is-light.is-active .file-cta {
+      background-color: #e8e8e8;
+      border-color: transparent;
+      color: rgba(0, 0, 0, 0.7); }
+    .sling-cms-editor .file.is-dark .file-cta {
+      background-color: #363636;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-dark:hover .file-cta, .sling-cms-editor .file.is-dark.is-hovered .file-cta {
+      background-color: #2f2f2f;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-dark:focus .file-cta, .sling-cms-editor .file.is-dark.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(54, 54, 54, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-dark:active .file-cta, .sling-cms-editor .file.is-dark.is-active .file-cta {
+      background-color: #292929;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-primary .file-cta {
+      background-color: #9E2165;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-primary:hover .file-cta, .sling-cms-editor .file.is-primary.is-hovered .file-cta {
+      background-color: #931f5e;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-primary:focus .file-cta, .sling-cms-editor .file.is-primary.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(158, 33, 101, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-primary:active .file-cta, .sling-cms-editor .file.is-primary.is-active .file-cta {
+      background-color: #891d58;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-link .file-cta {
+      background-color: #282661;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-link:hover .file-cta, .sling-cms-editor .file.is-link.is-hovered .file-cta {
+      background-color: #242258;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-link:focus .file-cta, .sling-cms-editor .file.is-link.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(40, 38, 97, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-link:active .file-cta, .sling-cms-editor .file.is-link.is-active .file-cta {
+      background-color: #201f4f;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-info .file-cta {
+      background-color: #3298dc;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-info:hover .file-cta, .sling-cms-editor .file.is-info.is-hovered .file-cta {
+      background-color: #2793da;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-info:focus .file-cta, .sling-cms-editor .file.is-info.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(50, 152, 220, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-info:active .file-cta, .sling-cms-editor .file.is-info.is-active .file-cta {
+      background-color: #238cd1;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-success .file-cta {
+      background-color: #48c774;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-success:hover .file-cta, .sling-cms-editor .file.is-success.is-hovered .file-cta {
+      background-color: #3ec46d;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-success:focus .file-cta, .sling-cms-editor .file.is-success.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(72, 199, 116, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-success:active .file-cta, .sling-cms-editor .file.is-success.is-active .file-cta {
+      background-color: #3abb67;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-warning .file-cta {
+      background-color: #EA7826;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-warning:hover .file-cta, .sling-cms-editor .file.is-warning.is-hovered .file-cta {
+      background-color: #e9711a;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-warning:focus .file-cta, .sling-cms-editor .file.is-warning.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(234, 120, 38, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-warning:active .file-cta, .sling-cms-editor .file.is-warning.is-active .file-cta {
+      background-color: #e16b16;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-danger .file-cta {
+      background-color: #CB2138;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-danger:hover .file-cta, .sling-cms-editor .file.is-danger.is-hovered .file-cta {
+      background-color: #c01f35;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-danger:focus .file-cta, .sling-cms-editor .file.is-danger.is-focused .file-cta {
+      border-color: transparent;
+      box-shadow: 0 0 0.5em rgba(203, 33, 56, 0.25);
+      color: #fff; }
+    .sling-cms-editor .file.is-danger:active .file-cta, .sling-cms-editor .file.is-danger.is-active .file-cta {
+      background-color: #b51d32;
+      border-color: transparent;
+      color: #fff; }
+    .sling-cms-editor .file.is-small {
+      font-size: 0.75rem; }
+    .sling-cms-editor .file.is-medium {
+      font-size: 1.25rem; }
+      .sling-cms-editor .file.is-medium .file-icon .fa {
+        font-size: 21px; }
+    .sling-cms-editor .file.is-large {
+      font-size: 1.5rem; }
+      .sling-cms-editor .file.is-large .file-icon .fa {
+        font-size: 28px; }
+    .sling-cms-editor .file.has-name .file-cta {
+      border-bottom-right-radius: 0;
+      border-top-right-radius: 0; }
+    .sling-cms-editor .file.has-name .file-name {
+      border-bottom-left-radius: 0;
+      border-top-left-radius: 0; }
+    .sling-cms-editor .file.has-name.is-empty .file-cta {
+      border-radius: 4px; }
+    .sling-cms-editor .file.has-name.is-empty .file-name {
+      display: none; }
+    .sling-cms-editor .file.is-boxed .file-label {
+      flex-direction: column; }
+    .sling-cms-editor .file.is-boxed .file-cta {
+      flex-direction: column;
+      height: auto;
+      padding: 1em 3em; }
+    .sling-cms-editor .file.is-boxed .file-name {
+      border-width: 0 1px 1px; }
+    .sling-cms-editor .file.is-boxed .file-icon {
+      height: 1.5em;
+      width: 1.5em; }
+      .sling-cms-editor .file.is-boxed .file-icon .fa {
+        font-size: 21px; }
+    .sling-cms-editor .file.is-boxed.is-small .file-icon .fa {
+      font-size: 14px; }
+    .sling-cms-editor .file.is-boxed.is-medium .file-icon .fa {
+      font-size: 28px; }
+    .sling-cms-editor .file.is-boxed.is-large .file-icon .fa {
+      font-size: 35px; }
+    .sling-cms-editor .file.is-boxed.has-name .file-cta {
+      border-radius: 4px 4px 0 0; }
+    .sling-cms-editor .file.is-boxed.has-name .file-name {
+      border-radius: 0 0 4px 4px;
+      border-width: 0 1px 1px; }
+    .sling-cms-editor .file.is-centered {
+      justify-content: center; }
+    .sling-cms-editor .file.is-fullwidth .file-label {
+      width: 100%; }
+    .sling-cms-editor .file.is-fullwidth .file-name {
+      flex-grow: 1;
+      max-width: none; }
+    .sling-cms-editor .file.is-right {
+      justify-content: flex-end; }
+      .sling-cms-editor .file.is-right .file-cta {
+        border-radius: 0 4px 4px 0; }
+      .sling-cms-editor .file.is-right .file-name {
+        border-radius: 4px 0 0 4px;
+        border-width: 1px 0 1px 1px;
+        order: -1; }
+  .sling-cms-editor .file-label {
+    align-items: stretch;
+    display: flex;
+    cursor: pointer;
+    justify-content: flex-start;
+    overflow: hidden;
+    position: relative; }
+    .sling-cms-editor .file-label:hover .file-cta {
+      background-color: #eeeeee;
+      color: #363636; }
+    .sling-cms-editor .file-label:hover .file-name {
+      border-color: #d5d5d5; }
+    .sling-cms-editor .file-label:active .file-cta {
+      background-color: #e8e8e8;
+      color: #363636; }
+    .sling-cms-editor .file-label:active .file-name {
+      border-color: #cfcfcf; }
+  .sling-cms-editor .file-input {
+    height: 100%;
+    left: 0;
+    opacity: 0;
+    outline: none;
+    position: absolute;
+    top: 0;
+    width: 100%; }
+  .sling-cms-editor .file-cta,
+  .sling-cms-editor .file-name {
+    border-color: #dbdbdb;
+    border-radius: 4px;
+    font-size: 1em;
+    padding-left: 1em;
+    padding-right: 1em;
+    white-space: nowrap; }
+  .sling-cms-editor .file-cta {
+    background-color: whitesmoke;
+    color: #4a4a4a; }
+  .sling-cms-editor .file-name {
+    border-color: #dbdbdb;
+    border-style: solid;
+    border-width: 1px 1px 1px 0;
+    display: block;
+    max-width: 16em;
+    overflow: hidden;
+    text-align: left;
+    text-overflow: ellipsis; }
+  .sling-cms-editor .file-icon {
+    align-items: center;
+    display: flex;
+    height: 1em;
+    justify-content: center;
+    margin-right: 0.5em;
+    width: 1em; }
+    .sling-cms-editor .file-icon .fa {
+      font-size: 14px; }
+  .sling-cms-editor .label {
+    color: #363636;
+    display: block;
+    font-size: 1rem;
+    font-weight: 700; }
+    .sling-cms-editor .label:not(:last-child) {
+      margin-bottom: 0.5em; }
+    .sling-cms-editor .label.is-small {
+      font-size: 0.75rem; }
+    .sling-cms-editor .label.is-medium {
+      font-size: 1.25rem; }
+    .sling-cms-editor .label.is-large {
+      font-size: 1.5rem; }
+  .sling-cms-editor .help {
+    display: block;
+    font-size: 0.75rem;
+    margin-top: 0.25rem; }
+    .sling-cms-editor .help.is-white {
+      color: white; }
+    .sling-cms-editor .help.is-black {
+      color: #0a0a0a; }
+    .sling-cms-editor .help.is-light {
+      color: whitesmoke; }
+    .sling-cms-editor .help.is-dark {
+      color: #363636; }
+    .sling-cms-editor .help.is-primary {
+      color: #9E2165; }
+    .sling-cms-editor .help.is-link {
+      color: #282661; }
+    .sling-cms-editor .help.is-info {
+      color: #3298dc; }
+    .sling-cms-editor .help.is-success {
+      color: #48c774; }
+    .sling-cms-editor .help.is-warning {
+      color: #EA7826; }
+    .sling-cms-editor .help.is-danger {
+      color: #CB2138; }
+  .sling-cms-editor .field:not(:last-child) {
+    margin-bottom: 0.75rem; }
+  .sling-cms-editor .field.has-addons {
+    display: flex;
+    justify-content: flex-start; }
+    .sling-cms-editor .field.has-addons .control:not(:last-child) {
+      margin-right: -1px; }
+    .sling-cms-editor .field.has-addons .control:not(:first-child):not(:last-child) .button,
+    .sling-cms-editor .field.has-addons .control:not(:first-child):not(:last-child) .input,
+    .sling-cms-editor .field.has-addons .control:not(:first-child):not(:last-child) .select select {
+      border-radius: 0; }
+    .sling-cms-editor .field.has-addons .control:first-child:not(:only-child) .button,
+    .sling-cms-editor .field.has-addons .control:first-child:not(:only-child) .input,
+    .sling-cms-editor .field.has-addons .control:first-child:not(:only-child) .select select {
+      border-bottom-right-radius: 0;
+      border-top-right-radius: 0; }
+    .sling-cms-editor .field.has-addons .control:last-child:not(:only-child) .button,
+    .sling-cms-editor .field.has-addons .control:last-child:not(:only-child) .input,
+    .sling-cms-editor .field.has-addons .control:last-child:not(:only-child) .select select {
+      border-bottom-left-radius: 0;
+      border-top-left-radius: 0; }
+    .sling-cms-editor .field.has-addons .control .button:not([disabled]):hover, .sling-cms-editor .field.has-addons .control .button:not([disabled]).is-hovered,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]):hover,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]).is-hovered,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]):hover,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]).is-hovered {
+      z-index: 2; }
+    .sling-cms-editor .field.has-addons .control .button:not([disabled]):focus, .sling-cms-editor .field.has-addons .control .button:not([disabled]).is-focused, .sling-cms-editor .field.has-addons .control .button:not([disabled]):active, .sling-cms-editor .field.has-addons .control .button:not([disabled]).is-active,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]):focus,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]).is-focused,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]):active,
+    .sling-cms-editor .field.has-addons .control .input:not([disabled]).is-active,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]):focus,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]).is-focused,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]):active,
+    .sling-cms-editor .field.has-addons .control .select select:not([disabled]).is-active {
+      z-index: 3; }
+      .sling-cms-editor .field.has-addons .control .button:not([disabled]):focus:hover, .sling-cms-editor .field.has-addons .control .button:not([disabled]).is-focused:hover, .sling-cms-editor .field.has-addons .control .button:not([disabled]):active:hover, .sling-cms-editor .field.has-addons .control .button:not([disabled]).is-active:hover,
+      .sling-cms-editor .field.has-addons .control .input:not([disabled]):focus:hover,
+      .sling-cms-editor .field.has-addons .control .input:not([disabled]).is-focused:hover,
+      .sling-cms-editor .field.has-addons .control .input:not([disabled]):active:hover,
+      .sling-cms-editor .field.has-addons .control .input:not([disabled]).is-active:hover,
+      .sling-cms-editor .field.has-addons .control .select select:not([disabled]):focus:hover,
+      .sling-cms-editor .field.has-addons .control .select select:not([disabled]).is-focused:hover,
+      .sling-cms-editor .field.has-addons .control .select select:not([disabled]):active:hover,
+      .sling-cms-editor .field.has-addons .control .select select:not([disabled]).is-active:hover {
+        z-index: 4; }
+    .sling-cms-editor .field.has-addons .control.is-expanded {
+      flex-grow: 1;
+      flex-shrink: 1; }
+    .sling-cms-editor .field.has-addons.has-addons-centered {
+      justify-content: center; }
+    .sling-cms-editor .field.has-addons.has-addons-right {
+      justify-content: flex-end; }
+    .sling-cms-editor .field.has-addons.has-addons-fullwidth .control {
+      flex-grow: 1;
+      flex-shrink: 0; }
+  .sling-cms-editor .field.is-grouped {
+    display: flex;
+    justify-content: flex-start; }
+    .sling-cms-editor .field.is-grouped > .control {
+      flex-shrink: 0; }
+      .sling-cms-editor .field.is-grouped > .control:not(:last-child) {
+        margin-bottom: 0;
+        margin-right: 0.75rem; }
+      .sling-cms-editor .field.is-grouped > .control.is-expanded {
+        flex-grow: 1;
+        flex-shrink: 1; }
+    .sling-cms-editor .field.is-grouped.is-grouped-centered {
+      justify-content: center; }
+    .sling-cms-editor .field.is-grouped.is-grouped-right {
+      justify-content: flex-end; }
+    .sling-cms-editor .field.is-grouped.is-grouped-multiline {
+      flex-wrap: wrap; }
+      .sling-cms-editor .field.is-grouped.is-grouped-multiline > .control:last-child, .sling-cms-editor .field.is-grouped.is-grouped-multiline > .control:not(:last-child) {
+        margin-bottom: 0.75rem; }
+      .sling-cms-editor .field.is-grouped.is-grouped-multiline:last-child {
+        margin-bottom: -0.75rem; }
+      .sling-cms-editor .field.is-grouped.is-grouped-multiline:not(:last-child) {
+        margin-bottom: 0; }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .field.is-horizontal {
+      display: flex; } }
+  .sling-cms-editor .field-label .label {
+    font-size: inherit; }
+  @media screen and (max-width: 768px) {
+    .sling-cms-editor .field-label {
+      margin-bottom: 0.5rem; } }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .field-label {
+      flex-basis: 0;
+      flex-grow: 1;
+      flex-shrink: 0;
+      margin-right: 1.5rem;
+      text-align: right; }
+      .sling-cms-editor .field-label.is-small {
+        font-size: 0.75rem;
+        padding-top: 0.375em; }
+      .sling-cms-editor .field-label.is-normal {
+        padding-top: 0.375em; }
+      .sling-cms-editor .field-label.is-medium {
+        font-size: 1.25rem;
+        padding-top: 0.375em; }
+      .sling-cms-editor .field-label.is-large {
+        font-size: 1.5rem;
+        padding-top: 0.375em; } }
+  .sling-cms-editor .field-body .field .field {
+    margin-bottom: 0; }
+  @media screen and (min-width: 769px), print {
+    .sling-cms-editor .field-body {
+      display: flex;
+      flex-basis: 0;
+      flex-grow: 5;
+      flex-shrink: 1; }
+      .sling-cms-editor .field-body .field {
+        margin-bottom: 0; }
+      .sling-cms-editor .field-body > .field {
+        flex-shrink: 1; }
+        .sling-cms-editor .field-body > .field:not(.is-narrow) {
+          flex-grow: 1; }
+        .sling-cms-editor .field-body > .field:not(:last-child) {
+          margin-right: 0.75rem; } }
+  .sling-cms-editor .control {
+    box-sizing: border-box;
+    clear: both;
+    font-size: 1rem;
+    position: relative;
+    text-align: left; }
+    .sling-cms-editor .control.has-icons-left .input:focus ~ .icon,
+    .sling-cms-editor .control.has-icons-left .select:focus ~ .icon, .sling-cms-editor .control.has-icons-right .input:focus ~ .icon,
+    .sling-cms-editor .control.has-icons-right .select:focus ~ .icon {
+      color: #4a4a4a; }
+    .sling-cms-editor .control.has-icons-left .input.is-small ~ .icon,
+    .sling-cms-editor .control.has-icons-left .select.is-small ~ .icon, .sling-cms-editor .control.has-icons-right .input.is-small ~ .icon,
+    .sling-cms-editor .control.has-icons-right .select.is-small ~ .icon {
+      font-size: 0.75rem; }
+    .sling-cms-editor .control.has-icons-left .input.is-medium ~ .icon,
+    .sling-cms-editor .control.has-icons-left .select.is-medium ~ .icon, .sling-cms-editor .control.has-icons-right .input.is-medium ~ .icon,
+    .sling-cms-editor .control.has-icons-right .select.is-medium ~ .icon {
+      font-size: 1.25rem; }
+    .sling-cms-editor .control.has-icons-left .input.is-large ~ .icon,
+    .sling-cms-editor .control.has-icons-left .select.is-large ~ .icon, .sling-cms-editor .control.has-icons-right .input.is-large ~ .icon,
+    .sling-cms-editor .control.has-icons-right .select.is-large ~ .icon {
+      font-size: 1.5rem; }
+    .sling-cms-editor .control.has-icons-left .icon, .sling-cms-editor .control.has-icons-right .icon {
+      color: #dbdbdb;
+      height: 2.5em;
+      pointer-events: none;
+      position: absolute;
+      top: 0;
+      width: 2.5em;
+      z-index: 4; }
+    .sling-cms-editor .control.has-icons-left .input,
+    .sling-cms-editor .control.has-icons-left .select select {
+      padding-left: 2.5em; }
+    .sling-cms-editor .control.has-icons-left .icon.is-left {
+      left: 0; }
+    .sling-cms-editor .control.has-icons-right .input,
+    .sling-cms-editor .control.has-icons-right .select select {
+      padding-right: 2.5em; }
+    .sling-cms-editor .control.has-icons-right .icon.is-right {
+      right: 0; }
+    .sling-cms-editor .control.is-loading::after {
+      position: absolute !important;
+      right: 0.625em;
+      top: 0.625em;
+      z-index: 4; }
+    .sling-cms-editor .control.is-loading.is-small:after {
+      font-size: 0.75rem; }
+    .sling-cms-editor .control.is-loading.is-medium:after {
+      font-size: 1.25rem; }
+    .sling-cms-editor .control.is-loading.is-large:after {
+      font-size: 1.5rem; }
+  .sling-cms-editor .icon {
+    align-items: center;
+    display: inline-flex;
+    justify-content: center;
+    height: 1.5rem;
+    width: 1.5rem; }
+    .sling-cms-editor .icon.is-small {
+      height: 1rem;
+      width: 1rem; }
+    .sling-cms-editor .icon.is-medium {
+      height: 2rem;
+      width: 2rem; }
+    .sling-cms-editor .icon.is-large {
+      height: 3rem;
+      width: 3rem; }
+  .sling-cms-editor .level {
+    align-items: center;
+    justify-content: space-between; }
+    .sling-cms-editor .level code {
+      border-radius: 4px; }
+    .sling-cms-editor .level img {
+      display: inline-block;
+      vertical-align: top; }
+    .sling-cms-editor .level.is-mobile {
+      display: flex; }
+      .sling-cms-editor .level.is-mobile .level-left,
+      .sling-cms-editor .level.is-mobile .level-right {
+        display: flex; }
+      .sling-cms-editor .level.is-mobile .level-left + .level-right {
+        margin-top: 0; }
+      .sling-cms-editor .level.is-mobile .level-item:not(:last-child) {
+        margin-bottom: 0;
+        margin-right: 0.75rem; }
+      .sling-cms-editor .level.is-mobile .level-item:not(.is-narrow) {
+        flex-grow: 1; }
+    @media screen and (min-width: 769px), print {
+      .sling-cms-editor .level {
+        display: flex; }
+        .sling-cms-editor .level > .level-item:not(.is-narrow) {
+          flex-grow: 1; } }
+  .sling-cms-editor .level-item {
+    align-items: center;
+    display: flex;
+    flex-basis: auto;
+    flex-grow: 0;
+    flex-shrink: 0;
+    justify-content: center; }
+    .sling-cms-editor .level-item .title,
+    .sling-cms-editor .level-item .subtitle {
+      margin-bottom: 0; }
+    @media screen and (max-width: 768px) {
+      .sling-cms-editor .level-item:not(:last-child) {
+        margin-bottom: 0.75rem; } }
+  .sling-cms-editor .level-left,
+  .sling-cms-editor .level-right {
+    flex-basis: auto;
+    flex-grow: 0;
+    flex-shrink: 0; }
+    .sling-cms-editor .level-left .level-item.is-flexible,
+    .sling-cms-editor .level-right .level-item.is-flexible {
+      flex-grow: 1; }
+    @media screen and (min-width: 769px), print {
+      .sling-cms-editor .level-left .level-item:not(:last-child),
+      .sling-cms-editor .level-right .level-item:not(:last-child) {
+        margin-right: 0.75rem; } }
+  .sling-cms-editor .level-left {
+    align-items: center;
+    justify-content: flex-start; }
+    @media screen and (max-width: 768px) {
+      .sling-cms-editor .level-left + .level-right {
+        margin-top: 1.5rem; } }
+    @media screen and (min-width: 769px), print {
+      .sling-cms-editor .level-left {
+        display: flex; } }
+  .sling-cms-editor .level-right {
+    align-items: center;
+    justify-content: flex-end; }
+    @media screen and (min-width: 769px), print {
+      .sling-cms-editor .level-right {
+        display: flex; } }
+  .sling-cms-editor .modal {
+    align-items: center;
+    display: none;
+    flex-direction: column;
+    justify-content: center;
+    overflow: hidden;
+    position: fixed;
+    z-index: 2000; }
+    .sling-cms-editor .modal.is-active {
+      display: flex; }
+  .sling-cms-editor .modal-background {
+    background-color: rgba(10, 10, 10, 0.86); }
+  .sling-cms-editor .modal-content,
+  .sling-cms-editor .modal-card {
+    margin: 0 20px;
+    max-height: calc(100vh - 160px);
+    overflow: auto;
+    position: relative;
+    width: 100%; }
+    @media screen and (min-width: 769px), print {
+      .sling-cms-editor .modal-content,
+      .sling-cms-editor .modal-card {
+        margin: 0 auto;
+        max-height: calc(100vh - 40px);
+        width: 640px; } }
+  .sling-cms-editor .modal-close {
+    background: none;
+    height: 40px;
+    position: fixed;
+    right: 20px;
+    top: 20px;
+    width: 40px; }
+  .sling-cms-editor .modal-card {
+    display: flex;
+    flex-direction: column;
+    max-height: calc(100vh - 40px);
+    overflow: hidden;
+    -ms-overflow-y: visible; }
+  .sling-cms-editor .modal-card-head,
+  .sling-cms-editor .modal-card-foot {
+    align-items: center;
+    background-color: whitesmoke;
+    display: flex;
+    flex-shrink: 0;
+    justify-content: flex-start;
+    padding: 20px;
+    position: relative; }
+  .sling-cms-editor .modal-card-head {
+    border-bottom: 1px solid #dbdbdb;
+    border-top-left-radius: 6px;
+    border-top-right-radius: 6px; }
+  .sling-cms-editor .modal-card-title {
+    color: #363636;
+    flex-grow: 1;
+    flex-shrink: 0;
+    font-size: 1.5rem;
+    line-height: 1; }
+  .sling-cms-editor .modal-card-foot {
+    border-bottom-left-radius: 6px;
+    border-bottom-right-radius: 6px;
+    border-top: 1px solid #dbdbdb; }
+    .sling-cms-editor .modal-card-foot .button:not(:last-child) {
+      margin-right: 0.5em; }
+  .sling-cms-editor .modal-card-body {
+    -webkit-overflow-scrolling: touch;
+    background-color: white;
+    flex-grow: 1;
+    flex-shrink: 1;
+    overflow: auto;
+    padding: 20px; }
+  .sling-cms-editor * {
+    box-sizing: border-box; }
+  .sling-cms-editor [draggable] {
+    -moz-user-select: none;
+    -khtml-user-select: none;
+    -webkit-user-select: none;
+    user-select: none;
+    /* Required to make elements draggable in old WebKit */
+    -khtml-user-drag: element;
+    -webkit-user-drag: element; }
+  .sling-cms-editor .is-draggable {
+    position: relative; }
+  .sling-cms-editor .is-vhidden {
+    position: absolute !important;
+    top: -9999px !important;
+    left: -9999px !important; }
+  .sling-cms-editor .level {
+    padding: 0.5em; }
+  .sling-cms-editor .modal-body {
+    padding: 0.5em;
+    height: 500px; }
+  .sling-cms-editor .modal-frame {
+    width: 100%;
+    height: 100%;
+    border: none; }
+  .sling-cms-editor .modal-title {
+    font-size: 180%;
+    cursor: move; }
+  .sling-cms-editor .page-wrapper-frame {
+    position: fixed;
+    top: 44px;
+    left: 0;
+    bottom: 0;
+    right: 0;
+    width: 100%;
+    height: 100%;
+    border: none;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+    z-index: 998;
+    padding-top: 8px; }
+    @media screen and (max-width: 769px) {
+      .sling-cms-editor .page-wrapper-frame {
+        top: 104px; } }
+  .sling-cms-editor .sling-cms-logo {
+    height: 25px; }
+
+.sling-cms-component {
+  border: 1px solid rgba(0, 0, 0, 0); }
+
+.sling-cms-component__is-active {
+  border-color: #dbdbdb; }
+
+.sling-cms-droptarget__is-active {
+  height: 2em;
+  border: 1px solid #b5b5b5;
+  background-color: whitesmoke; }
+
+.sling-cms-droptarget__is-over {
+  height: 2em;
+  border: 1px solid whitesmoke;
+  background-color: #b5b5b5; }
+
+/*
+  Copyright (c) 2018 Michael Amprimo <@michaelampr>.
+  Licensed under the MIT License (MIT), https://jam-icons.com
+*/
+@font-face{font-family:'jam-icons';src:url("../fonts/jam-icons.eot?osflwm");src:url("../fonts/jam-icons.eot?osflwm#iefix") format("embedded-opentype"),url("../fonts/jam-icons.ttf?osflwm") format("truetype"),url("../fonts/jam-icons.woff?osflwm") format("woff"),url("../fonts/jam-icons.svg?osflwm#Jam-icons") format("svg");font-weight:normal;font-style:normal}.jam{font-family:'jam-icons' !important;speak:none;font-style:normal;font-weight:normal;font-variant:normal;text-transform:none;line-h [...]
diff --git a/src/test/resources/sling.pdf b/src/test/resources/sling.pdf
new file mode 100644
index 0000000..a443389
Binary files /dev/null and b/src/test/resources/sling.pdf differ
diff --git a/src/test/resources/thumbnail.png b/src/test/resources/thumbnail.png
new file mode 100644
index 0000000..f656dbe
Binary files /dev/null and b/src/test/resources/thumbnail.png differ
diff --git a/src/test/resources/web-console.txt b/src/test/resources/web-console.txt
new file mode 100644
index 0000000..e6b9fc8
--- /dev/null
+++ b/src/test/resources/web-console.txt
@@ -0,0 +1,24 @@
+<pre>
+Supported Resource Types
+========================
+[Resource Type] => [MetaType Property Path]
+nt:file => jcr:content/jcr:mimeType
+</pre><br/>
+<pre>
+Persistable Resource Types
+========================
+[Resource Type] => [Rendition Path]
+</pre><br/>
+<pre>
+Registered Thumbnail Providers
+========================
+org.apache.sling.thumbnails.internal.providers.PdfThumbnailProvider
+org.apache.sling.thumbnails.internal.providers.ImageThumbnailProvider
+</pre><br/>
+<pre>
+Registered Transformation Providers
+========================
+sling/thumbnails/transformers/crop=org.apache.sling.thumbnails.internal.transformers.CropHandler
+sling/thumbnails/transformers/resize=org.apache.sling.thumbnails.internal.transformers.ResizeHandler
+</pre>
+</div>