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 2019/05/21 14:05:34 UTC

[sling-org-apache-sling-app-cms] branch master updated: Adding a service to generate thumbnails

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 257ba91  Adding a service to generate thumbnails
257ba91 is described below

commit 257ba9151f24c744436a010ee7b36b85f86a7268
Author: Dan Klco <dk...@apache.org>
AuthorDate: Tue May 21 10:05:28 2019 -0400

    Adding a service to generate thumbnails
---
 api/pom.xml                                        |  12 +++
 .../transformation/FileThumbnailTransformer.java   |  62 ++++++++++++
 .../sling/cms/transformation/OutputFileFormat.java |  51 ++++++++++
 .../cms/transformation/ThumbnailProvider.java      |  47 +++++++++
 .../cms/transformation/TransformationHandler.java  |  52 ++++++++++
 core/pom.xml                                       |  10 +-
 .../core/internal/models/ReferenceOperation.java   |   9 +-
 .../core/internal/servlets/TransformServlet.java   |  67 +++++--------
 .../core/internal/transformation/CropHandler.java  |  49 +++++++++
 .../FileThumbnailTransformerImpl.java              | 104 ++++++++++++++++++++
 .../transformation/ImageThumbnailProvider.java     |  43 ++++++++
 .../transformation/PdfThumbnailProvider.java       |  58 +++++++++++
 .../core/internal/transformation/SizeHandler.java  |  53 ++++++++++
 .../cms/core/helpers/SlingCMSContextHelper.java    |  43 ++++++++
 .../internal/transformation/CropHandlerTest.java   |  71 ++++++++++++++
 .../FileThumbnailTransformerImplTest.java          |  72 ++++++++++++++
 .../transformation/ImageThumbnailProviderTest.java |  76 ++++++++++++++
 .../transformation/PdfThumbnailProviderTest.java   |  68 +++++++++++++
 .../internal/transformation/SizeHandlerTest.java   |  68 +++++++++++++
 core/src/test/resources/content.json               | 109 +++++++++++++++++++++
 core/src/test/resources/simplelogger.properties    |  17 ----
 core/src/test/resources/sling.pdf                  | Bin 0 -> 251268 bytes
 pom.xml                                            |  25 +++++
 {api => thumbnailator}/pom.xml                     |  56 ++++-------
 24 files changed, 1120 insertions(+), 102 deletions(-)

diff --git a/api/pom.xml b/api/pom.xml
index 9cd4af5..3134e6a 100644
--- a/api/pom.xml
+++ b/api/pom.xml
@@ -73,6 +73,10 @@
 
     <dependencies>
         <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.api</artifactId>
         </dependency>
