Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit.

  • Loading branch information...
commit d264252512830d2d5d24763af306c212ead53693 0 parents
Jake Wharton JakeWharton authored
26 .gitignore
@@ -0,0 +1,26 @@
+# Eclipse
+.classpath
+.project
+.settings
+eclipsebin
+
+# Ant
+bin
+gen
+build
+out
+lib
+
+# Maven
+target
+pom.xml.*
+release.properties
+
+# IntelliJ
+.idea
+*.iml
+*.iws
+*.ipr
+classes
+
+.DS_Store
7 CHANGELOG.md
@@ -0,0 +1,7 @@
+Change Log
+==========
+
+Version 1.0.0 *(In Development)*
+--------------------------------
+
+Initial release.
202 LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
77 README.md
@@ -0,0 +1,77 @@
+Thumbor Java Client by Square
+=============================
+
+Standalone Java client for the [Thumbor image service][1] which allows you to
+build URIs in an expressive fashion using the fluent pattern.
+
+This library is also fully compatible with the Android platform.
+
+
+Examples
+--------
+
+```java
+build("http://example.com/image.png")
+ .resize(48, 48)
+// Outputs: /unsafe/48x48/example.com/image.png
+
+build("http://example.com/image.png")
+ .crop(10, 10, 90, 90)
+ .resize(40, 40)
+ .smart()
+// Outputs: /unsafe/10x10:90x90/smart/40x40/example.com/image.png
+
+build("http://example.com/image.png")
+ .crop(5, 5, 195, 195)
+ .resize(95, 95)
+ .align(BOTTOM, RIGHT)
+// Outputs: /unsafe/5x5:195x195/right/bottom/95x95/example.com/image.png
+
+build("http://example.com/background.png")
+ .resize(200, 100)
+ .filter(
+ roundCorner(10),
+ watermark(build("http://example.com/overlay1.png").resize(200, 100)),
+ watermark(build("http://example.com/overlay2.png").resize(50, 50), 75, 25),
+ quality(85)
+ )
+// Outputs: /unsafe/200x100/filters:round_corner(10,255,255,255):watermark(/unsafe/200x100/example.com/overlay1.png,0,0,0):watermark(/unsafe/50x50/example.com/overlay2.png,75,25,0):quality(85)/example.com/background.png
+
+build("http://example.com/image.png")
+ .resize(48, 48)
+ .key("super secret key")
+// Outputs: /ttdl3uu1vOdz7mxsjegdi6Q4iUuYq7IWPziAiW53Cff683quusS17Q-piahoiqd1/example.com/image.png
+```
+
+
+Building
+--------
+
+Compilation requires Maven 3.0 or newer. To compile a JAR run `mvn clean verify`
+in the project root folder. The assembled file will be in the `target/`
+directory.
+
+If you are modifying the source files and the build fails due to checkstyle you
+can see all of the errors in the `target/checkstyle-result.xml` file.
+
+
+
+License
+=======
+
+ Copyright 2012 Square, Inc.
+
+ Licensed 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.
+
+
+ [1]: https://github.com/globocom/thumbor
120 checkstyle.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+ <module name="NewlineAtEndOfFile"/>
+ <module name="FileLength"/>
+ <module name="FileTabCharacter"/>
+
+ <!-- Trailing spaces -->
+ <module name="RegexpSingleline">
+ <property name="format" value="\s+$"/>
+ <property name="message" value="Line has trailing spaces."/>
+ </module>
+
+ <module name="TreeWalker">
+ <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <!--module name="JavadocMethod"/-->
+ <module name="JavadocType"/>
+ <!--module name="JavadocVariable"/-->
+ <module name="JavadocStyle"/>
+
+
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <module name="ConstantName"/>
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <module name="LineLength">
+ <property name="max" value="120"/>
+ </module>
+ <module name="MethodLength"/>
+ <module name="ParameterNumber"/>
+
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="GenericWhitespace"/>
+ <module name="EmptyForIteratorPad"/>
+ <module name="MethodParamPad"/>
+ <module name="NoWhitespaceAfter"/>
+ <module name="NoWhitespaceBefore"/>
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/>
+
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <!--module name="ModifierOrder"/-->
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <module name="EmptyBlock"/>
+ <module name="LeftCurly"/>
+ <module name="NeedBraces"/>
+ <module name="RightCurly"/>
+
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <!--module name="AvoidInlineConditionals"/-->
+ <module name="CovariantEquals"/>
+ <module name="DoubleCheckedLocking"/>
+ <module name="EmptyStatement"/>
+ <module name="EqualsAvoidNull"/>
+ <module name="EqualsHashCode"/>
+ <!--module name="HiddenField"/-->
+ <module name="IllegalInstantiation"/>
+ <module name="InnerAssignment"/>
+ <!--module name="MagicNumber"/-->
+ <module name="MissingSwitchDefault"/>
+ <module name="RedundantThrows"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <module name="DesignForExtension"/>
+ <module name="FinalClass"/>
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <!--s/module name="VisibilityModifier"/-->
+
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <!--module name="FinalParameters"/-->
+ <module name="TodoComment"/>
+ <module name="UpperEll"/>
+ </module>
+</module>
93 pom.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<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>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
+ </parent>
+
+ <groupId>com.squareup</groupId>
+ <artifactId>thumbor-client</artifactId>
+ <packaging>jar</packaging>
+ <version>1.0.0-SNAPSHOT</version>
+
+ <name>Thumbor Client</name>
+ <description>Pure Java client for the Thumbor image service which allows you to build URIs in an expressive fashion using the fluent pattern.</description>
+ <inceptionYear>2012</inceptionYear>
+
+ <scm>
+ <url>http://github.com/square/thumbor-java</url>
+ <connection>scm:git:git://github.com/square/thumbor-java.git</connection>
+ <developerConnection>scg:git:git@github.com:square/thumbor-java.git</developerConnection>
+ </scm>
+
+ <organization>
+ <name>Square, Inc.</name>
+ <url>http://squareup.com</url>
+ </organization>
+
+ <issueManagement>
+ <system>GitHub Issues</system>
+ <url>http://github.com/square/thumbor-java/issues</url>
+ </issueManagement>
+
+ <licenses>
+ <license>
+ <name>Apache License Version 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ <distribution>repo</distribution>
+ </license>
+ </licenses>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+
+ <java.version>1.6</java.version>
+ <junit.version>4.10</junit.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>2.5</version>
+ <configuration>
+ <source>${java.version}</source>
+ <target>${java.version}</target>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-checkstyle-plugin</artifactId>
+ <version>2.9.1</version>
+ <configuration>
+ <failsOnError>true</failsOnError>
+ <configLocation>checkstyle.xml</configLocation>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>verify</phase>
+ <goals>
+ <goal>checkstyle</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
637 src/main/java/com/squareup/thumbor/ThumborUri.java
@@ -0,0 +1,637 @@
+// Copyright 2012 Square, Inc.
+package com.squareup.thumbor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.squareup.thumbor.Utilities.aes128Encrypt;
+import static com.squareup.thumbor.Utilities.md5;
+import static com.squareup.thumbor.Utilities.normalizeString;
+import static com.squareup.thumbor.Utilities.stripProtocolAndParams;
+
+/**
+ * Fluent interface to create a URI appropriate for passing to Thumbor.
+ *
+ * @see #build(String)
+ */
+public final class ThumborUri {
+ /**
+ * Horizontal alignment for crop positioning.
+ */
+ public enum HorizontalAlign {
+ LEFT("left"), CENTER("center"), RIGHT("right");
+
+ final String value;
+
+ private HorizontalAlign(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Vertical alignment for crop positioning.
+ */
+ public enum VerticalAlign {
+ TOP("top"), MIDDLE("middle"), BOTTOM("bottom");
+
+ final String value;
+
+ private VerticalAlign(String value) {
+ this.value = value;
+ }
+ }
+
+ /**
+ * Exception denoting that a fatal error occurred while assembling the URI for the current configuration.
+ *
+ * @see #getCause()
+ */
+ public static class UnableToBuildException extends RuntimeException {
+ public UnableToBuildException(Throwable e) {
+ super(e);
+ }
+ }
+
+ final String target;
+ String key;
+ boolean hasCrop = false;
+ boolean hasResize = false;
+ boolean isSmart = false;
+ boolean flipHorizontally = false;
+ boolean flipVertically = false;
+ boolean fitIn = false;
+ int resizeWidth;
+ int resizeHeight;
+ int cropTop;
+ int cropLeft;
+ int cropBottom;
+ int cropRight;
+ HorizontalAlign cropHorizontalAlign;
+ VerticalAlign cropVerticalAlign;
+ List<String> filters;
+
+ /**
+ * Start a new Thumbor URI configuration for the specified target image URL.
+ *
+ * @param target Target image URL.
+ */
+ ThumborUri(String target) {
+ this.target = stripProtocolAndParams(target);
+ }
+
+ /**
+ * Start building an image URI for Thumbor.
+ *
+ * @param target Target image to manipulate.
+ * @return New instance for configuration.
+ */
+ public static ThumborUri build(String target) {
+ if (target == null || target.length() == 0) {
+ throw new IllegalArgumentException("Target image URI must not be blank.");
+ }
+ return new ThumborUri(target);
+ }
+
+ /**
+ * Set a key for secure URI generation. This will default the {@link #toString()} to call {@link #buildSafe()}.
+ *
+ * @param key Security key for remote server.
+ * @return Current instance.
+ */
+ public ThumborUri key(String key) {
+ if (key == null || key.length() == 0) {
+ throw new IllegalArgumentException("Key must not be blank.");
+ }
+ this.key = key;
+ return this;
+ }
+
+ /**
+ * Resize picture to desired size.
+ *
+ * @param width Desired width.
+ * @param height Desired height.
+ * @return Current instance.
+ */
+ public ThumborUri resize(int width, int height) {
+ if (width < 1) {
+ throw new IllegalArgumentException("Width must be greater than zero.");
+ }
+ if (height < 1) {
+ throw new IllegalArgumentException("Height must be greater than zero.");
+ }
+ hasResize = true;
+ resizeWidth = width;
+ resizeHeight = height;
+ return this;
+ }
+
+ /**
+ * Flip the image horizontally.
+ *
+ * @return Current instance.
+ */
+ public ThumborUri flipHorizontally() {
+ if (!hasResize) {
+ throw new IllegalStateException("Image must be resized first in order to flip.");
+ }
+ flipHorizontally = true;
+ return this;
+ }
+
+ /**
+ * Flip the image vertically.
+ *
+ * @return Current instance.
+ */
+ public ThumborUri flipVertically() {
+ if (!hasResize) {
+ throw new IllegalStateException("Image must be resized first in order to flip.");
+ }
+ flipVertically = true;
+ return this;
+ }
+
+ /**
+ * Contrain the image size inside the resized box, scaling as needed.
+ *
+ * @return Current instance.
+ */
+ public ThumborUri fitIn() {
+ if (!hasResize) {
+ throw new IllegalStateException("Image must be resized first in order to apply 'fit-in'.");
+ }
+ fitIn = true;
+ return this;
+ }
+
+ /**
+ * Crop the image between two points.
+ *
+ * @param top Top bound.
+ * @param left Left bound.
+ * @param bottom Bottom bound.
+ * @param right Right bound.
+ * @return Current instance.
+ */
+ public ThumborUri crop(int top, int left, int bottom, int right) {
+ if (top < 0) {
+ throw new IllegalArgumentException("Top must be greater or equal to zero.");
+ }
+ if (left < 0) {
+ throw new IllegalArgumentException("Left must be greater or equal to zero.");
+ }
+ if (bottom < 1 || bottom <= top) {
+ throw new IllegalArgumentException("Bottom must be greater than zero and top.");
+ }
+ if (right < 1 || right <= left) {
+ throw new IllegalArgumentException("Right must be greater than zero and left.");
+ }
+ hasCrop = true;
+ cropTop = top;
+ cropLeft = left;
+ cropBottom = bottom;
+ cropRight = right;
+ return this;
+ }
+
+ /**
+ * Set the horizontal alignment for the image when cropping.
+ *
+ * @param align Horizontal alignment.
+ * @return Current instance.
+ */
+ public ThumborUri align(HorizontalAlign align) {
+ if (!hasCrop) {
+ throw new IllegalStateException("Image must be cropped first in order to align.");
+ }
+ cropHorizontalAlign = align;
+ return this;
+ }
+
+ /**
+ * Set the vertical alignment for the image when cropping.
+ *
+ * @param align Vertical alignment.
+ * @return Current instance.
+ */
+ public ThumborUri align(VerticalAlign align) {
+ if (!hasCrop) {
+ throw new IllegalStateException("Image must be cropped first in order to align.");
+ }
+ cropVerticalAlign = align;
+ return this;
+ }
+
+ /**
+ * Set the horizontal and vertical alignment for the image when cropping.
+ *
+ * @param valign Vertical alignment.
+ * @param halign Horizontal alignment.
+ * @return Current instance.
+ */
+ public ThumborUri align(VerticalAlign valign, HorizontalAlign halign) {
+ return align(valign).align(halign);
+ }
+
+ /**
+ * Use smart cropping for determining the imortant portion of an image.
+ *
+ * @return Current instance.
+ */
+ public ThumborUri smart() {
+ if (!hasCrop) {
+ throw new IllegalStateException("Image must be cropped first in order to smart align.");
+ }
+ isSmart = true;
+ return this;
+ }
+
+ /**
+ * Add one or more filters to the image.
+ *
+ * @param filters Filter strings.
+ * @return Current instance.
+ * @see #brightness(int)
+ * @see #contrast(int)
+ * @see #fill
+ * @see #noise(int)
+ * @see #quality(int)
+ * @see #rgb(int, int, int)
+ * @see #roundCorner(int)
+ * @see #roundCorner(int, int)
+ * @see #roundCorner(int, int, int)
+ * @see #sharpen(float, float, boolean)
+ * @see #watermark(String, int, int)
+ * @see #watermark(ThumborUri, int, int)
+ * @see #watermark(String, int, int, int)
+ * @see #watermark(ThumborUri, int, int, int)
+ */
+ public ThumborUri filter(String... filters) {
+ if (filters.length == 0) {
+ throw new IllegalArgumentException("You must provide at least one filter.");
+ }
+ if (this.filters == null) {
+ this.filters = new ArrayList<String>(1);
+ }
+ for (String filter : filters) {
+ if (filter == null || filter.length() == 0) {
+ throw new IllegalArgumentException("Filter must not be blank.");
+ }
+ this.filters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Build an unsafe version of the URI.
+ *
+ * @return Unsafe URI for the current configuration.
+ */
+ public String buildUnsafe() {
+ return new StringBuilder("/unsafe/").append(assembleConfig()).append("/").append(target).toString();
+ }
+
+ /**
+ * Build a safe version of the URI. Requires a prior call to {@link #key(String)}.
+ *
+ * @return Safe URI for the current configuration.
+ */
+ public String buildSafe() {
+ if (key == null) {
+ throw new IllegalStateException("Cannot build safe URI without a key.");
+ }
+
+ // Assemble config and an MD5 of the target image.
+ StringBuilder config = assembleConfig().append("/").append(md5(target));
+ final byte[] encrypted = aes128Encrypt(config, normalizeString(key, 16));
+
+ // URI-safe Base64 encode.
+ final String encoded = Utilities.base64Encode(encrypted);
+
+ return new StringBuilder("/").append(encoded).append("/").append(target).toString();
+ }
+
+ /**
+ * Build a URI for fetching Thumbor metadata.
+ *
+ * @return Meta URI for the current configuration.
+ */
+ public String buildMeta() {
+ return new StringBuilder("/meta/").append(assembleConfig()).append("/").append(target).toString();
+ }
+
+ @Override public String toString() {
+ return (key == null) ? buildUnsafe() : buildSafe();
+ }
+
+ /**
+ * Assembly the configuration section of the URI.
+ *
+ * @return Configuration assembled in a {@link StringBuilder}.
+ */
+ StringBuilder assembleConfig() {
+ StringBuilder builder = new StringBuilder();
+
+ if (hasCrop) {
+ builder.append("/").append(cropLeft).append("x").append(cropTop) //
+ .append(":").append(cropRight).append("x").append(cropBottom);
+
+ if (isSmart) {
+ builder.append("/smart");
+ } else {
+ if (cropHorizontalAlign != null) {
+ builder.append("/").append(cropHorizontalAlign.value);
+ }
+ if (cropVerticalAlign != null) {
+ builder.append("/").append(cropVerticalAlign.value);
+ }
+ }
+ }
+
+ if (hasResize) {
+ builder.append("/");
+ if (flipHorizontally) {
+ builder.append("-");
+ }
+ builder.append(resizeWidth).append("x");
+ if (flipVertically) {
+ builder.append("-");
+ }
+ builder.append(resizeHeight);
+
+ if (fitIn) {
+ builder.append("/fit-in");
+ }
+ }
+
+ if (filters != null) {
+ builder.append("/filters");
+ for (String filter : filters) {
+ builder.append(":").append(filter);
+ }
+ }
+
+ if (builder.length() > 0) {
+ builder.deleteCharAt(0);
+ }
+ return builder;
+ }
+
+ /**
+ * This filter increases or decreases the image brightness.
+ *
+ * @param amount -100 to 100 - The amount (in %) to change the image brightness. Positive numbers
+ * make the image brighter and negative numbers make the image darker.
+ * @return String representation of this filter.
+ */
+ public static String brightness(int amount) {
+ if (amount < -100 || amount > 100) {
+ throw new IllegalArgumentException("Amount must be between -100 and 100, inclusive.");
+ }
+ return new StringBuilder("brightness(").append(amount).append(")").toString();
+ }
+
+ /**
+ * The filter increases or decreases the image contrast.
+ *
+ * @param amount -100 to 100 - The amount (in %) to change the image contrast. Positive numbers
+ * increase contrast and negative numbers decrease contrast.
+ * @return String representation of this filter.
+ */
+ public static String contrast(int amount) {
+ if (amount < -100 || amount > 100) {
+ throw new IllegalArgumentException("Amount must be between -100 and 100, inclusive.");
+ }
+ return new StringBuilder("contrast(").append(amount).append(")").toString();
+ }
+
+ /**
+ * This filter adds noise to the image.
+ *
+ * @param amount 0 to 100 - The amount (in %) of noise to add to the image.
+ * @return String representation of this filter.
+ */
+ public static String noise(int amount) {
+ if (amount < 0 || amount > 100) {
+ throw new IllegalArgumentException("Amount must be between 0 and 100, inclusive");
+ }
+ return new StringBuilder("noise(").append(amount).append(")").toString();
+ }
+
+ /**
+ * This filter changes the overall quality of the JPEG image (does nothing for PNGs or GIFs).
+ *
+ * @param amount 0 to 100 - The quality level (in %) that the end image will feature.
+ * @return String representation of this filter.
+ */
+ public static String quality(int amount) {
+ if (amount < 0 || amount > 100) {
+ throw new IllegalArgumentException("Amount must be between 0 and 100, inclusive.");
+ }
+ return new StringBuilder("quality(").append(amount).append(")").toString();
+ }
+
+ /**
+ * This filter changes the amount of color in each of the three channels.
+ *
+ * @param r The amount of redness in the picture. Can range from -100 to 100 in percentage.
+ * @param g The amount of greenness in the picture. Can range from -100 to 100 in percentage.
+ * @param b The amount of blueness in the picture. Can range from -100 to 100 in percentage.
+ * @return String representation of this filter.
+ */
+ public static String rgb(int r, int g, int b) {
+ if (r < -100 || r > 100) {
+ throw new IllegalArgumentException("Redness value must be between -100 and 100, inclusive.");
+ }
+ if (g < -100 || g > 100) {
+ throw new IllegalArgumentException("Greenness value must be between -100 and 100, inclusive.");
+ }
+ if (b < -100 || b > 100) {
+ throw new IllegalArgumentException("Blueness value must be between -100 and 100, inclusive.");
+ }
+ return new StringBuilder("rgb(") //
+ .append(r).append(",") //
+ .append(g).append(",") //
+ .append(b).append(")") //
+ .toString();
+ }
+
+ /**
+ * This filter adds rounded corners to the image using the specified color as background.
+ *
+ * @param radius amount of pixels to use as radius.
+ * @return String representation of this filter.
+ */
+ public static String roundCorner(int radius) {
+ return roundCorner(radius, 0xFFFFFF);
+ }
+
+ /**
+ * This filter adds rounded corners to the image using the specified color as background.
+ *
+ * @param radius amount of pixels to use as radius.
+ * @param color fill color for clipped region.
+ * @return String representation of this filter.
+ */
+ public static String roundCorner(int radius, int color) {
+ return roundCorner(radius, 0, color);
+ }
+
+ /**
+ * This filter adds rounded corners to the image using the specified color as background.
+ *
+ * @param radiusInner amount of pixels to use as radius.
+ * @param radiusOuter specifies the second value for the ellipse used for the radius. Use 0 for
+ * no value.
+ * @param color fill color for clipped region.
+ * @return String representation of this filter.
+ */
+ public static String roundCorner(int radiusInner, int radiusOuter, int color) {
+ if (radiusInner < 1) {
+ throw new IllegalArgumentException("Radius must be greater than zero.");
+ }
+ if (radiusOuter < 0) {
+ throw new IllegalArgumentException("Outer radius must be greater than or equal to zero.");
+ }
+ StringBuilder builder = new StringBuilder("round_corner(").append(radiusInner);
+ if (radiusOuter > 0) {
+ builder.append("|").append(radiusOuter);
+ }
+ return builder.append(",") //
+ .append((color & 0xFF0000) >>> 16).append(",") //
+ .append((color & 0xFF00) >>> 8).append(",") //
+ .append(color & 0xFF).append(")") //
+ .toString();
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param imageUrl Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @return String representation of this filter.
+ */
+ public static String watermark(String imageUrl) {
+ return watermark(imageUrl, 0, 0);
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param image Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @return String representation of this filter.
+ */
+ public static String watermark(ThumborUri image) {
+ return watermark(image, 0, 0);
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param imageUrl Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @param x Horizontal position that the watermark will be in. Positive numbers indicate position
+ * from the left and negative numbers indicate position from the right.
+ * @param y Vertical position that the watermark will be in. Positive numbers indicate position
+ * from the top and negative numbers indicate position from the bottom.
+ * @return String representation of this filter.
+ */
+ public static String watermark(String imageUrl, int x, int y) {
+ return watermark(imageUrl, x, y, 0);
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param image Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @param x Horizontal position that the watermark will be in. Positive numbers indicate position
+ * from the left and negative numbers indicate position from the right.
+ * @param y Vertical position that the watermark will be in. Positive numbers indicate position
+ * from the top and negative numbers indicate position from the bottom.
+ * @return String representation of this filter.
+ */
+ public static String watermark(ThumborUri image, int x, int y) {
+ if (image == null) {
+ throw new IllegalArgumentException("ThumborUri must not be null.");
+ }
+ return watermark(image.toString(), x, y, 0);
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param imageUrl Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @param x Horizontal position that the watermark will be in. Positive numbers indicate position
+ * from the left and negative numbers indicate position from the right.
+ * @param y Vertical position that the watermark will be in. Positive numbers indicate position
+ * from the top and negative numbers indicate position from the bottom.
+ * @param transparency Watermark image transparency. Should be a number between 0 (fully opaque)
+ * and 100 (fully transparent).
+ * @return String representation of this filter.
+ */
+ public static String watermark(String imageUrl, int x, int y, int transparency) {
+ if (imageUrl == null || imageUrl.length() == 0) {
+ throw new IllegalArgumentException("Image URL must not be blank.");
+ }
+ if (transparency < 0 || transparency > 100) {
+ throw new IllegalArgumentException("Transparency must be between 0 and 100, inclusive.");
+ }
+ return new StringBuilder("watermark(") //
+ .append(stripProtocolAndParams(imageUrl)).append(",") //
+ .append(x).append(",") //
+ .append(y).append(",") //
+ .append(transparency).append(")") //
+ .toString();
+ }
+
+ /**
+ * This filter adds a watermark to the image.
+ *
+ * @param image Watermark image URL. It is very important to understand that the same image
+ * loader that Thumbor uses will be used here.
+ * @param x Horizontal position that the watermark will be in. Positive numbers indicate position
+ * from the left and negative numbers indicate position from the right.
+ * @param y Vertical position that the watermark will be in. Positive numbers indicate position
+ * from the top and negative numbers indicate position from the bottom.
+ * @param transparency Watermark image transparency. Should be a number between 0 (fully opaque)
+ * and 100 (fully transparent).
+ * @return String representation of this filter.
+ */
+ public static String watermark(ThumborUri image, int x, int y, int transparency) {
+ return watermark(image.toString(), x, y, transparency);
+ }
+
+ /**
+ * This filter enhances apparent sharpness of the image. It's heavily based on Marco Rossini's
+ * excellent Wavelet sharpen GIMP plugin. Check http://registry.gimp.org/node/9836 for details
+ * about how it work.
+ *
+ * @param amount Sharpen amount. Typical values are between 0.0 and 10.0.
+ * @param radius Sharpen radius. Typical values are between 0.0 and 2.0.
+ * @param luminanceOnly Sharpen only luminance channel.
+ * @return String representation of this filter.
+ */
+ public static String sharpen(float amount, float radius, boolean luminanceOnly) {
+ return new StringBuilder("sharpen(") //
+ .append(amount).append(",") //
+ .append(radius).append(",") //
+ .append(luminanceOnly).append(")") //
+ .toString();
+ }
+
+ /**
+ * This filter permit to return an image sized exactly as requested wherever is its ratio by
+ * filling with chosen color the missing parts. Usually used with "fit-in" or "adaptive-fit-in"
+ *
+ * @param color integer representation of color.
+ * @return String representation of this filter.
+ */
+ public static String fill(int color) {
+ final String colorCode = Integer.toHexString(color & 0xFFFFFF); // Strip alpha
+ return new StringBuilder("fill(").append(colorCode).append(")").toString();
+ }
+}
190 src/main/java/com/squareup/thumbor/Utilities.java
@@ -0,0 +1,190 @@
+// Copyright 2012 Square, Inc.
+package com.squareup.thumbor;
+
+import com.squareup.thumbor.ThumborUri.UnableToBuildException;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.security.MessageDigest;
+
+/**
+ * Utility methods for {@link ThumborUri}.
+ */
+final class Utilities {
+ private Utilities() {
+ // No instances.
+ }
+
+ private final static String BASE64_CHARS =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+
+ /**
+ * Base64 encodes a byte array.
+ *
+ * @param bytes Bytes to encode.
+ * @return Encoded string.
+ */
+ public static String base64Encode(byte[] bytes) {
+
+ // Every three bytes is encoded into four characters.
+ //
+ // Example:
+ // input |0 1 0 1 0 0 1 0|0 1 1 0 1 1 1 1|0 1 1 0 0 0 1 0|
+ // encode grouping |0 1 0 1 0 0|1 0 0 1 1 0|1 1 1 1 0 1|1 0 0 0 1 0|
+ // encoded ascii | U | m | 9 | i |
+
+ int triples = bytes.length / 3;
+
+ // If the number of input bytes is not a multiple of three, padding characters will be added.
+ if (bytes.length % 3 != 0) {
+ triples += 1;
+ }
+
+ // The encoded string will have four characters for every three bytes.
+ char[] encoding = new char[triples * 4];
+
+ for (int in = 0, out = 0; in < bytes.length; in += 3, out += 4) {
+ int triple = (bytes[in] & 0xff) << 16;
+ if (in + 1 < bytes.length) {
+ triple |= ((bytes[in + 1] & 0xff) << 8);
+ }
+ if (in + 2 < bytes.length) {
+ triple |= (bytes[in + 2] & 0xff);
+ }
+ encoding[out] = BASE64_CHARS.charAt((triple >> 18) & 0x3f);
+ encoding[out + 1] = BASE64_CHARS.charAt((triple >> 12) & 0x3f);
+ encoding[out + 2] = BASE64_CHARS.charAt((triple >> 6) & 0x3f);
+ encoding[out + 3] = BASE64_CHARS.charAt(triple & 0x3f);
+ }
+
+ // Add padding characters if needed.
+ for (int i = encoding.length - (triples * 3 - bytes.length); i < encoding.length; i++) {
+ encoding[i] = '=';
+ }
+
+ return String.valueOf(encoding);
+ }
+
+ /**
+ * Remove any protocol or parameters from the specified URL.
+ *
+ * @param url URL.
+ * @return Stripped URL.
+ */
+ static String stripProtocolAndParams(String url) {
+ final int length = url.length();
+
+ int start = 0;
+ int end = length;
+
+ if (url.startsWith("http://")) {
+ start = 7;
+ } else if (url.startsWith("https://")) {
+ start = 8;
+ }
+
+ int param = url.indexOf("?");
+ if (param != -1) {
+ end = param;
+ }
+
+ if (start > 0 || end < length) {
+ return url.substring(start, end);
+ }
+ return url;
+ }
+
+ /**
+ * Pad a {@link StringBuilder} to a desired multiple on the right using a specified character.
+ *
+ * @param builder Builder to pad.
+ * @param padding Padding character.
+ * @param multipleOf Number which the length must be a multiple of.
+ */
+ static void rightPadString(StringBuilder builder, char padding, int multipleOf) {
+ if (builder == null || builder.length() == 0) {
+ throw new IllegalArgumentException("Builder input must not be empty.");
+ }
+ if (multipleOf < 2) {
+ throw new IllegalArgumentException("Multiple must be greater than one.");
+ }
+ int needed = multipleOf - (builder.length() % multipleOf);
+ if (needed < multipleOf) {
+ for (int i = needed; i > 0; i--) {
+ builder.append(padding);
+ }
+ }
+ }
+
+ /**
+ * Normalize a string to a desired length by truncation or repeatedly appending itself.
+ *
+ * @param string Input string.
+ * @param desiredLength Desired length of string.
+ * @return Output string which is guaranteed to have a length equal to the desired length argument.
+ */
+ static String normalizeString(String string, int desiredLength) {
+ if (string == null || string.length() == 0) {
+ throw new IllegalArgumentException("Must supply a non-null, non-empty string.");
+ }
+ if (desiredLength < 0) {
+ throw new IllegalArgumentException("Desired length must be greater than zero.");
+ }
+ if (string.length() >= desiredLength) {
+ return string.substring(0, desiredLength);
+ } else {
+ StringBuilder builder = new StringBuilder(string);
+ while (builder.length() < desiredLength) {
+ builder.append(string);
+ }
+ return builder.substring(0, desiredLength);
+ }
+ }
+
+ /**
+ * Create an MD5 hash of a string.
+ *
+ * @param input Input string.
+ * @return Hash of input.
+ */
+ static String md5(String input) {
+ try {
+ MessageDigest algorithm = MessageDigest.getInstance("MD5");
+ algorithm.reset();
+ algorithm.update(input.getBytes());
+ byte[] messageDigest = algorithm.digest();
+
+ StringBuffer hexString = new StringBuffer();
+ for (int i = 0; i < messageDigest.length; i++) {
+ hexString.append(Integer.toHexString(0xFF & messageDigest[i] | 0x100).substring(1, 3));
+ }
+ return hexString.toString();
+ } catch (Exception e) {
+ throw new UnableToBuildException(e);
+ }
+ }
+
+ /**
+ * Encrypy a string using the specified key.
+ *
+ * @param message Input string.
+ * @param key Encryption key.
+ * @return Encrypted output.
+ */
+ static byte[] aes128Encrypt(StringBuilder message, String key) {
+ try {
+ rightPadString(message, '{', 16);
+ byte[] messageBytes = message.toString().getBytes();
+ SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(), "AES");
+ Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec);
+
+ byte[] cipherText = new byte[cipher.getOutputSize(messageBytes.length)];
+ int ctLength = cipher.update(messageBytes, 0, messageBytes.length, cipherText, 0);
+ ctLength += cipher.doFinal(cipherText, ctLength);
+ return cipherText;
+ } catch (Exception e) {
+ throw new UnableToBuildException(e);
+ }
+ }
+}
501 src/test/java/com/squareup/thumbor/ThumborUriTest.java
@@ -0,0 +1,501 @@
+// Copyright 2012 Square, Inc.
+package com.squareup.thumbor;
+
+import org.junit.Test;
+
+import static com.squareup.thumbor.ThumborUri.HorizontalAlign.CENTER;
+import static com.squareup.thumbor.ThumborUri.VerticalAlign.MIDDLE;
+import static com.squareup.thumbor.ThumborUri.brightness;
+import static com.squareup.thumbor.ThumborUri.build;
+import static com.squareup.thumbor.ThumborUri.contrast;
+import static com.squareup.thumbor.ThumborUri.fill;
+import static com.squareup.thumbor.ThumborUri.noise;
+import static com.squareup.thumbor.ThumborUri.quality;
+import static com.squareup.thumbor.ThumborUri.rgb;
+import static com.squareup.thumbor.ThumborUri.roundCorner;
+import static com.squareup.thumbor.ThumborUri.sharpen;
+import static com.squareup.thumbor.ThumborUri.watermark;
+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.junit.Assert.fail;
+
+public class ThumborUriTest {
+ @Test public void testComplexUnsafeBuild() {
+ String expected = "/unsafe/10x10:90x90/40x40/filters:watermark(/unsafe/20x20/b.com/c.jpg,10,10,0):round_corner(5,255,255,255)/a.com/b.png";
+ String actual = build("a.com/b.png")
+ .crop(10, 10, 90, 90)
+ .resize(40, 40)
+ .filter(watermark(
+ build("b.com/c.jpg").resize(20, 20), 10, 10))
+ .filter(roundCorner(5))
+ .buildUnsafe();
+ assertEquals(expected, actual);
+ }
+
+ @Test public void testComplexSafeBuild() {
+ String expected = "/xrUrWUD_ZhogPh-rvPF5VhgWENCgh-mzknoAEZ7dcX_xa7sjqP1ff9hQQq_ORAKmuCr5pyyU3srXG7BUdWUzBqp3AIucz8KiGsmHw1eFe4SBWhp1wSQNG49jSbbuHaFF_4jy5oV4Nh821F4yqNZfe6CIvjbrr1Vw2aMPL4bE7VCHBYE9ukKjVjLRiW3nLfih/a.com/b.png";
+ String actual = build("a.com/b.png")
+ .crop(10, 10, 90, 90)
+ .resize(40, 40)
+ .filter(watermark(
+ build("b.com/c.jpg").resize(20, 20), 10, 10))
+ .filter(roundCorner(5))
+ .key("test")
+ .buildSafe();
+ assertEquals(expected, actual);
+ }
+
+ @Test public void testKeyChangesToStringToSafeBuild() {
+ ThumborUri uri = build("a.com/b.png");
+ assertNull(uri.key);
+ assertTrue(uri.toString().startsWith("/unsafe/"));
+ uri.key("test");
+ assertNotNull(uri.key);
+ assertFalse(uri.toString().startsWith("/unsafe/"));
+ }
+
+ @Test public void testBuildMeta() {
+ assertTrue(build("a.com/b.png").buildMeta().startsWith("/meta/"));
+ }
+
+ @Test public void testResize() {
+ ThumborUri uri = new ThumborUri("a.com/b.png");
+ assertFalse(uri.hasResize);
+
+ uri.resize(10, 5);
+ assertTrue(uri.hasResize);
+ assertEquals(10, uri.resizeWidth);
+ assertEquals(5, uri.resizeHeight);
+ assertEquals("/unsafe/10x5/a.com/b.png", uri.buildUnsafe());
+ }
+
+ @Test public void testResizeAndFitIn() {
+ ThumborUri uri = new ThumborUri("a.com/b.png");
+ uri.resize(10, 5);
+ assertFalse(uri.fitIn);
+ uri.fitIn();
+ assertTrue(uri.fitIn);
+ assertEquals("/unsafe/10x5/fit-in/a.com/b.png", uri.buildUnsafe());
+ }
+
+ @Test public void testResizeAndFlip() {
+ ThumborUri uri1 = new ThumborUri("a.com/b.png").resize(10, 5).flipHorizontally();
+ assertTrue(uri1.flipHorizontally);
+ assertEquals("/unsafe/-10x5/a.com/b.png", uri1.buildUnsafe());
+
+ ThumborUri uri2 = new ThumborUri("a.com/b.png").resize(10, 5).flipVertically();
+ assertTrue(uri2.flipVertically);
+ assertEquals("/unsafe/10x-5/a.com/b.png", uri2.buildUnsafe());
+
+ ThumborUri uri3 = new ThumborUri("a.com/b.png").resize(10, 5).flipHorizontally().flipVertically();
+ assertTrue(uri3.flipHorizontally);
+ assertTrue(uri3.flipVertically);
+ assertEquals("/unsafe/-10x-5/a.com/b.png", uri3.buildUnsafe());
+ }
+
+ @Test public void testCrop() {
+ ThumborUri uri = new ThumborUri("a.com/b.png");
+ assertFalse(uri.hasCrop);
+
+ uri.crop(1, 2, 3, 4);
+ assertTrue(uri.hasCrop);
+ assertEquals(1, uri.cropTop);
+ assertEquals(2, uri.cropLeft);
+ assertEquals(3, uri.cropBottom);
+ assertEquals(4, uri.cropRight);
+ assertEquals("/unsafe/2x1:4x3/a.com/b.png", uri.buildUnsafe());
+ }
+
+ @Test public void testCropAndSmart() {
+ ThumborUri uri = new ThumborUri("a.com/b.png");
+ uri.crop(1, 2, 3, 4);
+
+ assertFalse(uri.isSmart);
+ uri.smart();
+ assertTrue(uri.isSmart);
+ assertEquals("/unsafe/2x1:4x3/smart/a.com/b.png", uri.buildUnsafe());
+ }
+
+ @Test public void testCannotFlipHorizontalWithoutResize() {
+ ThumborUri uri = new ThumborUri("");
+ assertFalse(uri.hasResize);
+ assertFalse(uri.flipHorizontally);
+ try {
+ uri.flipHorizontally();
+ fail("Allowed horizontal flip without resize.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+ assertFalse(uri.flipHorizontally);
+ }
+
+ @Test public void testCannotFlipVerticalWithoutResize() {
+ ThumborUri uri = new ThumborUri("");
+ assertFalse(uri.hasResize);
+ assertFalse(uri.flipVertically);
+ try {
+ uri.flipVertically();
+ fail("Allowed vertical flip without resize.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+ assertFalse(uri.flipVertically);
+ }
+
+ @Test public void testCannotFitInWithoutCrop() {
+ ThumborUri uri = new ThumborUri("");
+ assertFalse(uri.hasCrop);
+ assertFalse(uri.fitIn);
+ try {
+ uri.fitIn();
+ fail("Allowed fit-in resize without resize.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+ assertFalse(uri.fitIn);
+ }
+
+ @Test public void testCannotSmartWithoutCrop() {
+ ThumborUri uri = new ThumborUri("");
+ assertFalse(uri.hasCrop);
+ assertFalse(uri.isSmart);
+ try {
+ uri.smart();
+ fail("Allowed smart crop without crop.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+ assertFalse(uri.isSmart);
+ }
+
+ @Test public void testDoubleAlignmentMethodSetsBoth() {
+ ThumborUri uri = new ThumborUri("");
+ uri.crop(0, 0, 1, 1);
+ assertNull(uri.cropHorizontalAlign);
+ assertNull(uri.cropVerticalAlign);
+ uri.align(MIDDLE, CENTER);
+ assertEquals(CENTER, uri.cropHorizontalAlign);
+ assertEquals(MIDDLE, uri.cropVerticalAlign);
+ }
+
+ @Test public void testCannotAlignWithoutCrop() {
+ ThumborUri uri = new ThumborUri("");
+ assertFalse(uri.hasCrop);
+ assertNull(uri.cropHorizontalAlign);
+
+ try {
+ uri.align(CENTER);
+ fail("Allowed horizontal crop align without crop.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+
+ try {
+ uri.align(MIDDLE);
+ fail("Allowed vertical crop align without crop.");
+ } catch (IllegalStateException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testCannotIssueBadCrop() {
+ ThumborUri uri = new ThumborUri("");
+
+ try {
+ uri.crop(-1, 0, 1, 1);
+ fail("Bad top value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.crop(0, -1, 1, 1);
+ fail("Bad left value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.crop(0, 0, -1, 1);
+ fail("Bad bottom value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.crop(0, 0, 1, -1);
+ fail("Bad right value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.crop(0, 1, 1, 0);
+ fail("Right value less than left value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.crop(1, 0, 0, 1);
+ fail("Bottom value less than top value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testCannotIssueBadResize() {
+ ThumborUri uri = new ThumborUri("");
+
+ try {
+ uri.resize(0, 5);
+ fail("Bad width value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.resize(10, 0);
+ fail("Bad height value allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testCannotBuildWithInvalidTarget() {
+ try {
+ build(null);
+ fail("Bad target image URL allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ build("");
+ fail("Bad target image URL allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testCannotAddInvalidKey() {
+ ThumborUri uri = new ThumborUri("");
+
+ try {
+ uri.key(null);
+ fail("Bad key string allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+
+ try {
+ uri.key("");
+ fail("Bad key string allowed.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterBrightnessInvalidValues() {
+ try {
+ brightness(-101);
+ fail("Brightness allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ brightness(101);
+ fail("Brightness allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterBrightnessFormat() {
+ assertEquals("brightness(30)", brightness(30));
+ }
+
+ @Test public void testFilterContrastInvalidValues() {
+ try {
+ contrast(-101);
+ fail("Contrast allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ contrast(101);
+ fail("Contrast allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterContrastFormat() {
+ assertEquals("contrast(30)", contrast(30));
+ }
+
+ @Test public void testFilterNoiseInvalidValues() {
+ try {
+ noise(-1);
+ fail("Noise allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ noise(101);
+ fail("Noise allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterNoiseFormat() {
+ assertEquals("noise(30)", noise(30));
+ }
+
+ @Test public void testFilterQualityInvalidValues() {
+ try {
+ quality(-1);
+ fail("Quality allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ quality(101);
+ fail("Quality allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterQualityFormat() {
+ assertEquals("quality(30)", quality(30));
+ }
+
+ @Test public void testFilterRgbInvalidValues() {
+ try {
+ rgb(-101, 0, 0);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ rgb(101, 0, 0);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ rgb(0, -101, 0);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ rgb(0, 101, 0);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ rgb(0, 0, -101);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ rgb(0, 0, 101);
+ fail("RGB allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterRgbFormat() {
+ assertEquals("rgb(-30,40,-75)", rgb(-30, 40, -75));
+ }
+
+ @Test public void testFilterRoundCornerInvalidValues() {
+ try {
+ roundCorner(0);
+ fail("Round corner allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ roundCorner(-50);
+ fail("Round corner allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ roundCorner(1, -1, 0xFFFFFF);
+ fail("Round corner allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterRoundCornerFormat() {
+ assertEquals("round_corner(10,255,255,255)", roundCorner(10));
+ assertEquals("round_corner(10,255,16,16)", roundCorner(10, 0xFF1010));
+ assertEquals("round_corner(10|15,255,16,16)", roundCorner(10, 15, 0xFF1010));
+ }
+
+ @Test public void testFilterWatermarkInvalidValues() {
+ try {
+ watermark((String) null);
+ fail("Watermark allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ watermark((ThumborUri) null);
+ fail("Watermark allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ watermark("");
+ fail("Watermark allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ watermark("a.png", 0, 0, -1);
+ fail("Watermark allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ try {
+ watermark("a.png", 0, 0, 101);
+ fail("Watermark allowed invalid value.");
+ } catch (IllegalArgumentException e) {
+ // Pass.
+ }
+ }
+
+ @Test public void testFilterWatermarkFormat() {
+ assertEquals("watermark(a.png,0,0,0)", watermark("a.png"));
+ assertEquals("watermark(/unsafe/10x10/a.png,0,0,0)", watermark(build("a.png").resize(10, 10)));
+ assertEquals("watermark(a.png,20,20,0)", watermark("a.png", 20, 20));
+ assertEquals("watermark(/unsafe/10x10/a.png,20,20,0)", watermark(build("a.png").resize(10, 10), 20, 20));
+ assertEquals("watermark(a.png,20,20,50)", watermark("a.png", 20, 20, 50));
+ assertEquals("watermark(/unsafe/10x10/a.png,20,20,50)", watermark(build("a.png").resize(10, 10), 20, 20, 50));
+ }
+
+ @Test public void testFilterSharpenFormat() {
+ assertEquals("sharpen(3.0,4.0,true)", sharpen(3, 4, true));
+ assertEquals("sharpen(3.0,4.0,false)", sharpen(3, 4, false));
+ assertEquals("sharpen(3.1,4.2,true)", sharpen(3.1f, 4.2f, true));
+ assertEquals("sharpen(3.1,4.2,false)", sharpen(3.1f, 4.2f, false));
+ }
+
+ @Test public void testFilterFillingFormat() {
+ assertEquals("fill(ff2020)", fill(0xFF2020));
+ assertEquals("fill(ff2020)", fill(0xABFF2020));
+ }
+}
62 src/test/java/com/squareup/thumbor/UtilitiesTest.java
@@ -0,0 +1,62 @@
+// Copyright 2012 Square, Inc.
+package com.squareup.thumbor;
+
+import org.junit.Test;
+
+import static com.squareup.thumbor.Utilities.base64Encode;
+import static com.squareup.thumbor.Utilities.md5;
+import static com.squareup.thumbor.Utilities.normalizeString;
+import static com.squareup.thumbor.Utilities.rightPadString;
+import static com.squareup.thumbor.Utilities.stripProtocolAndParams;
+import static org.junit.Assert.assertEquals;
+
+public class UtilitiesTest {
+ @Test public void testProtocolAndParamStrip() {
+ assertEquals("hi.com", stripProtocolAndParams("http://hi.com"));
+ assertEquals("hi.com", stripProtocolAndParams("https://hi.com"));
+ assertEquals("hi.com", stripProtocolAndParams("hi.com?whatup"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("http://hi.com/hi.html"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("https://hi.com/hi.html"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("hi.com/hi.html?whatup"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("http://hi.com/hi.html?whatup"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("https://hi.com/hi.html?whatup"));
+ assertEquals("hi.com/hi.html", stripProtocolAndParams("http://hi.com/hi.html?http://whatever.com"));
+ assertEquals("hi.com/http://whatever.com", stripProtocolAndParams("http://hi.com/http://whatever.com"));
+ assertEquals("hi.com/http://whatever.com", stripProtocolAndParams("http://hi.com/http://whatever.com?whatup"));
+ }
+
+ @Test public void testKeyNormalization() {
+ assertEquals("oneoneoneo", normalizeString("one", 10));
+ assertEquals("equaltoten", normalizeString("equaltoten", 10));
+ assertEquals("reallylong", normalizeString("reallylongstring", 10));
+ }
+
+ @Test public void testMd5() {
+ assertEquals("5a105e8b9d40e1329780d62ea2265d8a", md5("test1"));
+ assertEquals("ad0234829205b9033196ba818f7a872b", md5("test2"));
+ assertEquals("8ad8757baa8564dc136c1e07507f4a98", md5("test3"));
+ }
+
+ @Test public void testBase64() {
+ assertEquals("dGVzdA==", base64Encode("test".getBytes()));
+ assertEquals("dGhpcyBpcyBhIHJlYWxseSBsb25nIHN0cmluZw==", base64Encode("this is a really long string".getBytes()));
+ }
+
+ @Test public void testPadString() {
+ StringBuilder b1 = new StringBuilder("abc");
+ rightPadString(b1, 'X', 3);
+ assertEquals("abc", b1.toString());
+
+ StringBuilder b2 = new StringBuilder("abcde");
+ rightPadString(b2, 'X', 6);
+ assertEquals("abcdeX", b2.toString());
+
+ StringBuilder b3 = new StringBuilder("a");
+ rightPadString(b3, 'X', 6);
+ assertEquals("aXXXXX", b3.toString());
+
+ StringBuilder b4 = new StringBuilder("abcdef");
+ rightPadString(b4, 'X', 16);
+ assertEquals("abcdefXXXXXXXXXX", b4.toString());
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.