@@ -108,5 +112,13 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/sling/cms/transformation/FileThumbnailTransformer.java b/api/src/main/java/org/apache/sling/cms/transformation/FileThumbnailTransformer.java
new file mode 100644
index 0000000..29f3099
--- /dev/null
+++ b/api/src/main/java/org/apache/sling/cms/transformation/FileThumbnailTransformer.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.cms.transformation;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.apache.sling.cms.transformation.OutputFileFormat;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.cms.File;
+
+/**
+ * Transforms a Sling File into thumbnails using the registered
+ * TransformationHandlers to invoke Thumbnails transformations on the file.
+ */
+public interface FileThumbnailTransformer {
+
+    /**
+     * Gets the transformation handler for the specified command
+     * 
+     * @param command the command string to use to look up the transformation
+     *                handler.
+     * @return the TransformationHandler from the command string or null if none
+     *         found
+     */
+    TransformationHandler getTransformationHandler(String command);
+
+    /**
+     * Transforms the file into a thumbnail using the specified commands.
+     * 
+     * @param commands the commands to execute
+     * @param format   the format of the file to return
+     * @param out      the Outputstream to write the thumbnail to
+     * @throws IOException an exception occurs transforming the file
+     */
+    void transformFile(File file, String[] commands, OutputFileFormat format, OutputStream out) throws IOException;
+
+    /**
+     * Transforms the file from the resource path into a thumbnail using the
+     * specified commands from the suffix.
+     * 
+     * @param request the request to parse the file and commands from
+     * @param out     the Outputstream to write the thumbnail to
+     * @throws IOException an exception occurs transforming the file
+     */
+    void transformFile(SlingHttpServletRequest request, OutputStream out) throws IOException;
+
+}
diff --git a/api/src/main/java/org/apache/sling/cms/transformation/OutputFileFormat.java b/api/src/main/java/org/apache/sling/cms/transformation/OutputFileFormat.java
new file mode 100644
index 0000000..2f158cb
--- /dev/null
+++ b/api/src/main/java/org/apache/sling/cms/transformation/OutputFileFormat.java
@@ -0,0 +1,51 @@
+/*
+ * 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.cms.transformation;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+
+import com.google.common.base.Enums;
+import com.google.common.net.MediaType;
+
+/**
+ * Enumeration of the valid output formats for the thumbnail generator.
+ */
+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) {
+        String suffixExtension = StringUtils.substringAfterLast(request.getRequestPathInfo().getSuffix(), ".").toUpperCase();
+        return Enums.getIfPresent(OutputFileFormat.class, suffixExtension).or(OutputFileFormat.JPEG);
+    }
+
+    private String mimeType;
+
+    private OutputFileFormat(String mimeType) {
+        this.mimeType = mimeType;
+    }
+
+    public String getMimeType() {
+        return mimeType;
+    }
+}
diff --git a/api/src/main/java/org/apache/sling/cms/transformation/ThumbnailProvider.java b/api/src/main/java/org/apache/sling/cms/transformation/ThumbnailProvider.java
new file mode 100644
index 0000000..4d9bb56
--- /dev/null
+++ b/api/src/main/java/org/apache/sling/cms/transformation/ThumbnailProvider.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.cms.transformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.sling.cms.File;
+
+/**
+ * Service for retrieving a thumbnail for the specified File object.
+ */
+public interface ThumbnailProvider {
+
+    /**
+     * Returns true if the ThumbnailProvider applies for the specified file.
+     * 
+     * @param file the file to check
+     * @return true if this ThumbnailProvider will create a thumbnail for this file,
+     *         false otherwise
+     */
+    boolean applies(File file);
+
+    /**
+     * Get the thumbnail from the specified file.
+     * 
+     * @param file the file from which to retrieve the thumbnail
+     * @return the thumbnail
+     * @throws IOException an exception occurs retrieving the thumbnail
+     */
+    InputStream getThumbnail(File file) throws IOException;
+
+}
\ No newline at end of file
diff --git a/api/src/main/java/org/apache/sling/cms/transformation/TransformationHandler.java b/api/src/main/java/org/apache/sling/cms/transformation/TransformationHandler.java
new file mode 100644
index 0000000..5e507e6
--- /dev/null
+++ b/api/src/main/java/org/apache/sling/cms/transformation/TransformationHandler.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.cms.transformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+/*
+ * Transformation handlers handle the transformation of thumbnails using the Thumbnails library.
+ * Each transformation handler implements a named transformation command which will be parsed out 
+ * of the suffix of the transformation request by splitting the suffix by slashes and checking the 
+ * "applies" method of each TransformationHandler, in order. 
+ */
+public interface TransformationHandler {
+
+    /**
+     * Returns true if the transformation handler should execute for the specified
+     * command.
+     * 
+     * @param command the command to check
+     * @return true if the handler will handle this, false otherwise
+     */
+    boolean applies(String command);
+
+    /**
+     * Handles the transformation of the thumbnail using the command values from the
+     * suffix segment.
+     * 
+     * @param builder the Thumbnails builder to use / update
+     * @param cmd     the command to parse to retrieve the configuration values for
+     *                the transformation
+     * @throws IOException an exception occurs transforming the thumbnail
+     */
+    void handle(Builder<? extends InputStream> builder, String cmd) throws IOException;
+
+}
diff --git a/core/pom.xml b/core/pom.xml
index 04a5232..49e5799 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -243,8 +243,6 @@
         <dependency>
             <groupId>net.coobird</groupId>
             <artifactId>thumbnailator</artifactId>
-            <version>[0.4, 0.5)</version>
-            <scope>compile</scope>
         </dependency>
         <dependency>
             <groupId>org.mockito</groupId>
@@ -255,5 +253,13 @@
             <artifactId>pdfbox</artifactId>
             <groupId>org.apache.pdfbox</groupId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.poi</groupId>
+            <artifactId>poi-ooxml</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+        </dependency>
     </dependencies>
 </project>
\ No newline at end of file
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/models/ReferenceOperation.java b/core/src/main/java/org/apache/sling/cms/core/internal/models/ReferenceOperation.java
index 1119c8c..9c63e3d 100644
--- a/core/src/main/java/org/apache/sling/cms/core/internal/models/ReferenceOperation.java
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/models/ReferenceOperation.java
@@ -24,6 +24,7 @@ import java.util.regex.Pattern;
 
 import javax.jcr.query.Query;
 
+import org.apache.jackrabbit.util.Text;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ValueMap;
 import org.apache.sling.cms.CMSConstants;
@@ -42,11 +43,11 @@ public abstract class ReferenceOperation {
     private Resource resource = null;
 
     public ReferenceOperation(Resource resource) {
-        String path = resource.getPath().replace("/", "\\/");
+        String path = resource.getPath();
         if (CMSConstants.NT_PAGE.equals(resource.getResourceType())) {
-            regex = Pattern.compile("(^" + path + "($|\\/)|(\\'|\\\")" + path + "(\\.html|\\'|\\\"|\\/))");
+            regex = Pattern.compile("(^\\Q" + path + "\\E($|\\/)|(\\'|\\\")\\Q" + path + "\\E(\\.html|\\'|\\\"|\\/))");
         } else {
-            regex = Pattern.compile("(^" + path + "($|\\/)|(\\'|\\\")" + path + "(\\'|\\\"|\\/))");
+            regex = Pattern.compile("(^\\Q" + path + "\\E($|\\/)|(\\'|\\\")\\Q" + path + "\\E(\\'|\\\"|\\/))");
         }
         this.resource = resource;
     }
@@ -82,7 +83,7 @@ public abstract class ReferenceOperation {
     public void init() {
         log.debug("Finding references to {}", resource.getPath());
         String query = "SELECT * FROM [nt:base] AS s WHERE NOT ISDESCENDANTNODE([/jcr:system/jcr:versionStorage]) AND CONTAINS(s.*, '"
-                + resource.getPath() + "')";
+                + Text.escapeIllegalXpathSearchChars(resource.getPath()) + "')";
         Set<String> paths = new HashSet<>();
 
         Iterator<Resource> resources = resource.getResourceResolver().findResources(query, Query.JCR_SQL2);
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java b/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java
index df79c9e..06143b6 100644
--- a/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java
@@ -16,67 +16,50 @@
  */
 package org.apache.sling.cms.core.internal.servlets;
 
-import java.awt.image.BufferedImage;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.InputStream;
-import java.util.Optional;
 
-import javax.imageio.ImageIO;
 import javax.servlet.Servlet;
 import javax.servlet.ServletException;
 
-import org.apache.pdfbox.pdmodel.PDDocument;
-import org.apache.pdfbox.rendering.ImageType;
-import org.apache.pdfbox.rendering.PDFRenderer;
 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.SlingSafeMethodsServlet;
-import org.apache.sling.cms.File;
+import org.apache.sling.cms.transformation.FileThumbnailTransformer;
+import org.apache.sling.cms.transformation.OutputFileFormat;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-import com.google.common.net.MediaType;
-
-import net.coobird.thumbnailator.Thumbnails;
-import net.coobird.thumbnailator.Thumbnails.Builder;
-import net.coobird.thumbnailator.geometry.Positions;
-
+/**
+ * A servlet to transform images using the FileThumbnailTransformer API. Can be
+ * invoked using the syntax:
+ * 
+ * /content/file/path.jpg.transform/command-param1-param2/command2-param1-param2.png
+ */
 @Component(service = { Servlet.class }, property = { "sling.servlet.extensions=transform",
         "sling.servlet.resourceTypes=sling:File", "sling.servlet.resourceTypes=nt:file" })
 public class TransformServlet extends SlingSafeMethodsServlet {
 
+    private static final Logger log = LoggerFactory.getLogger(TransformServlet.class);
+
     private static final long serialVersionUID = -1513067546618762171L;
 
+    @Reference
+    private transient FileThumbnailTransformer transformer;
+
+    @Override
     protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
             throws ServletException, IOException {
-
-        Builder<? extends InputStream> builder = Thumbnails.of(getInputStream(request.getResource()));
-        for (String cmd : request.getRequestPathInfo().getSuffix().split("/")) {
-            if (cmd.startsWith("resize-")) {
-                builder.size(Integer.parseInt(cmd.split("\\-")[1], 10), Integer.parseInt(cmd.split("\\-")[2], 10));
-            }
+        String original = response.getContentType();
+        try {
+            response.setContentType(OutputFileFormat.forRequest(request).getMimeType());
+            transformer.transformFile(request, response.getOutputStream());
+        } catch (Exception e) {
+            log.error("Exception transforming image", e);
+            response.setContentType(original);
+            response.sendError(400, "Could not transform image with provided commands");
         }
-        builder.crop(Positions.CENTER);
-        response.setContentType("image/png");
-        builder.toOutputStream(response.getOutputStream());
     }
 
-    private InputStream getInputStream(Resource resource) throws IOException {
-        String contentType = Optional.ofNullable(resource.adaptTo(File.class)).map(File::getContentType).orElse("");
-        if (contentType.startsWith("image")) {
-            return resource.adaptTo(InputStream.class);
-        }
-        if (MediaType.PDF.toString().equals(contentType)) {
-            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);
-            document.close();
-            return new ByteArrayInputStream(os.toByteArray());
-        }
-        return null;
-    }
 }
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/transformation/CropHandler.java b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/CropHandler.java
new file mode 100644
index 0000000..ee9f8e7
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/CropHandler.java
@@ -0,0 +1,49 @@
+/*
+ * 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.cms.core.internal.transformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.sling.cms.transformation.TransformationHandler;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails.Builder;
+import net.coobird.thumbnailator.geometry.Positions;
+
+/**
+ * A transformation handler to crop images
+ */
+@Component(service = TransformationHandler.class)
+public class CropHandler implements TransformationHandler {
+
+    @Override
+    public boolean applies(String command) {
+        return command.startsWith("crop-");
+    }
+
+    @Override
+    public void handle(Builder<? extends InputStream> builder, String cmd) throws IOException {
+        try {
+            Positions pos = Positions.valueOf(cmd.split("\\-")[1].toUpperCase());
+            builder.crop(pos);
+        } catch (IllegalArgumentException e) {
+            throw new IOException("Unable to load crop position from " + cmd.split("\\-")[1], e);
+        }
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImpl.java
new file mode 100644
index 0000000..49e750a
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImpl.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.cms.core.internal.transformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.cms.File;
+import org.apache.sling.cms.transformation.FileThumbnailTransformer;
+import org.apache.sling.cms.transformation.OutputFileFormat;
+import org.apache.sling.cms.transformation.ThumbnailProvider;
+import org.apache.sling.cms.transformation.TransformationHandler;
+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 = FileThumbnailTransformer.class)
+public class FileThumbnailTransformerImpl implements FileThumbnailTransformer {
+
+    private static final Logger log = LoggerFactory.getLogger(FileThumbnailTransformerImpl.class);
+
+    @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policyOption = ReferencePolicyOption.GREEDY)
+    private List<TransformationHandler> handlers;
+
+    @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policyOption = ReferencePolicyOption.GREEDY)
+    private List<ThumbnailProvider> thumbnailProviders;
+
+    @Override
+    public TransformationHandler getTransformationHandler(String command) {
+        return handlers.stream().filter(h -> h.applies(command)).findFirst().orElse(null);
+    }
+
+    /**
+     * @param handlers the handlers to set
+     */
+    public void setHandlers(List<TransformationHandler> handlers) {
+        this.handlers = handlers;
+    }
+
+    /**
+     * @param thumbnailProviders the thumbnailProviders to set
+     */
+    public void setThumbnailProviders(List<ThumbnailProvider> thumbnailProviders) {
+        this.thumbnailProviders = thumbnailProviders;
+    }
+
+    @Override
+    public void transformFile(File file, String[] commands, OutputFileFormat format, OutputStream out)
+            throws IOException {
+        ThumbnailProvider provider = thumbnailProviders.stream().filter(tp -> tp.applies(file)).findFirst()
+                .orElseThrow(() -> new IOException("Unable to find thumbnail provider for: " + file.getPath()));
+        log.debug("Using thumbnail provider {} for file {}", provider, file);
+        Builder<? extends InputStream> builder = Thumbnails.of(provider.getThumbnail(file));
+        for (String command : commands) {
+            if (StringUtils.isNotBlank(command)) {
+                log.debug("Handling command: {}", command);
+                TransformationHandler handler = getTransformationHandler(command);
+                if (handler != null) {
+                    log.debug("Invoking handler {} for command {}", handler.getClass().getCanonicalName(), command);
+                    handler.handle(builder, command);
+                } else {
+                    log.info("No handler found for: {}", command);
+                }
+            }
+        }
+        builder.outputFormat(format.toString());
+        builder.toOutputStream(out);
+    }
+
+    @Override
+    public void transformFile(SlingHttpServletRequest request, OutputStream out) throws IOException {
+        OutputFileFormat fileFormat = OutputFileFormat.forRequest(request);
+        transformFile(request.getResource().adaptTo(File.class),
+                Optional.ofNullable(request.getRequestPathInfo().getSuffix())
+                        .map(s -> StringUtils.substringBeforeLast(s, ".")).map(s -> s.split("/")).orElse(new String[0]),
+                fileFormat, out);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProvider.java b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProvider.java
new file mode 100644
index 0000000..1b7e747
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProvider.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.cms.core.internal.transformation;
+
+import java.io.InputStream;
+
+import org.apache.sling.cms.File;
+import org.apache.sling.cms.transformation.ThumbnailProvider;
+import org.osgi.service.component.annotations.Component;
+
+import com.google.common.net.MediaType;
+
+/**
+ * A thumbnail provider for image files.
+ */
+@Component(service = ThumbnailProvider.class)
+public class ImageThumbnailProvider implements ThumbnailProvider {
+
+    @Override
+    public boolean applies(File file) {
+        return MediaType.parse(file.getContentType()).is(MediaType.ANY_IMAGE_TYPE);
+    }
+
+    @Override
+    public InputStream getThumbnail(File file) {
+        return file.getResource().adaptTo(InputStream.class);
+    }
+
+}
diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProvider.java b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProvider.java
new file mode 100644
index 0000000..5087a17
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProvider.java
@@ -0,0 +1,58 @@
+/*
+ * 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.cms.core.internal.transformation;
+
+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 org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.rendering.ImageType;
+import org.apache.pdfbox.rendering.PDFRenderer;
+import org.apache.sling.cms.File;
+import org.apache.sling.cms.transformation.ThumbnailProvider;
+import org.osgi.service.component.annotations.Component;
+
+import com.google.common.net.MediaType;
+
+/**
+ * A thumbnail provider for PDF documents.
+ */
+@Component(service = ThumbnailProvider.class)
+public class PdfThumbnailProvider implements ThumbnailProvider {
+
+    @Override
+    public boolean applies(File file) {
+        return MediaType.PDF.is(MediaType.parse(file.getContentType()));
+    }
+
+    @Override
+    public InputStream getThumbnail(File file) throws IOException {
+        try (PDDocument document = PDDocument.load(file.getResource().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/core/src/main/java/org/apache/sling/cms/core/internal/transformation/SizeHandler.java b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/SizeHandler.java
new file mode 100644
index 0000000..9f44122
--- /dev/null
+++ b/core/src/main/java/org/apache/sling/cms/core/internal/transformation/SizeHandler.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.cms.core.internal.transformation;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.apache.sling.cms.transformation.TransformationHandler;
+import org.osgi.service.component.annotations.Component;
+
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+@Component(service = TransformationHandler.class)
+public class SizeHandler implements TransformationHandler {
+
+    @Override
+    public boolean applies(String command) {
+        return command.startsWith("size-");
+    }
+
+    @Override
+    public void handle(Builder<? extends InputStream> builder, String cmd) throws IOException {
+        int width = -1;
+        try {
+            width = Integer.parseInt(cmd.split("\\-")[1], 10);
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Failed to get width from " + cmd.split("\\-")[1]);
+        }
+
+        int height = -1;
+        try {
+            height = Integer.parseInt(cmd.split("\\-")[2], 10);
+        } catch (NumberFormatException nfe) {
+            throw new IOException("Failed to get height from " + cmd.split("\\-")[2]);
+        }
+        builder.size(width, height);
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.java b/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.java
new file mode 100644
index 0000000..aaef06b
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/helpers/SlingCMSContextHelper.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.cms.core.helpers;
+
+import java.io.InputStream;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+
+import com.google.common.base.Function;
+
+public class SlingCMSContextHelper {
+
+    public static final void initContext(SlingContext context) {
+        context.addModelsForPackage("org.apache.sling.cms.core.internal.models");
+        context.addModelsForPackage("org.apache.sling.cms.core.models");
+
+        context.load().json("/content.json", "/content");
+        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.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/core/src/test/java/org/apache/sling/cms/core/internal/transformation/CropHandlerTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/CropHandlerTest.java
new file mode 100644
index 0000000..da21d10
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/CropHandlerTest.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.cms.core.internal.transformation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+public class CropHandlerTest {
+    
+    private Builder<? extends InputStream> builder;
+    private CropHandler cropper;
+
+    @Before 
+    public void init() {
+        builder = Thumbnails.of(getClass().getClassLoader().getResourceAsStream("apache.png"));
+        builder.size(200, 200);
+        cropper = new CropHandler();
+    }
+
+    @Test
+    public void testApplies() {
+        assertTrue(cropper.applies("crop-CENTER"));
+    }
+
+    @Test
+    public void testCrop() throws IOException {
+        cropper.handle(builder, "crop-CENTER");
+        assertNotNull(builder.asBufferedImage());
+    }
+    
+
+    @Test
+    public void testCropLower() throws IOException {
+        cropper.handle(builder, "crop-center");
+        assertNotNull(builder.asBufferedImage());
+    }
+    
+    @Test
+    public void testInvalidParam() throws IOException {
+        try {
+            cropper.handle(builder, "crop-CENTERZ");
+            fail();
+        }catch(IOException e) {
+        }
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImplTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImplTest.java
new file mode 100644
index 0000000..6cd90ce
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/FileThumbnailTransformerImplTest.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.cms.core.internal.transformation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+
+import org.apache.poi.util.IOUtils;
+import org.apache.sling.cms.core.helpers.SlingCMSContextHelper;
+import org.apache.sling.cms.transformation.FileThumbnailTransformer;
+import org.apache.sling.cms.transformation.ThumbnailProvider;
+import org.apache.sling.cms.transformation.TransformationHandler;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import com.google.common.collect.Lists;
+
+public class FileThumbnailTransformerImplTest {
+    @Rule
+    public final SlingContext context = new SlingContext();
+    private FileThumbnailTransformer transformer;
+
+    @Before
+    public void init() {
+        SlingCMSContextHelper.initContext(context);
+        transformer = new FileThumbnailTransformerImpl();
+        ((FileThumbnailTransformerImpl) transformer)
+                .setHandlers(Lists.asList(new CropHandler(), new TransformationHandler[] { new SizeHandler() }));
+        ((FileThumbnailTransformerImpl) transformer).setThumbnailProviders(
+                Lists.asList(new ImageThumbnailProvider(), new ThumbnailProvider[] { new PdfThumbnailProvider() }));
+    }
+
+    @Test
+    public void testGetHandler() {
+        assertNotNull(transformer.getTransformationHandler("size-200-200"));
+        assertNull(transformer.getTransformationHandler("sizey-200-200"));
+    }
+
+
+    @Test
+    public void testImageThumbnail() throws IOException {
+        context.requestPathInfo().setSuffix("/not-a-command/size-200-200/crop-center.png");
+        context.currentResource("/content/apache/sling-apache-org/index/apache.png");
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        transformer.transformFile(context.request(), baos);
+        assertNotNull(baos);
+        
+        IOUtils.copy(new ByteArrayInputStream(baos.toByteArray()), new File("src/test/resources/thumbnail.png"));
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProviderTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProviderTest.java
new file mode 100644
index 0000000..01f7137
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/ImageThumbnailProviderTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.cms.core.internal.transformation;
+
+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.cms.File;
+import org.apache.sling.cms.core.helpers.SlingCMSContextHelper;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+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 File imageFile;
+    private File pdfFile;
+
+    @Before
+    public void init() {
+        SlingCMSContextHelper.initContext(context);
+
+        imageFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/apache.png")
+                .adaptTo(File.class);
+
+        pdfFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/sling.pdf")
+                .adaptTo(File.class);
+    }
+
+    @Test
+    public void testContentTypes() throws IOException {
+        log.info("testContentTypes");
+        ImageThumbnailProvider itp = new ImageThumbnailProvider();
+
+        assertTrue(itp.applies(imageFile));
+        assertFalse(itp.applies(pdfFile));
+    }
+
+    @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/core/src/test/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProviderTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProviderTest.java
new file mode 100644
index 0000000..c2acbbf
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/PdfThumbnailProviderTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.cms.core.internal.transformation;
+
+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.cms.File;
+import org.apache.sling.cms.core.helpers.SlingCMSContextHelper;
+import org.apache.sling.testing.mock.sling.junit.SlingContext;
+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 File imageFile;
+    private File pdfFile;
+
+    @Before
+    public void init() {
+        SlingCMSContextHelper.initContext(context);
+        imageFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/apache.png")
+                .adaptTo(File.class);
+        pdfFile = context.resourceResolver().getResource("/content/apache/sling-apache-org/index/sling.pdf")
+                .adaptTo(File.class);
+    }
+
+    @Test
+    public void testContentTypes() throws IOException {
+        log.info("testContentTypes");
+        PdfThumbnailProvider ptp = new PdfThumbnailProvider();
+        assertFalse(ptp.applies(imageFile));
+        assertTrue(ptp.applies(pdfFile));
+    }
+
+    @Test
+    public void testPDFThumbnailProvider() throws IOException {
+        log.info("testPDFThumbnailProvider");
+        PdfThumbnailProvider ptp = new PdfThumbnailProvider();
+        assertNotNull(ptp.getThumbnail(pdfFile));
+    }
+
+}
diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/transformation/SizeHandlerTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/SizeHandlerTest.java
new file mode 100644
index 0000000..762121d
--- /dev/null
+++ b/core/src/test/java/org/apache/sling/cms/core/internal/transformation/SizeHandlerTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.cms.core.internal.transformation;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import net.coobird.thumbnailator.Thumbnails;
+import net.coobird.thumbnailator.Thumbnails.Builder;
+
+public class SizeHandlerTest {
+    
+    private Builder<? extends InputStream> builder;
+    private SizeHandler sizer;
+
+    @Before 
+    public void init() {
+        builder = Thumbnails.of(getClass().getClassLoader().getResourceAsStream("apache.png"));
+        sizer = new SizeHandler();
+    }
+
+    @Test
+    public void testApplies() {
+        assertTrue(sizer.applies("size-200-200"));
+    }
+
+    @Test
+    public void testResize() throws IOException {
+        sizer.handle(builder, "size-200-200");
+        assertNotNull(builder.asBufferedImage());
+    }
+    
+    @Test
+    public void testInvalidParam() throws IOException {
+        try {
+            sizer.handle(builder, "size-k-200");
+            fail();
+        }catch(IOException e) {
+        }
+        try {
+            sizer.handle(builder, "size-222-h");
+            fail();
+        }catch(IOException e) {
+        }
+    }
+
+}
diff --git a/core/src/test/resources/content.json b/core/src/test/resources/content.json
new file mode 100644
index 0000000..82e8ffc
--- /dev/null
+++ b/core/src/test/resources/content.json
@@ -0,0 +1,109 @@
+{
+    "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": "sling:File"
+            },
+            "sling.pdf": {
+                "jcr:primaryType": "sling:File"
+            },
+            "sling.docx": {
+                "jcr:primaryType": "sling:File"
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/src/test/resources/simplelogger.properties b/core/src/test/resources/simplelogger.properties
deleted file mode 100644
index c33d3ba..0000000
--- a/core/src/test/resources/simplelogger.properties
+++ /dev/null
@@ -1,17 +0,0 @@
-#
-# 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.
-#
-org.slf4j.simpleLogger.defaultLogLevel=debug
\ No newline at end of file
diff --git a/core/src/test/resources/sling.pdf b/core/src/test/resources/sling.pdf
new file mode 100644
index 0000000..a443389
Binary files /dev/null and b/core/src/test/resources/sling.pdf differ
diff --git a/pom.xml b/pom.xml
index f186526..05fcc66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -38,6 +38,7 @@
         <module>api</module>
         <module>core</module>
         <module>metadata-extractor</module>
+        <module>thumbnailator</module>
         <module>ui</module>
         <module>reference</module>
         <module>builder</module>
@@ -238,6 +239,30 @@
                 <version>2.27.0</version>
                 <scope>test</scope>
             </dependency>
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>15.0</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
+                <groupId>net.coobird</groupId>
+                <artifactId>thumbnailator</artifactId>
+                <version>[0.4, 0.5)</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.poi</groupId>
+                <artifactId>poi-ooxml</artifactId>
+                <version>4.0.1</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.sling</groupId>
+                <artifactId>org.apache.sling.testing.sling-mock.junit4</artifactId>
+                <version>2.3.0</version>
+                <scope>test</scope>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 
diff --git a/api/pom.xml b/thumbnailator/pom.xml
similarity index 68%
copy from api/pom.xml
copy to thumbnailator/pom.xml
index 9cd4af5..a1d8b2e 100644
--- a/api/pom.xml
+++ b/thumbnailator/pom.xml
@@ -13,10 +13,10 @@
         <groupId>org.apache.sling</groupId>
         <version>0.11.3-SNAPSHOT</version>
     </parent>
-    <artifactId>org.apache.sling.cms.api</artifactId>
+    <artifactId>org.apache.sling.cms.thumbnailator</artifactId>
     <packaging>bundle</packaging>
-    <name>Apache Sling - CMS API</name>
-    <description>An API for the Apache Sling Reference CMS Application</description>
+    <name>Apache Sling - Thumbnailator Wrapper</name>
+    <description>OSGi Bundle Wrapper for https://github.com/coobird/thumbnailator</description>
 
     <properties>
         <sling.java.version>8</sling.java.version>
@@ -28,6 +28,12 @@
                 <groupId>org.apache.felix</groupId>
                 <artifactId>maven-bundle-plugin</artifactId>
                 <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency>
+                        <Export-Package>net.coobird.thumbnailator.*</Export-Package>
+                    </instructions>
+                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
@@ -46,6 +52,13 @@
                     <password>${sling.password}</password>
                 </configuration>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <useSystemClassLoader>false</useSystemClassLoader>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
@@ -73,40 +86,9 @@
 
     <dependencies>
         <dependency>
-            <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.event.api</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.jcr.resource</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>commons-lang</groupId>
-            <artifactId>commons-lang</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.jackrabbit</groupId>
-            <artifactId>jackrabbit-jcr-commons</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.osgi</groupId>
-            <artifactId>osgi.annotation</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.jetbrains</groupId>
-            <artifactId>annotations</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.jsoup</groupId>
-            <artifactId>jsoup</artifactId>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-api</artifactId>
+            <groupId>net.coobird</groupId>
+            <artifactId>thumbnailator</artifactId>
+            <scope>compile</scope>
         </dependency>
     </dependencies>
 </project>
\ No newline at end of file