├── .gitignore ├── CHANGELOG ├── README ├── ant.properties ├── build.xml ├── lib ├── ant.jar ├── datauri-core-0.2.1.jar ├── jargs-1.0.jar └── junit-4.1.jar ├── src └── net │ └── nczonline │ └── web │ ├── cssembed │ ├── CSSEmbed.java │ ├── CSSEmbedTask.java │ └── CSSURLEmbedder.java │ └── datauri │ ├── Base64.java │ ├── DataURI.java │ └── DataURIGenerator.java └── tests ├── folder.png ├── net └── nczonline │ └── web │ └── cssembed │ ├── CSSURLEmbedderTest.java │ ├── folder.png │ └── samefiletest.css └── samefiletest.css /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | nbproject/ 3 | tmp/ -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | November 22, 2011 - v0.4.5 2 | * Add option to skip some images using /*CSSEmbed:SKIP*/ after the declaration. 3 | 4 | v0.4.0 5 | * Make sure images with query string parameters don't cause an error (Sripathi Krishnan) 6 | * New CSSEmbed Ant task (Pat Cavit) 7 | * Allow specification of maximum image size to convert (Pat Cavit) 8 | 9 | v0.3.6 10 | * Allow specification of maximum data URI length (Pat Cavit and fearphage) 11 | * Option to skip files that are missing (Pat Cavit) 12 | 13 | v0.3.5 14 | * Allow input from STDIN. 15 | 16 | v0.3.4 17 | * Don't include data URIs if they're longer than 32KB (for IE8 support). 18 | * Accept input from stdin. 19 | 20 | v0.3.2 21 | * Brought DataURI into the same repo. 22 | * Added warning when data URI is larger than 32KB (for IE8 support) 23 | * Fixed bug where non-image URIs weren't being handled correctly in MHTML output. 24 | 25 | v0.3.1 26 | * Cleaned up MHTML support. 27 | 28 | v0.3.0 29 | * Initial MHTML support based on dchaplinsky's patch. 30 | http://github.com/nzakas/cssembed/issues/#issue/3, 31 | "MHTML support (patch inside)" 32 | 33 | v0.2.7 34 | * Added dchaplinsky's patch for warning about duplicate images. 35 | * Fixed http://github.com/nzakas/cssembed/issues/#issue/5, "Specifying the same input and output results in empty file". 36 | 37 | v0.2.6 38 | * First public release. 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | CSSEmbed 2 | Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | 4 | ------------------------------------------------------------------------------- 5 | About 6 | ------------------------------------------------------------------------------- 7 | 8 | CSSEmbed is a small program/library to automate embedding of data URIs in 9 | CSS files. 10 | 11 | ------------------------------------------------------------------------------- 12 | License 13 | ------------------------------------------------------------------------------- 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. -------------------------------------------------------------------------------- /ant.properties: -------------------------------------------------------------------------------- 1 | src.dir = src 2 | tests.dir = tests 3 | lib.dir = lib 4 | doc.dir = doc 5 | build.dir = build 6 | res.dir = res 7 | 8 | #Lib jars 9 | jargs.jar = jargs-1.0.jar 10 | junit.jar = junit-4.1.jar 11 | ant.jar = ant.jar 12 | 13 | #Global properties 14 | class.version = 1.5 15 | 16 | #CSSEmbed properties 17 | cssembed.name = cssembed 18 | cssembed.version = 0.4.5 19 | cssembed.jar = ${cssembed.name}-${cssembed.version}.jar 20 | cssembed.main = net.nczonline.web.cssembed.CSSEmbed 21 | 22 | #DataURI properties 23 | datauri.name = datauri 24 | datauri.version = 0.2.2 25 | datauri.jar = ${datauri.name}-${datauri.version}.jar 26 | datauri.main = net.nczonline.web.datauri.DataURI 27 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
${SIMPLE_DATE} - v${cssembed.version} 46 | 47 | ${git.changelog} 48 | 49 |
50 | 51 |
52 | 53 | 54 | 55 | 56 |
57 | 58 | 59 | 60 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 |
-------------------------------------------------------------------------------- /lib/ant.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzakas/cssembed/00b720f01b93019c5d036553043114dd74af8001/lib/ant.jar -------------------------------------------------------------------------------- /lib/datauri-core-0.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzakas/cssembed/00b720f01b93019c5d036553043114dd74af8001/lib/datauri-core-0.2.1.jar -------------------------------------------------------------------------------- /lib/jargs-1.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzakas/cssembed/00b720f01b93019c5d036553043114dd74af8001/lib/jargs-1.0.jar -------------------------------------------------------------------------------- /lib/junit-4.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzakas/cssembed/00b720f01b93019c5d036553043114dd74af8001/lib/junit-4.1.jar -------------------------------------------------------------------------------- /src/net/nczonline/web/cssembed/CSSEmbed.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | * http://www.nczonline.net/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | */ 23 | package net.nczonline.web.cssembed; 24 | 25 | import jargs.gnu.CmdLineParser; 26 | import java.io.File; 27 | import java.io.FileInputStream; 28 | import java.io.FileOutputStream; 29 | import java.io.IOException; 30 | import java.io.InputStreamReader; 31 | import java.io.OutputStreamWriter; 32 | import java.io.ByteArrayOutputStream; 33 | import java.io.Reader; 34 | import java.io.Writer; 35 | import java.nio.charset.Charset; 36 | 37 | 38 | public class CSSEmbed { 39 | 40 | 41 | /** 42 | * @param args the command line arguments 43 | */ 44 | public static void main(String[] args) { 45 | 46 | //default settings 47 | boolean verbose = false; 48 | String charset = null; 49 | String outputFilename = null; 50 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 51 | Writer out = null; 52 | Reader in = null; 53 | String root; 54 | int options = CSSURLEmbedder.DATAURI_OPTION; 55 | 56 | //initialize command line parser 57 | CmdLineParser parser = new CmdLineParser(); 58 | CmdLineParser.Option verboseOpt = parser.addBooleanOption('v', "verbose"); 59 | CmdLineParser.Option helpOpt = parser.addBooleanOption('h', "help"); 60 | CmdLineParser.Option charsetOpt = parser.addStringOption("charset"); 61 | CmdLineParser.Option rootOpt = parser.addStringOption("root"); 62 | CmdLineParser.Option outputFilenameOpt = parser.addStringOption('o', "output"); 63 | CmdLineParser.Option mhtmlOpt = parser.addBooleanOption("mhtml"); 64 | CmdLineParser.Option mhtmlRootOpt = parser.addStringOption("mhtmlroot"); 65 | CmdLineParser.Option skipMissingOpt = parser.addBooleanOption("skip-missing"); 66 | CmdLineParser.Option uriLengthOpt = parser.addIntegerOption("max-uri-length"); 67 | CmdLineParser.Option imageSizeOpt = parser.addIntegerOption("max-image-size"); 68 | 69 | try { 70 | 71 | //parse the arguments 72 | parser.parse(args); 73 | 74 | //figure out if the help option has been executed 75 | Boolean help = (Boolean) parser.getOptionValue(helpOpt); 76 | if (help != null && help.booleanValue()) { 77 | usage(); 78 | System.exit(0); 79 | } 80 | 81 | //determine boolean options 82 | verbose = parser.getOptionValue(verboseOpt) != null; 83 | 84 | //check for charset 85 | charset = (String) parser.getOptionValue(charsetOpt); 86 | if (charset == null || !Charset.isSupported(charset)) { 87 | charset = System.getProperty("file.encoding"); 88 | if (charset == null) { 89 | charset = "UTF-8"; 90 | } 91 | if (verbose) { 92 | System.err.println("\n[INFO] Using charset " + charset); 93 | } 94 | } 95 | 96 | //get the file arguments 97 | String[] fileArgs = parser.getRemainingArgs(); 98 | String inputFilename = null; 99 | 100 | //if no file is given, use stdin 101 | if (fileArgs.length == 0){ 102 | in = new InputStreamReader(System.in, charset); 103 | } else { 104 | //only the first filename is used 105 | inputFilename = fileArgs[0]; 106 | in = new InputStreamReader(new FileInputStream(inputFilename), charset); 107 | } 108 | 109 | //determine if there's a maximum URI length 110 | int uriLength = CSSURLEmbedder.DEFAULT_MAX_URI_LENGTH; 111 | Integer maxUriLength = (Integer) parser.getOptionValue(uriLengthOpt); 112 | if (maxUriLength != null){ 113 | uriLength = maxUriLength.intValue(); 114 | if (uriLength < 0){ 115 | uriLength = 0; 116 | } 117 | } 118 | 119 | //maximum size allowed for image files 120 | int imageSize = 0; 121 | Integer imageSizeOption = (Integer) parser.getOptionValue(imageSizeOpt); 122 | if (imageSizeOption != null){ 123 | imageSize = imageSizeOption.intValue(); 124 | if (imageSize < 0){ 125 | imageSize = 0; 126 | } 127 | } 128 | 129 | //determine if MHTML mode is on 130 | boolean mhtml = parser.getOptionValue(mhtmlOpt) != null; 131 | if(mhtml){ 132 | options = CSSURLEmbedder.MHTML_OPTION; 133 | } 134 | String mhtmlRoot = (String) parser.getOptionValue(mhtmlRootOpt); 135 | if (mhtml && mhtmlRoot == null){ 136 | throw new Exception("Must use --mhtmlroot when using --mhtml."); 137 | } 138 | 139 | //are missing files ok? 140 | boolean skipMissingFiles = parser.getOptionValue(skipMissingOpt) != null; 141 | if(skipMissingFiles) { 142 | options = options | CSSURLEmbedder.SKIP_MISSING_OPTION; 143 | } 144 | 145 | CSSURLEmbedder embedder = new CSSURLEmbedder(in, options, verbose, uriLength, imageSize); 146 | embedder.setMHTMLRoot(mhtmlRoot); 147 | 148 | //close in case writing to the same file 149 | in.close(); in = null; 150 | 151 | //get root for relative URLs 152 | root = (String) parser.getOptionValue(rootOpt); 153 | if(root == null){ 154 | 155 | if (inputFilename != null) { 156 | //no root specified, so get from input file 157 | root = (new File(inputFilename)).getCanonicalPath(); 158 | root = root.substring(0, root.lastIndexOf(File.separator)); 159 | } else { 160 | throw new Exception("Must use --root when not specifying a filename."); 161 | } 162 | } 163 | 164 | if (!root.endsWith(File.separator)){ 165 | root += File.separator; 166 | } 167 | 168 | if (verbose){ 169 | System.err.println("[INFO] Using '" + root + "' as root for relative file paths."); 170 | } 171 | 172 | //get output filename 173 | outputFilename = (String) parser.getOptionValue(outputFilenameOpt); 174 | if (outputFilename == null) { 175 | if (verbose){ 176 | System.err.println("[INFO] No output file specified, defaulting to stdout."); 177 | } 178 | 179 | out = new OutputStreamWriter(System.out); 180 | } else { 181 | File outputFile = new File(outputFilename); 182 | if (verbose){ 183 | System.err.println("[INFO] Output file is '" + outputFile.getAbsolutePath() + "'"); 184 | } 185 | embedder.setFilename(outputFile.getName()); 186 | out = new OutputStreamWriter(bytes, charset); 187 | } 188 | 189 | //set verbose option 190 | embedder.embedImages(out, root); 191 | 192 | } catch (CmdLineParser.OptionException e) { 193 | usage(); 194 | System.exit(1); 195 | } catch (Exception e) { 196 | System.err.println("[ERROR] " + e.getMessage()); 197 | if (verbose){ 198 | e.printStackTrace(); 199 | } 200 | System.exit(1); 201 | } finally { 202 | if (out != null) { 203 | try { 204 | out.close(); 205 | 206 | if(bytes.size() > 0) { 207 | bytes.writeTo(new FileOutputStream(outputFilename)); 208 | } 209 | } catch (IOException e) { 210 | System.err.println("[ERROR] " + e.getMessage()); 211 | if (verbose){ 212 | e.printStackTrace(); 213 | } 214 | } 215 | } 216 | } 217 | 218 | } 219 | 220 | /** 221 | * Outputs help information to the console. 222 | */ 223 | private static void usage() { 224 | System.out.println( 225 | "\nUsage: java -jar cssembed-x.y.z.jar [options] [input file]\n\n" 226 | 227 | + "Global Options\n" 228 | + " -h, --help Displays this information.\n" 229 | + " --charset Character set of the input file.\n" 230 | + " --mhtml Enable MHTML mode.\n" 231 | + " --mhtmlroot Use as the MHTML root for the file.\n" 232 | + " -v, --verbose Display informational messages and warnings.\n" 233 | + " --root Prepends to all relative URLs.\n" 234 | + " --skip-missing Don't throw an error for missing image files.\n" 235 | + " --max-uri-length len Maximum length for a data URI. Defaults to 32768.\n" 236 | + " --max-image-size size Maximum image size (in bytes) to convert.\n" 237 | + " -o Place the output into . Defaults to stdout."); 238 | } 239 | } -------------------------------------------------------------------------------- /src/net/nczonline/web/cssembed/CSSEmbedTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | * http://www.nczonline.net/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | */ 23 | 24 | package net.nczonline.web.cssembed; 25 | 26 | import org.apache.tools.ant.Project; 27 | import org.apache.tools.ant.Task; 28 | import org.apache.tools.ant.BuildException; 29 | 30 | import org.apache.tools.ant.types.Mapper; 31 | import org.apache.tools.ant.types.ResourceCollection; 32 | import org.apache.tools.ant.types.resources.FileResource; 33 | 34 | import org.apache.tools.ant.util.FileNameMapper; 35 | import org.apache.tools.ant.util.IdentityMapper; 36 | 37 | import java.io.*; 38 | 39 | import java.util.Vector; 40 | import java.util.Iterator; 41 | 42 | //Define a custom Ant Task that calls into the CSS Embedder 43 | public class CSSEmbedTask extends Task { 44 | 45 | //attribute options 46 | private String charset = "UTF-8"; 47 | private String root; 48 | private boolean mhtml; 49 | private String mhtmlRoot; 50 | private boolean skipMissing; 51 | private boolean verbose = false; 52 | private int maxUriLength = 0; 53 | private int maxImageSize = 0; 54 | private File srcFile; 55 | private File destFile; 56 | 57 | //support nested resource collections & mappers 58 | private Mapper mapperElement = null; 59 | private Vector rcs = new Vector(); 60 | 61 | //Simple Setters 62 | public void setCharset(String charset) { 63 | this.charset = charset; 64 | } 65 | 66 | public void setRoot(String root) { 67 | this.root = root; 68 | } 69 | 70 | public void setMhtml(boolean mhtml) { 71 | this.mhtml = mhtml; 72 | } 73 | 74 | public void setMhtmlRoot(String mhtmlRoot) { 75 | this.mhtmlRoot = mhtmlRoot; 76 | } 77 | 78 | public void setSkipMissing(boolean skipMissing) { 79 | this.skipMissing = skipMissing; 80 | } 81 | 82 | public void setVerbose(boolean verbose) { 83 | this.verbose = verbose; 84 | } 85 | 86 | public void setMaxUriLength(int maxUriLength) { 87 | this.maxUriLength = maxUriLength; 88 | } 89 | 90 | public void setMaxImageSize(int maxImageSize) { 91 | this.maxImageSize = maxImageSize; 92 | } 93 | 94 | public void setSrcFile(File srcFile) { 95 | this.srcFile = srcFile; 96 | } 97 | 98 | public void setDestFile(File destFile) { 99 | this.destFile = destFile; 100 | } 101 | 102 | //More complicated setters for nested elements... 103 | 104 | //add a collection of resources to copy 105 | public void add(ResourceCollection res) { 106 | rcs.add(res); 107 | } 108 | 109 | //mapper takes source files & converts them to dest files 110 | public Mapper createMapper() throws BuildException { 111 | if (mapperElement != null) { 112 | throw new BuildException("Cannot define more than one mapper", getLocation()); 113 | } 114 | mapperElement = new Mapper(getProject()); 115 | return mapperElement; 116 | } 117 | 118 | //support multiple types of filename mappers being added 119 | public void add(FileNameMapper fileNameMapper) { 120 | createMapper().add(fileNameMapper); 121 | } 122 | 123 | //returns the mapper to use based on nested elements, defaults to IdentityMapper 124 | private FileNameMapper getMapper() { 125 | FileNameMapper mapper = null; 126 | if (mapperElement != null) { 127 | mapper = mapperElement.getImplementation(); 128 | } else { 129 | mapper = new IdentityMapper(); 130 | } 131 | return mapper; 132 | } 133 | 134 | //ensure that attributes are legit 135 | protected void validateAttributes() throws BuildException { 136 | //if there's no nested resource containers make sure that a srcFile/destFile are set 137 | if(this.rcs == null || this.rcs.size() == 0) { 138 | if (this.srcFile == null || !this.srcFile.exists()) { 139 | throw new BuildException("Must specify an input file or at least one nested resource", getLocation()); 140 | } 141 | 142 | if(this.destFile == null) { 143 | throw new BuildException("Must specify an output file or at least one nested resource", getLocation()); 144 | } 145 | } 146 | 147 | if(this.mhtml && this.mhtmlRoot == null) { 148 | throw new BuildException("Must specify mhtmlRoot in mhtml mode", getLocation()); 149 | } 150 | 151 | if(this.mhtmlRoot != null && !this.mhtml) { 152 | log("mhtmlRoot has no effect if mhtml mode is not activated", Project.MSG_WARN); 153 | } 154 | } 155 | 156 | //run the task 157 | public void execute () throws BuildException { 158 | validateAttributes(); 159 | 160 | //set options flags 161 | int options = (this.mhtml) ? CSSURLEmbedder.MHTML_OPTION : CSSURLEmbedder.DATAURI_OPTION; 162 | if(skipMissing) { 163 | options = options | CSSURLEmbedder.SKIP_MISSING_OPTION; 164 | } 165 | 166 | if(srcFile != null && srcFile.exists()) { 167 | try { 168 | embed(srcFile, destFile, options); 169 | } catch(IOException ex) { 170 | throw new BuildException(ex.getMessage(), ex); 171 | } 172 | } 173 | 174 | FileNameMapper mapper = getMapper(); 175 | 176 | for(Iterator it = this.rcs.iterator(); it.hasNext();) { 177 | ResourceCollection rc = (ResourceCollection) it.next(); 178 | 179 | for(Iterator rcit = rc.iterator(); rcit.hasNext();) { 180 | FileResource fr = (FileResource) rcit.next(); 181 | File in = fr.getFile(); 182 | 183 | String[] mapped = mapper.mapFileName(in.getName()); 184 | if (mapped != null && mapped.length > 0) { 185 | for(int k = 0; k < mapped.length; k++) { 186 | File out = getProject().resolveFile(in.getParent() + File.separator + mapped[k]); 187 | 188 | try { 189 | embed(in, out, options); 190 | } catch(IOException ex) { 191 | throw new BuildException(ex.getMessage(), ex); 192 | } 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | private void embed(File input, File output, int options) throws IOException { 200 | ByteArrayOutputStream bytes = new ByteArrayOutputStream(); 201 | Reader in = new InputStreamReader(new FileInputStream(input), charset); 202 | Writer out = new OutputStreamWriter(bytes, charset); 203 | String pathRoot = root; 204 | 205 | if(pathRoot == null) { 206 | pathRoot = input.getCanonicalPath(); 207 | pathRoot = pathRoot.substring(0, pathRoot.lastIndexOf(File.separator)); 208 | } 209 | 210 | if (!pathRoot.endsWith(File.separator)){ 211 | pathRoot += File.separator; 212 | } 213 | 214 | if(verbose) { 215 | log("[INFO] embedding images from '" + input + "'"); 216 | } 217 | 218 | CSSURLEmbedder embedder = new CSSURLEmbedder(in, options, verbose, maxUriLength, maxImageSize); 219 | 220 | if(mhtml) { 221 | embedder.setMHTMLRoot(mhtmlRoot); 222 | embedder.setFilename(output.getName()); 223 | } 224 | 225 | embedder.embedImages(out, pathRoot); 226 | 227 | in.close(); 228 | out.close(); 229 | 230 | if(bytes.size() > 0) { 231 | FileOutputStream fos = new FileOutputStream(output); 232 | 233 | if(verbose) { 234 | log("[INFO] Writing to file: " + output); 235 | } 236 | 237 | bytes.writeTo(fos); 238 | fos.close(); 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /src/net/nczonline/web/cssembed/CSSURLEmbedder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | * http://www.nczonline.net/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | */ 23 | 24 | package net.nczonline.web.cssembed; 25 | 26 | import java.io.BufferedReader; 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.io.FileNotFoundException; 30 | import java.io.Reader; 31 | import java.io.StringReader; 32 | import java.io.StringWriter; 33 | import java.io.Writer; 34 | import java.net.URL; 35 | import java.util.HashMap; 36 | import java.util.HashSet; 37 | import net.nczonline.web.datauri.DataURIGenerator; 38 | import java.util.regex.*; 39 | 40 | /** 41 | * Generator for Data URIs. 42 | * @author Nicholas C. Zakas 43 | */ 44 | public class CSSURLEmbedder { 45 | 46 | public static final int DATAURI_OPTION = 1; 47 | public static final int MHTML_OPTION = 2; 48 | public static final int SKIP_MISSING_OPTION = 4; 49 | 50 | public static final int DEFAULT_MAX_URI_LENGTH = 32768; 51 | 52 | public static final String PROC_DIRECTIVE_PREFIX = "CssEmbed"; 53 | public static final String PROC_DIRECTIVE_SKIP = "SKIP"; 54 | 55 | protected static String MHTML_SEPARATOR = "CSSEmbed_Image"; 56 | 57 | private static HashSet imageTypes; 58 | static { 59 | imageTypes = new HashSet(); 60 | imageTypes.add("jpg"); 61 | imageTypes.add("jpeg"); 62 | imageTypes.add("gif"); 63 | imageTypes.add("png"); 64 | } 65 | 66 | private boolean verbose = false; 67 | private String code = null; 68 | private int options = 1; 69 | private String mhtmlRoot = ""; 70 | private String outputFilename = ""; 71 | private int maxUriLength = DEFAULT_MAX_URI_LENGTH; //IE8 only allows dataURIs up to 32KB 72 | private int maxImageSize; 73 | 74 | //-------------------------------------------------------------------------- 75 | // Constructors 76 | //-------------------------------------------------------------------------- 77 | 78 | public CSSURLEmbedder(Reader in) throws IOException { 79 | this(in, false); 80 | } 81 | 82 | public CSSURLEmbedder(Reader in, int options) throws IOException { 83 | this(in, false); 84 | } 85 | 86 | public CSSURLEmbedder(Reader in, boolean verbose) throws IOException { 87 | this(in, 1, verbose); 88 | } 89 | 90 | public CSSURLEmbedder(Reader in, int options, boolean verbose) throws IOException { 91 | this(in, options, verbose, 0); 92 | } 93 | 94 | public CSSURLEmbedder(Reader in, int options, boolean verbose, int maxUriLength) throws IOException { 95 | this(in, options, verbose, maxUriLength, 0); 96 | } 97 | 98 | public CSSURLEmbedder(Reader in, int options, boolean verbose, int maxUriLength, int maxImageSize) throws IOException { 99 | this.code = readCode(in); 100 | this.verbose = verbose; 101 | this.options = options; 102 | this.maxUriLength = maxUriLength; 103 | this.maxImageSize = maxImageSize; 104 | } 105 | 106 | //-------------------------------------------------------------------------- 107 | // Get/Set verbose flag 108 | //-------------------------------------------------------------------------- 109 | 110 | public boolean getVerbose(){ 111 | return verbose; 112 | } 113 | 114 | public void setVerbose(boolean newVerbose){ 115 | verbose = newVerbose; 116 | } 117 | 118 | //-------------------------------------------------------------------------- 119 | // Determine if an option is set - Options support not yet complete 120 | //-------------------------------------------------------------------------- 121 | 122 | private boolean hasOption(int option){ 123 | return (options & option) > 0; 124 | } 125 | 126 | //-------------------------------------------------------------------------- 127 | // MHTML Support 128 | //-------------------------------------------------------------------------- 129 | 130 | public String getMHTMLRoot(){ 131 | return mhtmlRoot; 132 | } 133 | 134 | public void setMHTMLRoot(String mhtmlRoot){ 135 | this.mhtmlRoot = mhtmlRoot; 136 | } 137 | 138 | public String getFilename(){ 139 | return outputFilename; 140 | } 141 | 142 | public void setFilename(String filename){ 143 | this.outputFilename = filename; 144 | } 145 | 146 | //-------------------------------------------------------------------------- 147 | // Embed images 148 | //-------------------------------------------------------------------------- 149 | 150 | /** 151 | * Embeds data URI images into a CSS file. 152 | * @param out The place to write out the source code. 153 | * @throws java.io.IOException 154 | */ 155 | public void embedImages(Writer out) throws IOException { 156 | embedImages(out, null); 157 | } 158 | 159 | /** 160 | * Embeds data URI images into a CSS file. 161 | * @param out The place to write out the source code. 162 | * @param root The root to prepend to any relative paths. 163 | * @throws java.io.IOException 164 | */ 165 | public void embedImages(Writer out, String root) throws IOException { 166 | BufferedReader reader = new BufferedReader(new StringReader(code)); 167 | StringBuilder builder = new StringBuilder(); 168 | StringBuilder mhtmlHeader = new StringBuilder(); 169 | HashMap foundMedia = new HashMap(); 170 | String line; 171 | int lineNum = 1; 172 | int conversions = 0; 173 | 174 | //create initial MHTML code 175 | if (hasOption(MHTML_OPTION)){ 176 | mhtmlHeader.append("/*\n"); 177 | mhtmlHeader.append("Content-Type: multipart/related; boundary=\""); 178 | mhtmlHeader.append(MHTML_SEPARATOR); 179 | mhtmlHeader.append("\"\n\n"); 180 | } 181 | 182 | while((line = reader.readLine()) != null){ 183 | 184 | int start = 0; 185 | int pos = line.indexOf("url(", start); 186 | int npos; 187 | 188 | if (lineNum > 1){ 189 | builder.append("\n"); 190 | } 191 | 192 | Pattern checkForSkip = Pattern.compile("\\/\\*.*" + PROC_DIRECTIVE_PREFIX + ".*" + PROC_DIRECTIVE_SKIP + ".*\\*\\/", Pattern.CASE_INSENSITIVE); 193 | Matcher skipMatch = checkForSkip.matcher(line); 194 | if (skipMatch.find()) { 195 | builder.append(line); 196 | if (verbose) { 197 | System.err.println("[INFO] line #" + lineNum + " skipped due to SKIP directive (" + PROC_DIRECTIVE_PREFIX + ": " + PROC_DIRECTIVE_SKIP + ")"); 198 | } 199 | } else if (pos > -1){ 200 | while (pos > -1){ 201 | pos += 4; 202 | builder.append(line.substring(start, pos)); 203 | npos = line.indexOf(")", pos); 204 | String url = line.substring(pos, npos).trim(); 205 | 206 | //eliminate quotes at the beginning and end 207 | if (url.startsWith("\"")){ 208 | if (url.endsWith("\"")){ 209 | url = url.substring(1, url.length()-1); 210 | } else { 211 | throw new IOException("Invalid CSS URL format (" + url + ") at line " + lineNum + ", col " + pos + "."); 212 | } 213 | } else if (url.startsWith("'")){ 214 | if (url.endsWith("'")){ 215 | url = url.substring(1, url.length()-1); 216 | } else { 217 | throw new IOException("Invalid CSS URL format (" + url + ") at line " + lineNum + ", col " + pos + "."); 218 | } 219 | } 220 | 221 | //check for duplicates 222 | if (foundMedia.containsKey(url)){ 223 | if (verbose){ 224 | System.err.println("[WARNING] Duplicate URL '" + url + "' found at line " + lineNum + ", previously declared at line " + foundMedia.get(url) + "."); 225 | } 226 | } 227 | foundMedia.put(url, lineNum); 228 | 229 | //Begin processing URL 230 | String newUrl = url; 231 | if (verbose){ 232 | System.err.println("[INFO] Found URL '" + url + "' at line " + lineNum + ", col " + pos + "."); 233 | } 234 | if (url.indexOf("http:") != 0 && root != null){ 235 | newUrl = root + url; 236 | if (verbose){ 237 | System.err.println("[INFO] Applying root to URL, URL is now '" + newUrl + "'."); 238 | } 239 | } 240 | 241 | //get the data URI format 242 | String uriString = getImageURIString(newUrl, url); 243 | 244 | //if it doesn't begin with data:, it's not a data URI 245 | if (uriString.startsWith("data:")){ 246 | if (maxUriLength > 0 && uriString.length() > maxUriLength){ 247 | if (verbose){ 248 | System.err.println("[WARNING] File " + newUrl + " creates a data URI larger than " + maxUriLength + " bytes. Skipping."); 249 | } 250 | builder.append(url); 251 | } else { 252 | 253 | /* 254 | * Determine what to do. Eventually, you should be able to 255 | * have both a data URI and MHTML in the same file. 256 | */ 257 | if (hasOption(MHTML_OPTION)){ 258 | String entryName = getFilename(url); 259 | 260 | //create MHTML header entry 261 | mhtmlHeader.append("--"); 262 | mhtmlHeader.append(MHTML_SEPARATOR); 263 | mhtmlHeader.append("\nContent-Location:"); 264 | mhtmlHeader.append(entryName); 265 | mhtmlHeader.append("\nContent-Transfer-Encoding:base64\n\n"); 266 | mhtmlHeader.append(uriString.substring(uriString.indexOf(",")+1)); 267 | mhtmlHeader.append("\n"); 268 | 269 | //output the URI 270 | builder.append("mhtml:"); 271 | builder.append(getMHTMLPath()); 272 | builder.append("!"); 273 | builder.append(entryName); 274 | conversions++; 275 | } else if (hasOption(DATAURI_OPTION)){ 276 | builder.append(uriString); 277 | conversions++; 278 | } 279 | } 280 | } else { 281 | //TODO: Clean up, duplicate code 282 | builder.append(uriString); 283 | } 284 | 285 | start = npos; 286 | pos = line.indexOf("url(", start); 287 | } 288 | 289 | //finish out the line 290 | if (start < line.length()){ 291 | builder.append(line.substring(start)); 292 | } 293 | } else { 294 | builder.append(line); 295 | } 296 | 297 | lineNum++; 298 | } 299 | reader.close(); 300 | 301 | if (hasOption(MHTML_OPTION) && conversions > 0){ 302 | 303 | //Add one more boundary to fix IE/Vista issue 304 | mhtmlHeader.append("\n--"); 305 | mhtmlHeader.append(MHTML_SEPARATOR); 306 | mhtmlHeader.append("--\n"); 307 | 308 | //close comment 309 | mhtmlHeader.append("*/\n"); 310 | out.write(mhtmlHeader.toString()); 311 | } 312 | 313 | if (verbose){ 314 | System.err.println("[INFO] Converted " + conversions + " images to data URIs."); 315 | } 316 | 317 | out.write(builder.toString()); 318 | } 319 | 320 | /** 321 | * Returns a URI string for the given URL. If the URL is for an image, 322 | * the data URI will be returned. If the URL is not for an image, then the 323 | * original URI is returned. 324 | * @param url The URL to attempt to read. 325 | * @param originalUrl The original URL as stated in the source code. 326 | * @return The appropriate data URI to use. 327 | * @throws java.io.IOException 328 | */ 329 | String getImageURIString(String url, String originalUrl) throws IOException { 330 | 331 | //it's an image, so encode it 332 | if (isImage(url)){ 333 | 334 | DataURIGenerator.setVerbose(verbose); 335 | 336 | StringWriter writer = new StringWriter(); 337 | 338 | try { 339 | if (url.startsWith("http://")){ 340 | if (verbose){ 341 | System.err.println("[INFO] Downloading '" + url + "' to generate data URI."); 342 | } 343 | 344 | DataURIGenerator.generate(new URL(url), writer); 345 | 346 | } else { 347 | if (verbose){ 348 | System.err.println("[INFO] Opening file '" + url + "' to generate data URI."); 349 | } 350 | 351 | File file = new File(url); 352 | 353 | if (verbose && !file.isFile()){ 354 | System.err.println("[INFO] Could not find file '" + file.getCanonicalPath() + "'."); 355 | } 356 | 357 | //check file size if we've been asked to 358 | if (maxImageSize > 0 && file.length() > maxImageSize){ 359 | if (verbose){ 360 | System.err.println("[INFO] File '" + originalUrl + "' is larger than " + maxImageSize + " bytes. Skipping."); 361 | } 362 | 363 | writer.write(originalUrl); 364 | 365 | } else { 366 | DataURIGenerator.generate(new File(url), writer); 367 | } 368 | } 369 | 370 | if (verbose){ 371 | System.err.println("[INFO] Generated data URI for '" + url + "'."); 372 | } 373 | } catch (FileNotFoundException e){ 374 | if(hasOption(SKIP_MISSING_OPTION)) { 375 | if (verbose){ 376 | System.err.println("[INFO] Could not find file. " + e.getMessage() + " Skipping."); 377 | } 378 | 379 | writer.write(originalUrl); 380 | } else { 381 | throw e; 382 | } 383 | } 384 | 385 | return writer.toString(); 386 | 387 | } else { 388 | 389 | if (verbose){ 390 | System.err.println("[INFO] URL '" + originalUrl + "' is not an image, skipping."); 391 | } 392 | 393 | //not an image, ignore 394 | return originalUrl; 395 | } 396 | 397 | } 398 | 399 | /* 400 | * Detects if the given url represents an image 401 | * This method simply checks the file extension. 402 | * A better way to detect an image is via content type response headers or by content sniffing, 403 | * but both are expensive approaches. We can do without them for now. 404 | */ 405 | static boolean isImage(String url) { 406 | int startPos = url.lastIndexOf(".") + 1; 407 | /* 408 | * Some images are of the form some-image.png?parameter=value 409 | */ 410 | int endPos = url.lastIndexOf("?"); 411 | if(endPos == -1 || endPos <= startPos) { 412 | endPos = url.length(); 413 | } 414 | String fileType = url.substring(startPos, endPos); 415 | return imageTypes.contains(fileType); 416 | } 417 | 418 | private String getFilename(String path){ 419 | if (path.indexOf("/") > -1){ 420 | return path.substring(path.lastIndexOf("/")+1); 421 | } else if (path.indexOf("\\") > -1){ 422 | return path.substring(path.lastIndexOf("\\")+1); 423 | } else { 424 | return path; 425 | } 426 | } 427 | 428 | private String getMHTMLPath(){ 429 | String result = mhtmlRoot; 430 | if (!result.endsWith("/")){ 431 | result += "/"; 432 | } 433 | 434 | result += outputFilename; 435 | 436 | return result; 437 | } 438 | 439 | private String readCode(Reader in) throws IOException { 440 | StringBuilder builder = new StringBuilder(); 441 | int c; 442 | 443 | while ((c = in.read()) != -1){ 444 | builder.append((char)c); 445 | } 446 | 447 | in.close(); 448 | return builder.toString(); 449 | } 450 | 451 | } 452 | -------------------------------------------------------------------------------- /src/net/nczonline/web/datauri/Base64.java: -------------------------------------------------------------------------------- 1 | 2 | package net.nczonline.web.datauri; 3 | 4 | /** 5 | *

Encodes and decodes to and from Base64 notation.

6 | *

Homepage: http://iharder.net/base64.

7 | * 8 | *

Example:

9 | * 10 | * String encoded = Base64.encode( myByteArray ); 11 | *
12 | * byte[] myByteArray = Base64.decode( encoded ); 13 | * 14 | *

The options parameter, which appears in a few places, is used to pass 15 | * several pieces of information to the encoder. In the "higher level" methods such as 16 | * encodeBytes( bytes, options ) the options parameter can be used to indicate such 17 | * things as first gzipping the bytes before encoding them, not inserting linefeeds, 18 | * and encoding using the URL-safe and Ordered dialects.

19 | * 20 | *

Note, according to RFC3548, 21 | * Section 2.1, implementations should not add line feeds unless explicitly told 22 | * to do so. I've got Base64 set to this behavior now, although earlier versions 23 | * broke lines by default.

24 | * 25 | *

The constants defined in Base64 can be OR-ed together to combine options, so you 26 | * might make a call like this:

27 | * 28 | * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); 29 | *

to compress the data before encoding it and then making the output have newline characters.

30 | *

Also...

31 | * String encoded = Base64.encodeBytes( crazyString.getBytes() ); 32 | * 33 | * 34 | * 35 | *

36 | * Change Log: 37 | *

38 | *
    39 | *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size 40 | * was wrong for files of size 31, 34, and 37 bytes.
  • 41 | *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing 42 | * the Base64.OutputStream closed the Base64 encoding (by padding with equals 43 | * signs) too soon. Also added an option to suppress the automatic decoding 44 | * of gzipped streams. Also added experimental support for specifying a 45 | * class loader when using the 46 | * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} 47 | * method.
  • 48 | *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java 49 | * footprint with its CharEncoders and so forth. Fixed some javadocs that were 50 | * inconsistent. Removed imports and specified things like java.io.IOException 51 | * explicitly inline.
  • 52 | *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the 53 | * final encoded data will be so that the code doesn't have to create two output 54 | * arrays: an oversized initial one and then a final, exact-sized one. Big win 55 | * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not 56 | * using the gzip options which uses a different mechanism with streams and stuff).
  • 57 | *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some 58 | * similar helper methods to be more efficient with memory by not returning a 59 | * String but just a byte array.
  • 60 | *
  • v2.3 - This is not a drop-in replacement! This is two years of comments 61 | * and bug fixes queued up and finally executed. Thanks to everyone who sent 62 | * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. 63 | * Much bad coding was cleaned up including throwing exceptions where necessary 64 | * instead of returning null values or something similar. Here are some changes 65 | * that may affect you: 66 | *
      67 | *
    • Does not break lines, by default. This is to keep in compliance with 68 | * RFC3548.
    • 69 | *
    • Throws exceptions instead of returning null values. Because some operations 70 | * (especially those that may permit the GZIP option) use IO streams, there 71 | * is a possiblity of an java.io.IOException being thrown. After some discussion and 72 | * thought, I've changed the behavior of the methods to throw java.io.IOExceptions 73 | * rather than return null if ever there's an error. I think this is more 74 | * appropriate, though it will require some changes to your code. Sorry, 75 | * it should have been done this way to begin with.
    • 76 | *
    • Removed all references to System.out, System.err, and the like. 77 | * Shame on me. All I can say is sorry they were ever there.
    • 78 | *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed 79 | * such as when passed arrays are null or offsets are invalid.
    • 80 | *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. 81 | * This was especially annoying before for people who were thorough in their 82 | * own projects and then had gobs of javadoc warnings on this file.
    • 83 | *
    84 | *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug 85 | * when using very small files (~< 40 bytes).
  • 86 | *
  • v2.2 - Added some helper methods for encoding/decoding directly from 87 | * one file to the next. Also added a main() method to support command line 88 | * encoding/decoding from one file to the next. Also added these Base64 dialects: 89 | *
      90 | *
    1. The default is RFC3548 format.
    2. 91 | *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates 92 | * URL and file name friendly format as described in Section 4 of RFC3548. 93 | * http://www.faqs.org/rfcs/rfc3548.html
    4. 94 | *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates 95 | * URL and file name friendly format that preserves lexical ordering as described 96 | * in http://www.faqs.org/qa/rfcc-1940.html
    6. 97 | *
    98 | * Special thanks to Jim Kellerman at http://www.powerset.com/ 99 | * for contributing the new Base64 dialects. 100 | *
  • 101 | * 102 | *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added 103 | * some convenience methods for reading and writing to and from files.
  • 104 | *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems 105 | * with other encodings (like EBCDIC).
  • 106 | *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the 107 | * encoded data was a single byte.
  • 108 | *
  • v2.0 - I got rid of methods that used booleans to set options. 109 | * Now everything is more consolidated and cleaner. The code now detects 110 | * when data that's being decoded is gzip-compressed and will decompress it 111 | * automatically. Generally things are cleaner. You'll probably have to 112 | * change some method calls that you were making to support the new 113 | * options format (ints that you "OR" together).
  • 114 | *
  • v1.5.1 - Fixed bug when decompressing and decoding to a 115 | * byte[] using decode( String s, boolean gzipCompressed ). 116 | * Added the ability to "suspend" encoding in the Output Stream so 117 | * you can turn on and off the encoding if you need to embed base64 118 | * data in an otherwise "normal" stream (like an XML file).
  • 119 | *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. 120 | * This helps when using GZIP streams. 121 | * Added the ability to GZip-compress objects before encoding them.
  • 122 | *
  • v1.4 - Added helper methods to read/write files.
  • 123 | *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • 124 | *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream 125 | * where last buffer being read, if not completely full, was not returned.
  • 126 | *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • 127 | *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • 128 | *
129 | * 130 | *

131 | * I am placing this code in the Public Domain. Do with it as you will. 132 | * This software comes with no guarantees or warranties but with 133 | * plenty of well-wishing instead! 134 | * Please visit http://iharder.net/base64 135 | * periodically to check for updates or to contribute improvements. 136 | *

137 | * 138 | * @author Robert Harder 139 | * @author rob@iharder.net 140 | * @version 2.3.5 141 | */ 142 | public class Base64 143 | { 144 | 145 | /* ******** P U B L I C F I E L D S ******** */ 146 | 147 | 148 | /** No options specified. Value is zero. */ 149 | public final static int NO_OPTIONS = 0; 150 | 151 | /** Specify encoding in first bit. Value is one. */ 152 | public final static int ENCODE = 1; 153 | 154 | 155 | /** Specify decoding in first bit. Value is zero. */ 156 | public final static int DECODE = 0; 157 | 158 | 159 | /** Specify that data should be gzip-compressed in second bit. Value is two. */ 160 | public final static int GZIP = 2; 161 | 162 | /** Specify that gzipped data should not be automatically gunzipped. */ 163 | public final static int DONT_GUNZIP = 4; 164 | 165 | 166 | /** Do break lines when encoding. Value is 8. */ 167 | public final static int DO_BREAK_LINES = 8; 168 | 169 | /** 170 | * Encode using Base64-like encoding that is URL- and Filename-safe as described 171 | * in Section 4 of RFC3548: 172 | * http://www.faqs.org/rfcs/rfc3548.html. 173 | * It is important to note that data encoded this way is not officially valid Base64, 174 | * or at the very least should not be called Base64 without also specifying that is 175 | * was encoded using the URL- and Filename-safe dialect. 176 | */ 177 | public final static int URL_SAFE = 16; 178 | 179 | 180 | /** 181 | * Encode using the special "ordered" dialect of Base64 described here: 182 | * http://www.faqs.org/qa/rfcc-1940.html. 183 | */ 184 | public final static int ORDERED = 32; 185 | 186 | 187 | /* ******** P R I V A T E F I E L D S ******** */ 188 | 189 | 190 | /** Maximum line length (76) of Base64 output. */ 191 | private final static int MAX_LINE_LENGTH = 76; 192 | 193 | 194 | /** The equals sign (=) as a byte. */ 195 | private final static byte EQUALS_SIGN = (byte)'='; 196 | 197 | 198 | /** The new line character (\n) as a byte. */ 199 | private final static byte NEW_LINE = (byte)'\n'; 200 | 201 | 202 | /** Preferred encoding. */ 203 | private final static String PREFERRED_ENCODING = "US-ASCII"; 204 | 205 | 206 | private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding 207 | private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding 208 | 209 | 210 | /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ 211 | 212 | /** The 64 valid Base64 values. */ 213 | /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ 214 | private final static byte[] _STANDARD_ALPHABET = { 215 | (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', 216 | (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', 217 | (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', 218 | (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', 219 | (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', 220 | (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', 221 | (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', 222 | (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', 223 | (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', 224 | (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/' 225 | }; 226 | 227 | 228 | /** 229 | * Translates a Base64 value to either its 6-bit reconstruction value 230 | * or a negative number indicating some other meaning. 231 | **/ 232 | private final static byte[] _STANDARD_DECODABET = { 233 | -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 234 | -5,-5, // Whitespace: Tab and Linefeed 235 | -9,-9, // Decimal 11 - 12 236 | -5, // Whitespace: Carriage Return 237 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 238 | -9,-9,-9,-9,-9, // Decimal 27 - 31 239 | -5, // Whitespace: Space 240 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 241 | 62, // Plus sign at decimal 43 242 | -9,-9,-9, // Decimal 44 - 46 243 | 63, // Slash at decimal 47 244 | 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine 245 | -9,-9,-9, // Decimal 58 - 60 246 | -1, // Equals sign at decimal 61 247 | -9,-9,-9, // Decimal 62 - 64 248 | 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' 249 | 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' 250 | -9,-9,-9,-9,-9,-9, // Decimal 91 - 96 251 | 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' 252 | 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' 253 | -9,-9,-9,-9 // Decimal 123 - 126 254 | /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 255 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 256 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 257 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 258 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 259 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 260 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 261 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 262 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 263 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 264 | }; 265 | 266 | 267 | /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ 268 | 269 | /** 270 | * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: 271 | * http://www.faqs.org/rfcs/rfc3548.html. 272 | * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." 273 | */ 274 | private final static byte[] _URL_SAFE_ALPHABET = { 275 | (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', 276 | (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', 277 | (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', 278 | (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', 279 | (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', 280 | (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', 281 | (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', 282 | (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z', 283 | (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', 284 | (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_' 285 | }; 286 | 287 | /** 288 | * Used in decoding URL- and Filename-safe dialects of Base64. 289 | */ 290 | private final static byte[] _URL_SAFE_DECODABET = { 291 | -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 292 | -5,-5, // Whitespace: Tab and Linefeed 293 | -9,-9, // Decimal 11 - 12 294 | -5, // Whitespace: Carriage Return 295 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 296 | -9,-9,-9,-9,-9, // Decimal 27 - 31 297 | -5, // Whitespace: Space 298 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 299 | -9, // Plus sign at decimal 43 300 | -9, // Decimal 44 301 | 62, // Minus sign at decimal 45 302 | -9, // Decimal 46 303 | -9, // Slash at decimal 47 304 | 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine 305 | -9,-9,-9, // Decimal 58 - 60 306 | -1, // Equals sign at decimal 61 307 | -9,-9,-9, // Decimal 62 - 64 308 | 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N' 309 | 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z' 310 | -9,-9,-9,-9, // Decimal 91 - 94 311 | 63, // Underscore at decimal 95 312 | -9, // Decimal 96 313 | 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm' 314 | 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z' 315 | -9,-9,-9,-9 // Decimal 123 - 126 316 | /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 317 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 318 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 319 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 320 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 321 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 322 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 323 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 324 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 325 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 326 | }; 327 | 328 | 329 | 330 | /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ 331 | 332 | /** 333 | * I don't get the point of this technique, but someone requested it, 334 | * and it is described here: 335 | * http://www.faqs.org/qa/rfcc-1940.html. 336 | */ 337 | private final static byte[] _ORDERED_ALPHABET = { 338 | (byte)'-', 339 | (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', 340 | (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', 341 | (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G', 342 | (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N', 343 | (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U', 344 | (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z', 345 | (byte)'_', 346 | (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g', 347 | (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n', 348 | (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u', 349 | (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z' 350 | }; 351 | 352 | /** 353 | * Used in decoding the "ordered" dialect of Base64. 354 | */ 355 | private final static byte[] _ORDERED_DECODABET = { 356 | -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8 357 | -5,-5, // Whitespace: Tab and Linefeed 358 | -9,-9, // Decimal 11 - 12 359 | -5, // Whitespace: Carriage Return 360 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26 361 | -9,-9,-9,-9,-9, // Decimal 27 - 31 362 | -5, // Whitespace: Space 363 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42 364 | -9, // Plus sign at decimal 43 365 | -9, // Decimal 44 366 | 0, // Minus sign at decimal 45 367 | -9, // Decimal 46 368 | -9, // Slash at decimal 47 369 | 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine 370 | -9,-9,-9, // Decimal 58 - 60 371 | -1, // Equals sign at decimal 61 372 | -9,-9,-9, // Decimal 62 - 64 373 | 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M' 374 | 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z' 375 | -9,-9,-9,-9, // Decimal 91 - 94 376 | 37, // Underscore at decimal 95 377 | -9, // Decimal 96 378 | 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm' 379 | 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z' 380 | -9,-9,-9,-9 // Decimal 123 - 126 381 | /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 382 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 383 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 384 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 385 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 386 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 387 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 388 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 389 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 390 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 391 | }; 392 | 393 | 394 | /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ 395 | 396 | 397 | /** 398 | * Returns one of the _SOMETHING_ALPHABET byte arrays depending on 399 | * the options specified. 400 | * It's possible, though silly, to specify ORDERED and URLSAFE 401 | * in which case one of them will be picked, though there is 402 | * no guarantee as to which one will be picked. 403 | */ 404 | private final static byte[] getAlphabet( int options ) { 405 | if ((options & URL_SAFE) == URL_SAFE) { 406 | return _URL_SAFE_ALPHABET; 407 | } else if ((options & ORDERED) == ORDERED) { 408 | return _ORDERED_ALPHABET; 409 | } else { 410 | return _STANDARD_ALPHABET; 411 | } 412 | } // end getAlphabet 413 | 414 | 415 | /** 416 | * Returns one of the _SOMETHING_DECODABET byte arrays depending on 417 | * the options specified. 418 | * It's possible, though silly, to specify ORDERED and URL_SAFE 419 | * in which case one of them will be picked, though there is 420 | * no guarantee as to which one will be picked. 421 | */ 422 | private final static byte[] getDecodabet( int options ) { 423 | if( (options & URL_SAFE) == URL_SAFE) { 424 | return _URL_SAFE_DECODABET; 425 | } else if ((options & ORDERED) == ORDERED) { 426 | return _ORDERED_DECODABET; 427 | } else { 428 | return _STANDARD_DECODABET; 429 | } 430 | } // end getAlphabet 431 | 432 | 433 | 434 | /** Defeats instantiation. */ 435 | private Base64(){} 436 | 437 | 438 | 439 | 440 | /* ******** E N C O D I N G M E T H O D S ******** */ 441 | 442 | 443 | /** 444 | * Encodes up to the first three bytes of array threeBytes 445 | * and returns a four-byte array in Base64 notation. 446 | * The actual number of significant bytes in your array is 447 | * given by numSigBytes. 448 | * The array threeBytes needs only be as big as 449 | * numSigBytes. 450 | * Code can reuse a byte array by passing a four-byte array as b4. 451 | * 452 | * @param b4 A reusable byte array to reduce array instantiation 453 | * @param threeBytes the array to convert 454 | * @param numSigBytes the number of significant bytes in your array 455 | * @return four byte array in Base64 notation. 456 | * @since 1.5.1 457 | */ 458 | private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options ) { 459 | encode3to4( threeBytes, 0, numSigBytes, b4, 0, options ); 460 | return b4; 461 | } // end encode3to4 462 | 463 | 464 | /** 465 | *

Encodes up to three bytes of the array source 466 | * and writes the resulting four Base64 bytes to destination. 467 | * The source and destination arrays can be manipulated 468 | * anywhere along their length by specifying 469 | * srcOffset and destOffset. 470 | * This method does not check to make sure your arrays 471 | * are large enough to accomodate srcOffset + 3 for 472 | * the source array or destOffset + 4 for 473 | * the destination array. 474 | * The actual number of significant bytes in your array is 475 | * given by numSigBytes.

476 | *

This is the lowest level of the encoding methods with 477 | * all possible parameters.

478 | * 479 | * @param source the array to convert 480 | * @param srcOffset the index where conversion begins 481 | * @param numSigBytes the number of significant bytes in your array 482 | * @param destination the array to hold the conversion 483 | * @param destOffset the index where output will be put 484 | * @return the destination array 485 | * @since 1.3 486 | */ 487 | private static byte[] encode3to4( 488 | byte[] source, int srcOffset, int numSigBytes, 489 | byte[] destination, int destOffset, int options ) { 490 | 491 | byte[] ALPHABET = getAlphabet( options ); 492 | 493 | // 1 2 3 494 | // 01234567890123456789012345678901 Bit position 495 | // --------000000001111111122222222 Array position from threeBytes 496 | // --------| || || || | Six bit groups to index ALPHABET 497 | // >>18 >>12 >> 6 >> 0 Right shift necessary 498 | // 0x3f 0x3f 0x3f Additional AND 499 | 500 | // Create buffer with zero-padding if there are only one or two 501 | // significant bytes passed in the array. 502 | // We have to shift left 24 in order to flush out the 1's that appear 503 | // when Java treats a value as negative that is cast from a byte to an int. 504 | int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 ) 505 | | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 ) 506 | | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 ); 507 | 508 | switch( numSigBytes ) 509 | { 510 | case 3: 511 | destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; 512 | destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; 513 | destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; 514 | destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ]; 515 | return destination; 516 | 517 | case 2: 518 | destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; 519 | destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; 520 | destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ]; 521 | destination[ destOffset + 3 ] = EQUALS_SIGN; 522 | return destination; 523 | 524 | case 1: 525 | destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ]; 526 | destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ]; 527 | destination[ destOffset + 2 ] = EQUALS_SIGN; 528 | destination[ destOffset + 3 ] = EQUALS_SIGN; 529 | return destination; 530 | 531 | default: 532 | return destination; 533 | } // end switch 534 | } // end encode3to4 535 | 536 | 537 | 538 | /** 539 | * Performs Base64 encoding on the raw ByteBuffer, 540 | * writing it to the encoded ByteBuffer. 541 | * This is an experimental feature. Currently it does not 542 | * pass along any options (such as {@link #DO_BREAK_LINES} 543 | * or {@link #GZIP}. 544 | * 545 | * @param raw input buffer 546 | * @param encoded output buffer 547 | * @since 2.3 548 | */ 549 | public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded ){ 550 | byte[] raw3 = new byte[3]; 551 | byte[] enc4 = new byte[4]; 552 | 553 | while( raw.hasRemaining() ){ 554 | int rem = Math.min(3,raw.remaining()); 555 | raw.get(raw3,0,rem); 556 | Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); 557 | encoded.put(enc4); 558 | } // end input remaining 559 | } 560 | 561 | 562 | /** 563 | * Performs Base64 encoding on the raw ByteBuffer, 564 | * writing it to the encoded CharBuffer. 565 | * This is an experimental feature. Currently it does not 566 | * pass along any options (such as {@link #DO_BREAK_LINES} 567 | * or {@link #GZIP}. 568 | * 569 | * @param raw input buffer 570 | * @param encoded output buffer 571 | * @since 2.3 572 | */ 573 | public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded ){ 574 | byte[] raw3 = new byte[3]; 575 | byte[] enc4 = new byte[4]; 576 | 577 | while( raw.hasRemaining() ){ 578 | int rem = Math.min(3,raw.remaining()); 579 | raw.get(raw3,0,rem); 580 | Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS ); 581 | for( int i = 0; i < 4; i++ ){ 582 | encoded.put( (char)(enc4[i] & 0xFF) ); 583 | } 584 | } // end input remaining 585 | } 586 | 587 | 588 | 589 | 590 | /** 591 | * Serializes an object and returns the Base64-encoded 592 | * version of that serialized object. 593 | * 594 | *

As of v 2.3, if the object 595 | * cannot be serialized or there is another error, 596 | * the method will throw an java.io.IOException. This is new to v2.3! 597 | * In earlier versions, it just returned a null value, but 598 | * in retrospect that's a pretty poor way to handle it.

599 | * 600 | * The object is not GZip-compressed before being encoded. 601 | * 602 | * @param serializableObject The object to encode 603 | * @return The Base64-encoded object 604 | * @throws java.io.IOException if there is an error 605 | * @throws NullPointerException if serializedObject is null 606 | * @since 1.4 607 | */ 608 | public static String encodeObject( java.io.Serializable serializableObject ) 609 | throws java.io.IOException { 610 | return encodeObject( serializableObject, NO_OPTIONS ); 611 | } // end encodeObject 612 | 613 | 614 | 615 | /** 616 | * Serializes an object and returns the Base64-encoded 617 | * version of that serialized object. 618 | * 619 | *

As of v 2.3, if the object 620 | * cannot be serialized or there is another error, 621 | * the method will throw an java.io.IOException. This is new to v2.3! 622 | * In earlier versions, it just returned a null value, but 623 | * in retrospect that's a pretty poor way to handle it.

624 | * 625 | * The object is not GZip-compressed before being encoded. 626 | *

627 | * Example options:

 628 |      *   GZIP: gzip-compresses object before encoding it.
 629 |      *   DO_BREAK_LINES: break lines at 76 characters
 630 |      * 
631 | *

632 | * Example: encodeObject( myObj, Base64.GZIP ) or 633 | *

634 | * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) 635 | * 636 | * @param serializableObject The object to encode 637 | * @param options Specified options 638 | * @return The Base64-encoded object 639 | * @see Base64#GZIP 640 | * @see Base64#DO_BREAK_LINES 641 | * @throws java.io.IOException if there is an error 642 | * @since 2.0 643 | */ 644 | public static String encodeObject( java.io.Serializable serializableObject, int options ) 645 | throws java.io.IOException { 646 | 647 | if( serializableObject == null ){ 648 | throw new NullPointerException( "Cannot serialize a null object." ); 649 | } // end if: null 650 | 651 | // Streams 652 | java.io.ByteArrayOutputStream baos = null; 653 | java.io.OutputStream b64os = null; 654 | java.util.zip.GZIPOutputStream gzos = null; 655 | java.io.ObjectOutputStream oos = null; 656 | 657 | 658 | try { 659 | // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream 660 | baos = new java.io.ByteArrayOutputStream(); 661 | b64os = new Base64.OutputStream( baos, ENCODE | options ); 662 | if( (options & GZIP) != 0 ){ 663 | // Gzip 664 | gzos = new java.util.zip.GZIPOutputStream(b64os); 665 | oos = new java.io.ObjectOutputStream( gzos ); 666 | } else { 667 | // Not gzipped 668 | oos = new java.io.ObjectOutputStream( b64os ); 669 | } 670 | oos.writeObject( serializableObject ); 671 | } // end try 672 | catch( java.io.IOException e ) { 673 | // Catch it and then throw it immediately so that 674 | // the finally{} block is called for cleanup. 675 | throw e; 676 | } // end catch 677 | finally { 678 | try{ oos.close(); } catch( Exception e ){} 679 | try{ gzos.close(); } catch( Exception e ){} 680 | try{ b64os.close(); } catch( Exception e ){} 681 | try{ baos.close(); } catch( Exception e ){} 682 | } // end finally 683 | 684 | // Return value according to relevant encoding. 685 | try { 686 | return new String( baos.toByteArray(), PREFERRED_ENCODING ); 687 | } // end try 688 | catch (java.io.UnsupportedEncodingException uue){ 689 | // Fall back to some Java default 690 | return new String( baos.toByteArray() ); 691 | } // end catch 692 | 693 | } // end encode 694 | 695 | 696 | 697 | /** 698 | * Encodes a byte array into Base64 notation. 699 | * Does not GZip-compress data. 700 | * 701 | * @param source The data to convert 702 | * @return The data in Base64-encoded form 703 | * @throws NullPointerException if source array is null 704 | * @since 1.4 705 | */ 706 | public static String encodeBytes( byte[] source ) { 707 | // Since we're not going to have the GZIP encoding turned on, 708 | // we're not going to have an java.io.IOException thrown, so 709 | // we should not force the user to have to catch it. 710 | String encoded = null; 711 | try { 712 | encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); 713 | } catch (java.io.IOException ex) { 714 | assert false : ex.getMessage(); 715 | } // end catch 716 | assert encoded != null; 717 | return encoded; 718 | } // end encodeBytes 719 | 720 | 721 | 722 | /** 723 | * Encodes a byte array into Base64 notation. 724 | *

725 | * Example options:

 726 |      *   GZIP: gzip-compresses object before encoding it.
 727 |      *   DO_BREAK_LINES: break lines at 76 characters
 728 |      *     Note: Technically, this makes your encoding non-compliant.
 729 |      * 
730 | *

731 | * Example: encodeBytes( myData, Base64.GZIP ) or 732 | *

733 | * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) 734 | * 735 | * 736 | *

As of v 2.3, if there is an error with the GZIP stream, 737 | * the method will throw an java.io.IOException. This is new to v2.3! 738 | * In earlier versions, it just returned a null value, but 739 | * in retrospect that's a pretty poor way to handle it.

740 | * 741 | * 742 | * @param source The data to convert 743 | * @param options Specified options 744 | * @return The Base64-encoded data as a String 745 | * @see Base64#GZIP 746 | * @see Base64#DO_BREAK_LINES 747 | * @throws java.io.IOException if there is an error 748 | * @throws NullPointerException if source array is null 749 | * @since 2.0 750 | */ 751 | public static String encodeBytes( byte[] source, int options ) throws java.io.IOException { 752 | return encodeBytes( source, 0, source.length, options ); 753 | } // end encodeBytes 754 | 755 | 756 | /** 757 | * Encodes a byte array into Base64 notation. 758 | * Does not GZip-compress data. 759 | * 760 | *

As of v 2.3, if there is an error, 761 | * the method will throw an java.io.IOException. This is new to v2.3! 762 | * In earlier versions, it just returned a null value, but 763 | * in retrospect that's a pretty poor way to handle it.

764 | * 765 | * 766 | * @param source The data to convert 767 | * @param off Offset in array where conversion should begin 768 | * @param len Length of data to convert 769 | * @return The Base64-encoded data as a String 770 | * @throws NullPointerException if source array is null 771 | * @throws IllegalArgumentException if source array, offset, or length are invalid 772 | * @since 1.4 773 | */ 774 | public static String encodeBytes( byte[] source, int off, int len ) { 775 | // Since we're not going to have the GZIP encoding turned on, 776 | // we're not going to have an java.io.IOException thrown, so 777 | // we should not force the user to have to catch it. 778 | String encoded = null; 779 | try { 780 | encoded = encodeBytes( source, off, len, NO_OPTIONS ); 781 | } catch (java.io.IOException ex) { 782 | assert false : ex.getMessage(); 783 | } // end catch 784 | assert encoded != null; 785 | return encoded; 786 | } // end encodeBytes 787 | 788 | 789 | 790 | /** 791 | * Encodes a byte array into Base64 notation. 792 | *

793 | * Example options:

 794 |      *   GZIP: gzip-compresses object before encoding it.
 795 |      *   DO_BREAK_LINES: break lines at 76 characters
 796 |      *     Note: Technically, this makes your encoding non-compliant.
 797 |      * 
798 | *

799 | * Example: encodeBytes( myData, Base64.GZIP ) or 800 | *

801 | * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) 802 | * 803 | * 804 | *

As of v 2.3, if there is an error with the GZIP stream, 805 | * the method will throw an java.io.IOException. This is new to v2.3! 806 | * In earlier versions, it just returned a null value, but 807 | * in retrospect that's a pretty poor way to handle it.

808 | * 809 | * 810 | * @param source The data to convert 811 | * @param off Offset in array where conversion should begin 812 | * @param len Length of data to convert 813 | * @param options Specified options 814 | * @return The Base64-encoded data as a String 815 | * @see Base64#GZIP 816 | * @see Base64#DO_BREAK_LINES 817 | * @throws java.io.IOException if there is an error 818 | * @throws NullPointerException if source array is null 819 | * @throws IllegalArgumentException if source array, offset, or length are invalid 820 | * @since 2.0 821 | */ 822 | public static String encodeBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { 823 | byte[] encoded = encodeBytesToBytes( source, off, len, options ); 824 | 825 | // Return value according to relevant encoding. 826 | try { 827 | return new String( encoded, PREFERRED_ENCODING ); 828 | } // end try 829 | catch (java.io.UnsupportedEncodingException uue) { 830 | return new String( encoded ); 831 | } // end catch 832 | 833 | } // end encodeBytes 834 | 835 | 836 | 837 | 838 | /** 839 | * Similar to {@link #encodeBytes(byte[])} but returns 840 | * a byte array instead of instantiating a String. This is more efficient 841 | * if you're working with I/O streams and have large data sets to encode. 842 | * 843 | * 844 | * @param source The data to convert 845 | * @return The Base64-encoded data as a byte[] (of ASCII characters) 846 | * @throws NullPointerException if source array is null 847 | * @since 2.3.1 848 | */ 849 | public static byte[] encodeBytesToBytes( byte[] source ) { 850 | byte[] encoded = null; 851 | try { 852 | encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS ); 853 | } catch( java.io.IOException ex ) { 854 | assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); 855 | } 856 | return encoded; 857 | } 858 | 859 | 860 | /** 861 | * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns 862 | * a byte array instead of instantiating a String. This is more efficient 863 | * if you're working with I/O streams and have large data sets to encode. 864 | * 865 | * 866 | * @param source The data to convert 867 | * @param off Offset in array where conversion should begin 868 | * @param len Length of data to convert 869 | * @param options Specified options 870 | * @return The Base64-encoded data as a String 871 | * @see Base64#GZIP 872 | * @see Base64#DO_BREAK_LINES 873 | * @throws java.io.IOException if there is an error 874 | * @throws NullPointerException if source array is null 875 | * @throws IllegalArgumentException if source array, offset, or length are invalid 876 | * @since 2.3.1 877 | */ 878 | public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException { 879 | 880 | if( source == null ){ 881 | throw new NullPointerException( "Cannot serialize a null array." ); 882 | } // end if: null 883 | 884 | if( off < 0 ){ 885 | throw new IllegalArgumentException( "Cannot have negative offset: " + off ); 886 | } // end if: off < 0 887 | 888 | if( len < 0 ){ 889 | throw new IllegalArgumentException( "Cannot have length offset: " + len ); 890 | } // end if: len < 0 891 | 892 | if( off + len > source.length ){ 893 | throw new IllegalArgumentException( 894 | "Cannot have offset of " + off + " and length of " + len + " with array of length " + source.length); 895 | } // end if: off < 0 896 | 897 | 898 | 899 | // Compress? 900 | if( (options & GZIP) != 0 ) { 901 | java.io.ByteArrayOutputStream baos = null; 902 | java.util.zip.GZIPOutputStream gzos = null; 903 | Base64.OutputStream b64os = null; 904 | 905 | try { 906 | // GZip -> Base64 -> ByteArray 907 | baos = new java.io.ByteArrayOutputStream(); 908 | b64os = new Base64.OutputStream( baos, ENCODE | options ); 909 | gzos = new java.util.zip.GZIPOutputStream( b64os ); 910 | 911 | gzos.write( source, off, len ); 912 | gzos.close(); 913 | } // end try 914 | catch( java.io.IOException e ) { 915 | // Catch it and then throw it immediately so that 916 | // the finally{} block is called for cleanup. 917 | throw e; 918 | } // end catch 919 | finally { 920 | try{ gzos.close(); } catch( Exception e ){} 921 | try{ b64os.close(); } catch( Exception e ){} 922 | try{ baos.close(); } catch( Exception e ){} 923 | } // end finally 924 | 925 | return baos.toByteArray(); 926 | } // end if: compress 927 | 928 | // Else, don't compress. Better not to use streams at all then. 929 | else { 930 | boolean breakLines = (options & DO_BREAK_LINES) > 0; 931 | 932 | //int len43 = len * 4 / 3; 933 | //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 934 | // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding 935 | // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines 936 | // Try to determine more precisely how big the array needs to be. 937 | // If we get it right, we don't have to do an array copy, and 938 | // we save a bunch of memory. 939 | int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding 940 | if( breakLines ){ 941 | encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters 942 | } 943 | byte[] outBuff = new byte[ encLen ]; 944 | 945 | 946 | int d = 0; 947 | int e = 0; 948 | int len2 = len - 2; 949 | int lineLength = 0; 950 | for( ; d < len2; d+=3, e+=4 ) { 951 | encode3to4( source, d+off, 3, outBuff, e, options ); 952 | 953 | lineLength += 4; 954 | if( breakLines && lineLength >= MAX_LINE_LENGTH ) 955 | { 956 | outBuff[e+4] = NEW_LINE; 957 | e++; 958 | lineLength = 0; 959 | } // end if: end of line 960 | } // en dfor: each piece of array 961 | 962 | if( d < len ) { 963 | encode3to4( source, d+off, len - d, outBuff, e, options ); 964 | e += 4; 965 | } // end if: some padding needed 966 | 967 | 968 | // Only resize array if we didn't guess it right. 969 | if( e < outBuff.length - 1 ){ 970 | byte[] finalOut = new byte[e]; 971 | System.arraycopy(outBuff,0, finalOut,0,e); 972 | //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); 973 | return finalOut; 974 | } else { 975 | //System.err.println("No need to resize array."); 976 | return outBuff; 977 | } 978 | 979 | } // end else: don't compress 980 | 981 | } // end encodeBytesToBytes 982 | 983 | 984 | 985 | 986 | 987 | /* ******** D E C O D I N G M E T H O D S ******** */ 988 | 989 | 990 | /** 991 | * Decodes four bytes from array source 992 | * and writes the resulting bytes (up to three of them) 993 | * to destination. 994 | * The source and destination arrays can be manipulated 995 | * anywhere along their length by specifying 996 | * srcOffset and destOffset. 997 | * This method does not check to make sure your arrays 998 | * are large enough to accomodate srcOffset + 4 for 999 | * the source array or destOffset + 3 for 1000 | * the destination array. 1001 | * This method returns the actual number of bytes that 1002 | * were converted from the Base64 encoding. 1003 | *

This is the lowest level of the decoding methods with 1004 | * all possible parameters.

1005 | * 1006 | * 1007 | * @param source the array to convert 1008 | * @param srcOffset the index where conversion begins 1009 | * @param destination the array to hold the conversion 1010 | * @param destOffset the index where output will be put 1011 | * @param options alphabet type is pulled from this (standard, url-safe, ordered) 1012 | * @return the number of decoded bytes converted 1013 | * @throws NullPointerException if source or destination arrays are null 1014 | * @throws IllegalArgumentException if srcOffset or destOffset are invalid 1015 | * or there is not enough room in the array. 1016 | * @since 1.3 1017 | */ 1018 | private static int decode4to3( 1019 | byte[] source, int srcOffset, 1020 | byte[] destination, int destOffset, int options ) { 1021 | 1022 | // Lots of error checking and exception throwing 1023 | if( source == null ){ 1024 | throw new NullPointerException( "Source array was null." ); 1025 | } // end if 1026 | if( destination == null ){ 1027 | throw new NullPointerException( "Destination array was null." ); 1028 | } // end if 1029 | if( srcOffset < 0 || srcOffset + 3 >= source.length ){ 1030 | throw new IllegalArgumentException( 1031 | "Source array with length " + source.length + " cannot have offset of " + srcOffset + " and still process four bytes."); 1032 | } // end if 1033 | if( destOffset < 0 || destOffset +2 >= destination.length ){ 1034 | throw new IllegalArgumentException( 1035 | "Destination array with length " + destination.length + " cannot have offset of " ); 1036 | } // end if 1037 | 1038 | 1039 | byte[] DECODABET = getDecodabet( options ); 1040 | 1041 | // Example: Dk== 1042 | if( source[ srcOffset + 2] == EQUALS_SIGN ) { 1043 | // Two ways to do the same thing. Don't know which way I like best. 1044 | //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) 1045 | // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); 1046 | int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) 1047 | | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 ); 1048 | 1049 | destination[ destOffset ] = (byte)( outBuff >>> 16 ); 1050 | return 1; 1051 | } 1052 | 1053 | // Example: DkL= 1054 | else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) { 1055 | // Two ways to do the same thing. Don't know which way I like best. 1056 | //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) 1057 | // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) 1058 | // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); 1059 | int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) 1060 | | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) 1061 | | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 ); 1062 | 1063 | destination[ destOffset ] = (byte)( outBuff >>> 16 ); 1064 | destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 ); 1065 | return 2; 1066 | } 1067 | 1068 | // Example: DkLE 1069 | else { 1070 | // Two ways to do the same thing. Don't know which way I like best. 1071 | //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) 1072 | // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) 1073 | // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) 1074 | // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); 1075 | int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 ) 1076 | | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 ) 1077 | | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6) 1078 | | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) ); 1079 | 1080 | 1081 | destination[ destOffset ] = (byte)( outBuff >> 16 ); 1082 | destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); 1083 | destination[ destOffset + 2 ] = (byte)( outBuff ); 1084 | 1085 | return 3; 1086 | } 1087 | } // end decodeToBytes 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | /** 1094 | * Low-level access to decoding ASCII characters in 1095 | * the form of a byte array. Ignores GUNZIP option, if 1096 | * it's set. This is not generally a recommended method, 1097 | * although it is used internally as part of the decoding process. 1098 | * Special case: if len = 0, an empty array is returned. Still, 1099 | * if you need more speed and reduced memory footprint (and aren't 1100 | * gzipping), consider this method. 1101 | * 1102 | * @param source The Base64 encoded data 1103 | * @return decoded data 1104 | * @since 2.3.1 1105 | */ 1106 | public static byte[] decode( byte[] source ){ 1107 | byte[] decoded = null; 1108 | try { 1109 | decoded = decode( source, 0, source.length, Base64.NO_OPTIONS ); 1110 | } catch( java.io.IOException ex ) { 1111 | assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); 1112 | } 1113 | return decoded; 1114 | } 1115 | 1116 | 1117 | 1118 | /** 1119 | * Low-level access to decoding ASCII characters in 1120 | * the form of a byte array. Ignores GUNZIP option, if 1121 | * it's set. This is not generally a recommended method, 1122 | * although it is used internally as part of the decoding process. 1123 | * Special case: if len = 0, an empty array is returned. Still, 1124 | * if you need more speed and reduced memory footprint (and aren't 1125 | * gzipping), consider this method. 1126 | * 1127 | * @param source The Base64 encoded data 1128 | * @param off The offset of where to begin decoding 1129 | * @param len The length of characters to decode 1130 | * @param options Can specify options such as alphabet type to use 1131 | * @return decoded data 1132 | * @throws java.io.IOException If bogus characters exist in source data 1133 | * @since 1.3 1134 | */ 1135 | public static byte[] decode( byte[] source, int off, int len, int options ) 1136 | throws java.io.IOException { 1137 | 1138 | // Lots of error checking and exception throwing 1139 | if( source == null ){ 1140 | throw new NullPointerException( "Cannot decode null source array." ); 1141 | } // end if 1142 | if( off < 0 || off + len > source.length ){ 1143 | throw new IllegalArgumentException( 1144 | "Source array with length " + source.length + " cannot have offset of " + off + " and process " + len + " bytes." ); 1145 | } // end if 1146 | 1147 | if( len == 0 ){ 1148 | return new byte[0]; 1149 | }else if( len < 4 ){ 1150 | throw new IllegalArgumentException( 1151 | "Base64-encoded string must have at least four characters, but length specified was " + len ); 1152 | } // end if 1153 | 1154 | byte[] DECODABET = getDecodabet( options ); 1155 | 1156 | int len34 = len * 3 / 4; // Estimate on array size 1157 | byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output 1158 | int outBuffPosn = 0; // Keep track of where we're writing 1159 | 1160 | byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space 1161 | int b4Posn = 0; // Keep track of four byte input buffer 1162 | int i = 0; // Source array counter 1163 | byte sbiCrop = 0; // Low seven bits (ASCII) of input 1164 | byte sbiDecode = 0; // Special value from DECODABET 1165 | 1166 | for( i = off; i < off+len; i++ ) { // Loop through source 1167 | 1168 | sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits 1169 | sbiDecode = DECODABET[ sbiCrop ]; // Special value 1170 | 1171 | // White space, Equals sign, or legit Base64 character 1172 | // Note the values such as -5 and -9 in the 1173 | // DECODABETs at the top of the file. 1174 | if( sbiDecode >= WHITE_SPACE_ENC ) { 1175 | if( sbiDecode >= EQUALS_SIGN_ENC ) { 1176 | b4[ b4Posn++ ] = sbiCrop; // Save non-whitespace 1177 | if( b4Posn > 3 ) { // Time to decode? 1178 | outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options ); 1179 | b4Posn = 0; 1180 | 1181 | // If that was the equals sign, break out of 'for' loop 1182 | if( sbiCrop == EQUALS_SIGN ) { 1183 | break; 1184 | } // end if: equals sign 1185 | } // end if: quartet built 1186 | } // end if: equals sign or better 1187 | } // end if: white space, equals sign or better 1188 | else { 1189 | // There's a bad input character in the Base64 stream. 1190 | throw new java.io.IOException( 1191 | "Bad Base64 input character '" + source[i] + "' in array position " + i); 1192 | } // end else: 1193 | } // each input character 1194 | 1195 | byte[] out = new byte[ outBuffPosn ]; 1196 | System.arraycopy( outBuff, 0, out, 0, outBuffPosn ); 1197 | return out; 1198 | } // end decode 1199 | 1200 | 1201 | 1202 | 1203 | /** 1204 | * Decodes data from Base64 notation, automatically 1205 | * detecting gzip-compressed data and decompressing it. 1206 | * 1207 | * @param s the string to decode 1208 | * @return the decoded data 1209 | * @throws java.io.IOException If there is a problem 1210 | * @since 1.4 1211 | */ 1212 | public static byte[] decode( String s ) throws java.io.IOException { 1213 | return decode( s, NO_OPTIONS ); 1214 | } 1215 | 1216 | 1217 | 1218 | /** 1219 | * Decodes data from Base64 notation, automatically 1220 | * detecting gzip-compressed data and decompressing it. 1221 | * 1222 | * @param s the string to decode 1223 | * @param options encode options such as URL_SAFE 1224 | * @return the decoded data 1225 | * @throws java.io.IOException if there is an error 1226 | * @throws NullPointerException if s is null 1227 | * @since 1.4 1228 | */ 1229 | public static byte[] decode( String s, int options ) throws java.io.IOException { 1230 | 1231 | if( s == null ){ 1232 | throw new NullPointerException( "Input string was null." ); 1233 | } // end if 1234 | 1235 | byte[] bytes; 1236 | try { 1237 | bytes = s.getBytes( PREFERRED_ENCODING ); 1238 | } // end try 1239 | catch( java.io.UnsupportedEncodingException uee ) { 1240 | bytes = s.getBytes(); 1241 | } // end catch 1242 | // 1243 | 1244 | // Decode 1245 | bytes = decode( bytes, 0, bytes.length, options ); 1246 | 1247 | // Check to see if it's gzip-compressed 1248 | // GZIP Magic Two-Byte Number: 0x8b1f (35615) 1249 | boolean dontGunzip = (options & DONT_GUNZIP) != 0; 1250 | if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) { 1251 | 1252 | int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); 1253 | if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) { 1254 | java.io.ByteArrayInputStream bais = null; 1255 | java.util.zip.GZIPInputStream gzis = null; 1256 | java.io.ByteArrayOutputStream baos = null; 1257 | byte[] buffer = new byte[2048]; 1258 | int length = 0; 1259 | 1260 | try { 1261 | baos = new java.io.ByteArrayOutputStream(); 1262 | bais = new java.io.ByteArrayInputStream( bytes ); 1263 | gzis = new java.util.zip.GZIPInputStream( bais ); 1264 | 1265 | while( ( length = gzis.read( buffer ) ) >= 0 ) { 1266 | baos.write(buffer,0,length); 1267 | } // end while: reading input 1268 | 1269 | // No error? Get new bytes. 1270 | bytes = baos.toByteArray(); 1271 | 1272 | } // end try 1273 | catch( java.io.IOException e ) { 1274 | e.printStackTrace(); 1275 | // Just return originally-decoded bytes 1276 | } // end catch 1277 | finally { 1278 | try{ baos.close(); } catch( Exception e ){} 1279 | try{ gzis.close(); } catch( Exception e ){} 1280 | try{ bais.close(); } catch( Exception e ){} 1281 | } // end finally 1282 | 1283 | } // end if: gzipped 1284 | } // end if: bytes.length >= 2 1285 | 1286 | return bytes; 1287 | } // end decode 1288 | 1289 | /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ 1290 | 1291 | 1292 | 1293 | /** 1294 | * A {@link Base64.InputStream} will read data from another 1295 | * java.io.InputStream, given in the constructor, 1296 | * and encode/decode to/from Base64 notation on the fly. 1297 | * 1298 | * @see Base64 1299 | * @since 1.3 1300 | */ 1301 | public static class InputStream extends java.io.FilterInputStream { 1302 | 1303 | private boolean encode; // Encoding or decoding 1304 | private int position; // Current position in the buffer 1305 | private byte[] buffer; // Small buffer holding converted data 1306 | private int bufferLength; // Length of buffer (3 or 4) 1307 | private int numSigBytes; // Number of meaningful bytes in the buffer 1308 | private int lineLength; 1309 | private boolean breakLines; // Break lines at less than 80 characters 1310 | private int options; // Record options used to create the stream. 1311 | private byte[] decodabet; // Local copies to avoid extra method calls 1312 | 1313 | 1314 | /** 1315 | * Constructs a {@link Base64.InputStream} in DECODE mode. 1316 | * 1317 | * @param in the java.io.InputStream from which to read data. 1318 | * @since 1.3 1319 | */ 1320 | public InputStream( java.io.InputStream in ) { 1321 | this( in, DECODE ); 1322 | } // end constructor 1323 | 1324 | 1325 | /** 1326 | * Constructs a {@link Base64.InputStream} in 1327 | * either ENCODE or DECODE mode. 1328 | *

1329 | * Valid options:

1330 |          *   ENCODE or DECODE: Encode or Decode as data is read.
1331 |          *   DO_BREAK_LINES: break lines at 76 characters
1332 |          *     (only meaningful when encoding)
1333 |          * 
1334 | *

1335 | * Example: new Base64.InputStream( in, Base64.DECODE ) 1336 | * 1337 | * 1338 | * @param in the java.io.InputStream from which to read data. 1339 | * @param options Specified options 1340 | * @see Base64#ENCODE 1341 | * @see Base64#DECODE 1342 | * @see Base64#DO_BREAK_LINES 1343 | * @since 2.0 1344 | */ 1345 | public InputStream( java.io.InputStream in, int options ) { 1346 | 1347 | super( in ); 1348 | this.options = options; // Record for later 1349 | this.breakLines = (options & DO_BREAK_LINES) > 0; 1350 | this.encode = (options & ENCODE) > 0; 1351 | this.bufferLength = encode ? 4 : 3; 1352 | this.buffer = new byte[ bufferLength ]; 1353 | this.position = -1; 1354 | this.lineLength = 0; 1355 | this.decodabet = getDecodabet(options); 1356 | } // end constructor 1357 | 1358 | /** 1359 | * Reads enough of the input stream to convert 1360 | * to/from Base64 and returns the next byte. 1361 | * 1362 | * @return next byte 1363 | * @since 1.3 1364 | */ 1365 | public int read() throws java.io.IOException { 1366 | 1367 | // Do we need to get data? 1368 | if( position < 0 ) { 1369 | if( encode ) { 1370 | byte[] b3 = new byte[3]; 1371 | int numBinaryBytes = 0; 1372 | for( int i = 0; i < 3; i++ ) { 1373 | int b = in.read(); 1374 | 1375 | // If end of stream, b is -1. 1376 | if( b >= 0 ) { 1377 | b3[i] = (byte)b; 1378 | numBinaryBytes++; 1379 | } else { 1380 | break; // out of for loop 1381 | } // end else: end of stream 1382 | 1383 | } // end for: each needed input byte 1384 | 1385 | if( numBinaryBytes > 0 ) { 1386 | encode3to4( b3, 0, numBinaryBytes, buffer, 0, options ); 1387 | position = 0; 1388 | numSigBytes = 4; 1389 | } // end if: got data 1390 | else { 1391 | return -1; // Must be end of stream 1392 | } // end else 1393 | } // end if: encoding 1394 | 1395 | // Else decoding 1396 | else { 1397 | byte[] b4 = new byte[4]; 1398 | int i = 0; 1399 | for( i = 0; i < 4; i++ ) { 1400 | // Read four "meaningful" bytes: 1401 | int b = 0; 1402 | do{ b = in.read(); } 1403 | while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC ); 1404 | 1405 | if( b < 0 ) { 1406 | break; // Reads a -1 if end of stream 1407 | } // end if: end of stream 1408 | 1409 | b4[i] = (byte)b; 1410 | } // end for: each needed input byte 1411 | 1412 | if( i == 4 ) { 1413 | numSigBytes = decode4to3( b4, 0, buffer, 0, options ); 1414 | position = 0; 1415 | } // end if: got four characters 1416 | else if( i == 0 ){ 1417 | return -1; 1418 | } // end else if: also padded correctly 1419 | else { 1420 | // Must have broken out from above. 1421 | throw new java.io.IOException( "Improperly padded Base64 input." ); 1422 | } // end 1423 | 1424 | } // end else: decode 1425 | } // end else: get data 1426 | 1427 | // Got data? 1428 | if( position >= 0 ) { 1429 | // End of relevant data? 1430 | if( /*!encode &&*/ position >= numSigBytes ){ 1431 | return -1; 1432 | } // end if: got data 1433 | 1434 | if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) { 1435 | lineLength = 0; 1436 | return '\n'; 1437 | } // end if 1438 | else { 1439 | lineLength++; // This isn't important when decoding 1440 | // but throwing an extra "if" seems 1441 | // just as wasteful. 1442 | 1443 | int b = buffer[ position++ ]; 1444 | 1445 | if( position >= bufferLength ) { 1446 | position = -1; 1447 | } // end if: end 1448 | 1449 | return b & 0xFF; // This is how you "cast" a byte that's 1450 | // intended to be unsigned. 1451 | } // end else 1452 | } // end if: position >= 0 1453 | 1454 | // Else error 1455 | else { 1456 | throw new java.io.IOException( "Error in Base64 code reading stream." ); 1457 | } // end else 1458 | } // end read 1459 | 1460 | 1461 | /** 1462 | * Calls {@link #read()} repeatedly until the end of stream 1463 | * is reached or len bytes are read. 1464 | * Returns number of bytes read into array or -1 if 1465 | * end of stream is encountered. 1466 | * 1467 | * @param dest array to hold values 1468 | * @param off offset for array 1469 | * @param len max number of bytes to read into array 1470 | * @return bytes read into array or -1 if end of stream is encountered. 1471 | * @since 1.3 1472 | */ 1473 | public int read( byte[] dest, int off, int len ) 1474 | throws java.io.IOException { 1475 | int i; 1476 | int b; 1477 | for( i = 0; i < len; i++ ) { 1478 | b = read(); 1479 | 1480 | if( b >= 0 ) { 1481 | dest[off + i] = (byte) b; 1482 | } 1483 | else if( i == 0 ) { 1484 | return -1; 1485 | } 1486 | else { 1487 | break; // Out of 'for' loop 1488 | } // Out of 'for' loop 1489 | } // end for: each byte read 1490 | return i; 1491 | } // end read 1492 | 1493 | } // end inner class InputStream 1494 | 1495 | 1496 | 1497 | 1498 | 1499 | 1500 | /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ 1501 | 1502 | 1503 | 1504 | /** 1505 | * A {@link Base64.OutputStream} will write data to another 1506 | * java.io.OutputStream, given in the constructor, 1507 | * and encode/decode to/from Base64 notation on the fly. 1508 | * 1509 | * @see Base64 1510 | * @since 1.3 1511 | */ 1512 | public static class OutputStream extends java.io.FilterOutputStream { 1513 | 1514 | private boolean encode; 1515 | private int position; 1516 | private byte[] buffer; 1517 | private int bufferLength; 1518 | private int lineLength; 1519 | private boolean breakLines; 1520 | private byte[] b4; // Scratch used in a few places 1521 | private boolean suspendEncoding; 1522 | private int options; // Record for later 1523 | private byte[] decodabet; // Local copies to avoid extra method calls 1524 | 1525 | /** 1526 | * Constructs a {@link Base64.OutputStream} in ENCODE mode. 1527 | * 1528 | * @param out the java.io.OutputStream to which data will be written. 1529 | * @since 1.3 1530 | */ 1531 | public OutputStream( java.io.OutputStream out ) { 1532 | this( out, ENCODE ); 1533 | } // end constructor 1534 | 1535 | 1536 | /** 1537 | * Constructs a {@link Base64.OutputStream} in 1538 | * either ENCODE or DECODE mode. 1539 | *

1540 | * Valid options:

1541 |          *   ENCODE or DECODE: Encode or Decode as data is read.
1542 |          *   DO_BREAK_LINES: don't break lines at 76 characters
1543 |          *     (only meaningful when encoding)
1544 |          * 
1545 | *

1546 | * Example: new Base64.OutputStream( out, Base64.ENCODE ) 1547 | * 1548 | * @param out the java.io.OutputStream to which data will be written. 1549 | * @param options Specified options. 1550 | * @see Base64#ENCODE 1551 | * @see Base64#DECODE 1552 | * @see Base64#DO_BREAK_LINES 1553 | * @since 1.3 1554 | */ 1555 | public OutputStream( java.io.OutputStream out, int options ) { 1556 | super( out ); 1557 | this.breakLines = (options & DO_BREAK_LINES) != 0; 1558 | this.encode = (options & ENCODE) != 0; 1559 | this.bufferLength = encode ? 3 : 4; 1560 | this.buffer = new byte[ bufferLength ]; 1561 | this.position = 0; 1562 | this.lineLength = 0; 1563 | this.suspendEncoding = false; 1564 | this.b4 = new byte[4]; 1565 | this.options = options; 1566 | this.decodabet = getDecodabet(options); 1567 | } // end constructor 1568 | 1569 | 1570 | /** 1571 | * Writes the byte to the output stream after 1572 | * converting to/from Base64 notation. 1573 | * When encoding, bytes are buffered three 1574 | * at a time before the output stream actually 1575 | * gets a write() call. 1576 | * When decoding, bytes are buffered four 1577 | * at a time. 1578 | * 1579 | * @param theByte the byte to write 1580 | * @since 1.3 1581 | */ 1582 | public void write(int theByte) 1583 | throws java.io.IOException { 1584 | // Encoding suspended? 1585 | if( suspendEncoding ) { 1586 | this.out.write( theByte ); 1587 | return; 1588 | } // end if: supsended 1589 | 1590 | // Encode? 1591 | if( encode ) { 1592 | buffer[ position++ ] = (byte)theByte; 1593 | if( position >= bufferLength ) { // Enough to encode. 1594 | 1595 | this.out.write( encode3to4( b4, buffer, bufferLength, options ) ); 1596 | 1597 | lineLength += 4; 1598 | if( breakLines && lineLength >= MAX_LINE_LENGTH ) { 1599 | this.out.write( NEW_LINE ); 1600 | lineLength = 0; 1601 | } // end if: end of line 1602 | 1603 | position = 0; 1604 | } // end if: enough to output 1605 | } // end if: encoding 1606 | 1607 | // Else, Decoding 1608 | else { 1609 | // Meaningful Base64 character? 1610 | if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) { 1611 | buffer[ position++ ] = (byte)theByte; 1612 | if( position >= bufferLength ) { // Enough to output. 1613 | 1614 | int len = Base64.decode4to3( buffer, 0, b4, 0, options ); 1615 | out.write( b4, 0, len ); 1616 | position = 0; 1617 | } // end if: enough to output 1618 | } // end if: meaningful base64 character 1619 | else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) { 1620 | throw new java.io.IOException( "Invalid character in Base64 data." ); 1621 | } // end else: not white space either 1622 | } // end else: decoding 1623 | } // end write 1624 | 1625 | 1626 | 1627 | /** 1628 | * Calls {@link #write(int)} repeatedly until len 1629 | * bytes are written. 1630 | * 1631 | * @param theBytes array from which to read bytes 1632 | * @param off offset for array 1633 | * @param len max number of bytes to read into array 1634 | * @since 1.3 1635 | */ 1636 | public void write( byte[] theBytes, int off, int len ) 1637 | throws java.io.IOException { 1638 | // Encoding suspended? 1639 | if( suspendEncoding ) { 1640 | this.out.write( theBytes, off, len ); 1641 | return; 1642 | } // end if: supsended 1643 | 1644 | for( int i = 0; i < len; i++ ) { 1645 | write( theBytes[ off + i ] ); 1646 | } // end for: each byte written 1647 | 1648 | } // end write 1649 | 1650 | 1651 | 1652 | /** 1653 | * Method added by PHIL. [Thanks, PHIL. -Rob] 1654 | * This pads the buffer without closing the stream. 1655 | * @throws java.io.IOException if there's an error. 1656 | */ 1657 | public void flushBase64() throws java.io.IOException { 1658 | if( position > 0 ) { 1659 | if( encode ) { 1660 | out.write( encode3to4( b4, buffer, position, options ) ); 1661 | position = 0; 1662 | } // end if: encoding 1663 | else { 1664 | throw new java.io.IOException( "Base64 input not properly padded." ); 1665 | } // end else: decoding 1666 | } // end if: buffer partially full 1667 | 1668 | } // end flush 1669 | 1670 | 1671 | /** 1672 | * Flushes and closes (I think, in the superclass) the stream. 1673 | * 1674 | * @since 1.3 1675 | */ 1676 | public void close() throws java.io.IOException { 1677 | // 1. Ensure that pending characters are written 1678 | flushBase64(); 1679 | 1680 | // 2. Actually close the stream 1681 | // Base class both flushes and closes. 1682 | super.close(); 1683 | 1684 | buffer = null; 1685 | out = null; 1686 | } // end close 1687 | 1688 | 1689 | 1690 | /** 1691 | * Suspends encoding of the stream. 1692 | * May be helpful if you need to embed a piece of 1693 | * base64-encoded data in a stream. 1694 | * 1695 | * @throws java.io.IOException if there's an error flushing 1696 | * @since 1.5.1 1697 | */ 1698 | public void suspendEncoding() throws java.io.IOException { 1699 | flushBase64(); 1700 | this.suspendEncoding = true; 1701 | } // end suspendEncoding 1702 | 1703 | 1704 | /** 1705 | * Resumes encoding of the stream. 1706 | * May be helpful if you need to embed a piece of 1707 | * base64-encoded data in a stream. 1708 | * 1709 | * @since 1.5.1 1710 | */ 1711 | public void resumeEncoding() { 1712 | this.suspendEncoding = false; 1713 | } // end resumeEncoding 1714 | 1715 | 1716 | 1717 | } // end inner class OutputStream 1718 | 1719 | 1720 | } // end class Base64 1721 | -------------------------------------------------------------------------------- /src/net/nczonline/web/datauri/DataURI.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | * http://www.nczonline.net/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | */ 23 | package net.nczonline.web.datauri; 24 | 25 | import jargs.gnu.CmdLineParser; 26 | import java.io.File; 27 | import java.io.FileOutputStream; 28 | import java.io.IOException; 29 | import java.io.OutputStreamWriter; 30 | import java.io.Writer; 31 | import java.net.URL; 32 | 33 | 34 | public class DataURI { 35 | 36 | 37 | /** 38 | * @param args the command line arguments 39 | */ 40 | public static void main(String[] args) { 41 | 42 | //default settings 43 | boolean verbose = false; 44 | String charset = null; 45 | String outputFilename = null; 46 | Writer out = null; 47 | String mimeType = null; 48 | 49 | //initialize command line parser 50 | CmdLineParser parser = new CmdLineParser(); 51 | CmdLineParser.Option verboseOpt = parser.addBooleanOption('v', "verbose"); 52 | CmdLineParser.Option mimeTypeOpt = parser.addStringOption('m', "mime"); 53 | CmdLineParser.Option helpOpt = parser.addBooleanOption('h', "help"); 54 | CmdLineParser.Option charsetOpt = parser.addStringOption("charset"); 55 | CmdLineParser.Option outputFilenameOpt = parser.addStringOption('o', "output"); 56 | 57 | try { 58 | 59 | //parse the arguments 60 | parser.parse(args); 61 | 62 | //figure out if the help option has been executed 63 | Boolean help = (Boolean) parser.getOptionValue(helpOpt); 64 | if (help != null && help.booleanValue()) { 65 | usage(); 66 | System.exit(0); 67 | } 68 | 69 | //determine boolean options 70 | verbose = parser.getOptionValue(verboseOpt) != null; 71 | 72 | //check for charset 73 | charset = (String) parser.getOptionValue(charsetOpt); 74 | 75 | //check for MIME type 76 | mimeType = (String) parser.getOptionValue(mimeTypeOpt); 77 | 78 | //get the file arguments 79 | String[] fileArgs = parser.getRemainingArgs(); 80 | 81 | //need to have at least one file 82 | if (fileArgs.length == 0){ 83 | System.err.println("[ERROR] No files specified."); 84 | System.exit(1); 85 | } 86 | 87 | //only the first filename is used 88 | String inputFilename = fileArgs[0]; 89 | 90 | //get output filename 91 | outputFilename = (String) parser.getOptionValue(outputFilenameOpt); 92 | 93 | if (outputFilename == null) { 94 | if (verbose){ 95 | System.err.println("[INFO] No output file specified, defaulting to stdout."); 96 | } 97 | 98 | out = new OutputStreamWriter(System.out); 99 | } else { 100 | if (verbose){ 101 | System.err.println("[INFO] Output file is '" + (new File(outputFilename)).getAbsolutePath() + "'"); 102 | } 103 | out = new OutputStreamWriter(new FileOutputStream(outputFilename), "UTF-8"); 104 | } 105 | 106 | //set verbose option 107 | DataURIGenerator.setVerbose(verbose); 108 | 109 | //determine if the filename is a local file or a URL 110 | if (inputFilename.startsWith("http://")){ 111 | DataURIGenerator.generate(new URL(inputFilename), out, mimeType); 112 | } else { 113 | DataURIGenerator.generate(new File(inputFilename), out, mimeType); 114 | } 115 | 116 | } catch (CmdLineParser.OptionException e) { 117 | usage(); 118 | System.exit(1); 119 | } catch (Exception e) { 120 | e.printStackTrace(); 121 | System.exit(1); 122 | } finally { 123 | if (out != null) { 124 | try { 125 | out.close(); 126 | } catch (IOException e) { 127 | e.printStackTrace(); 128 | } 129 | } 130 | } 131 | 132 | } 133 | 134 | /** 135 | * Outputs help information to the console. 136 | */ 137 | private static void usage() { 138 | System.out.println( 139 | "\nUsage: java -jar datauri-x.y.z.jar [options] [input file]\n\n" 140 | 141 | + "Global Options\n" 142 | + " -h, --help Displays this information.\n" 143 | + " --charset Character set of the input file.\n" 144 | + " -v, --verbose Display informational messages and warnings.\n" 145 | + " -m, --mime Mime type to encode into the data URI.\n" 146 | + " -o Place the output into . Defaults to stdout."); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/net/nczonline/web/datauri/DataURIGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2009 Nicholas C. Zakas. All rights reserved. 3 | * http://www.nczonline.net/ 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy 6 | * of this software and associated documentation files (the "Software"), to deal 7 | * in the Software without restriction, including without limitation the rights 8 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | * copies of the Software, and to permit persons to whom the Software is 10 | * furnished to do so, subject to the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be included in 13 | * all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | * THE SOFTWARE. 22 | */ 23 | 24 | package net.nczonline.web.datauri; 25 | 26 | import java.io.ByteArrayOutputStream; 27 | import java.io.File; 28 | import java.io.IOException; 29 | import java.io.Writer; 30 | import java.util.HashMap; 31 | import java.io.FileInputStream; 32 | import java.io.InputStream; 33 | import java.net.URL; 34 | import java.net.URLConnection; 35 | 36 | /** 37 | * Generator for Data URIs. 38 | * @author Nicholas C. Zakas 39 | */ 40 | public class DataURIGenerator { 41 | 42 | 43 | private static HashMap binaryTypes = new HashMap(); 44 | private static HashMap textTypes = new HashMap(); 45 | private static boolean verbose = false; 46 | 47 | //initialize file types and MIME types 48 | static { 49 | binaryTypes.put("gif", "image/gif"); 50 | binaryTypes.put("jpg", "image/jpeg"); 51 | binaryTypes.put("png", "image/png"); 52 | binaryTypes.put("jpeg", "image/jpeg"); 53 | 54 | textTypes.put("htm", "text/html"); 55 | textTypes.put("html", "text/html"); 56 | textTypes.put("xml", "application/xml"); 57 | textTypes.put("xhtml", "application/xhtml+xml"); 58 | textTypes.put("js", "application/x-javascript"); 59 | textTypes.put("css", "text/css"); 60 | textTypes.put("txt", "text/plain"); 61 | } 62 | 63 | //-------------------------------------------------------------------------- 64 | // Get/Set verbose flag 65 | //-------------------------------------------------------------------------- 66 | 67 | public static boolean getVerbose(){ 68 | return verbose; 69 | } 70 | 71 | public static void setVerbose(boolean newVerbose){ 72 | verbose = newVerbose; 73 | } 74 | 75 | //-------------------------------------------------------------------------- 76 | // Generate data URIs from a file 77 | //-------------------------------------------------------------------------- 78 | 79 | /** 80 | * Generates a data URI from a file, outputting it to the given writer. The 81 | * MIME type is determined from examining the filename. 82 | * @param file The file from which to generate the data URI. 83 | * @param out Where to output the data URI. 84 | * @throws java.io.IOException 85 | */ 86 | public static void generate(File file, Writer out) throws IOException { 87 | generate(file, out, null); 88 | } 89 | 90 | /** 91 | * Generates a data URI from a file, outputting it to the given writer. 92 | * @param file The file from which to generate the data URI. 93 | * @param out Where to output the data URI. 94 | * @param mimeType The MIME type to use for the data URI. 95 | * @throws java.io.IOException 96 | */ 97 | public static void generate(File file, Writer out, String mimeType) throws IOException { 98 | generateDataURI(file, out, mimeType); 99 | } 100 | 101 | //-------------------------------------------------------------------------- 102 | // Generate data URIs from a URL 103 | //-------------------------------------------------------------------------- 104 | 105 | /** 106 | * Generates a data URI from a file, outputting it to the given writer. The 107 | * MIME type is determined from examining the filename. 108 | * @param file The file from which to generate the data URI. 109 | * @param out Where to output the data URI. 110 | * @throws java.io.IOException 111 | */ 112 | public static void generate(URL url, Writer out) throws IOException { 113 | generate(url, out, null); 114 | } 115 | 116 | /** 117 | * Generates a data URI from a URL, outputting it to the given writer. 118 | * @param url The URL form which to generate the data URI. 119 | * @param out Where to output the data URI. 120 | * @param mimeType The MIME type to use for the data URI. 121 | * @throws java.io.IOException 122 | */ 123 | public static void generate(URL url, Writer out, String mimeType) throws IOException { 124 | generateDataURI(url, out, mimeType); 125 | } 126 | 127 | //-------------------------------------------------------------------------- 128 | // Helper methods 129 | //-------------------------------------------------------------------------- 130 | 131 | /** 132 | * Generates a data URI from the specified file and outputs to the given writer. 133 | * @param file The file to from which to create a data URI. 134 | * @param out Where to output the data URI. 135 | * @param mimeType The MIME type to specify in the data URI. 136 | * @param charset The character set to specify in the data URI. 137 | * @throws java.io.IOException 138 | */ 139 | private static void generateDataURI(File file, Writer out, String mimeType) throws IOException{ 140 | 141 | //read the bytes from the file 142 | InputStream in = new FileInputStream(file); 143 | byte[] bytes = new byte[(int) file.length()]; 144 | in.read(bytes); 145 | in.close(); 146 | 147 | //verify MIME type and charset 148 | mimeType = getMimeType(file.getName(), mimeType); 149 | 150 | //actually write 151 | generateDataURI(bytes, out, mimeType); 152 | } 153 | 154 | /** 155 | * Generates a data URI from the specified URL and outputs to the given writer. 156 | * @param url The URL to from which to create a data URI. 157 | * @param out Where to output the data URI. 158 | * @param mimeType The MIME type to specify in the data URI. 159 | * @throws java.io.IOException 160 | */ 161 | private static void generateDataURI(URL url, Writer out, String mimeType) throws IOException{ 162 | 163 | //get information about the URL 164 | URLConnection conn = url.openConnection(); 165 | 166 | //if no MIME type has been specified, get from the connection 167 | if (mimeType == null){ 168 | mimeType = getMimeType(url.getFile(), conn.getContentType()); 169 | if (verbose){ 170 | System.err.println("[INFO] No MIME type provided, using detected type of '" + mimeType + "'."); 171 | } 172 | } 173 | 174 | //sometimes charset is in the MIME type 175 | if (mimeType.indexOf("; charset=") > -1){ 176 | mimeType = mimeType.replace(" ", ""); //remove the space 177 | } else { 178 | mimeType = getMimeTypeWithCharset(mimeType); 179 | } 180 | 181 | //read the bytes from the URL 182 | InputStream in = conn.getInputStream(); 183 | ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); 184 | int c; 185 | 186 | while((c = in.read()) != -1){ 187 | byteStream.write(c); 188 | } 189 | 190 | byteStream.flush(); 191 | in.close(); 192 | 193 | //actually write 194 | generateDataURI(byteStream.toByteArray(), out, mimeType); 195 | } 196 | 197 | /** 198 | * Generates a data URI from a byte array and outputs to the given writer. 199 | * @param bytes The array of bytes to output to the data URI. 200 | * @param out Where to output the data URI. 201 | * @param mimeType The MIME type to specify in the data URI. 202 | * @param charset The character set to specify in the data URI. 203 | * @throws java.io.IOException 204 | */ 205 | private static void generateDataURI(byte[] bytes, Writer out, String mimeType) throws IOException { 206 | 207 | //create the output 208 | StringBuffer buffer = new StringBuffer(); 209 | buffer.append("data:"); 210 | 211 | //add MIME type 212 | buffer.append(mimeType); 213 | 214 | //output base64-encoding 215 | buffer.append(";base64,"); 216 | buffer.append(new String(Base64.encodeBytes(bytes))); 217 | 218 | //output to writer 219 | out.write(buffer.toString()); 220 | } 221 | 222 | /** 223 | * Determines if the given filename represents an image file. 224 | * @param filename The filename to check. 225 | * @return True if the filename represents an image, false if not. 226 | */ 227 | private static boolean isImageFile(String filename){ 228 | String fileType = getFileType(filename); 229 | return binaryTypes.containsKey(fileType) && binaryTypes.get(fileType).toString().startsWith("image"); 230 | } 231 | 232 | /** 233 | * Retrieves the extension for the filename. 234 | * @param filename The filename to get the extension from. 235 | * @return All characters after the final "." in the filename. 236 | */ 237 | private static String getFileType(String filename){ 238 | String type = ""; 239 | 240 | int idx = filename.lastIndexOf('.'); 241 | if (idx >= 0 && idx < filename.length() - 1) { 242 | type = filename.substring(idx + 1); 243 | } 244 | 245 | return type; 246 | } 247 | 248 | /** 249 | * Determines the MIME type to use for the given filename. If a MIME type 250 | * is passed in, then that is used by default. Otherwise, the filename 251 | * is inspected to determine the appropriate MIME type. 252 | * @param filename The filename to check. 253 | * @param mimeType The provided MIME type or null if nothing was provided. 254 | * @return The MIME type string to use for the filename. 255 | * @throws java.io.IOException When no MIME type can be determined. 256 | */ 257 | private static String getMimeType(String filename, String mimeType) throws IOException { 258 | if (mimeType == null){ 259 | 260 | String type = getFileType(filename); 261 | 262 | //if it's an image type, don't use a charset 263 | if (binaryTypes.containsKey(type)){ 264 | mimeType = (String) binaryTypes.get(type); 265 | } else if (textTypes.containsKey(type)){ 266 | mimeType = (String) textTypes.get(type) + ";charset=UTF-8"; 267 | } else { 268 | throw new IOException("No MIME type provided and MIME type couldn't be automatically determined."); 269 | } 270 | 271 | if (verbose){ 272 | System.err.println("[INFO] No MIME type provided, defaulting to '" + mimeType + "'."); 273 | } 274 | } 275 | 276 | return mimeType; 277 | } 278 | 279 | private static String getMimeTypeWithCharset(String mimeType){ 280 | 281 | if (binaryTypes.containsValue(mimeType)){ 282 | if (verbose){ 283 | System.err.println("[INFO] Image file detected, skipping charset."); 284 | } 285 | return mimeType; 286 | } else { 287 | if (verbose){ 288 | System.err.println("[INFO] Using charset 'UTF-8'."); 289 | } 290 | return mimeType + ";charset=UTF-8"; 291 | } 292 | 293 | } 294 | 295 | } 296 | -------------------------------------------------------------------------------- /tests/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nzakas/cssembed/00b720f01b93019c5d036553043114dd74af8001/tests/folder.png -------------------------------------------------------------------------------- /tests/net/nczonline/web/cssembed/CSSURLEmbedderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this template, choose Tools | Templates 3 | * and open the template in the editor. 4 | */ 5 | 6 | package net.nczonline.web.cssembed; 7 | 8 | import java.io.File; 9 | import java.io.FileInputStream; 10 | import java.io.FileOutputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.io.OutputStreamWriter; 14 | import java.io.Reader; 15 | import java.io.StringReader; 16 | import java.io.StringWriter; 17 | import java.io.Writer; 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | import static org.junit.Assert.*; 22 | 23 | /** 24 | * 25 | * @author Nicholas C. Zakas 26 | */ 27 | public class CSSURLEmbedderTest { 28 | 29 | private static String folderDataURI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAbCAMAAAAu7K2VAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAwUExURWxsbNbW1v/rhf/ge//3kf/Ub9/f3/b29oeHh/7LZv/0juazTktLS8WSLf//mf/////BPrAAAAB4SURBVHja3NLdCoAgDIbhqbXZz2f3f7eZWUpMO67nQEReBqK0vaLPJohYegnSYqSdYAtRGvUYVpJhPpx7z2piLSqsJQ73oY1ztGREuEwBpCUTwpAt7cRmncRlnWTMoCdcXxmrdiMxngpvtDcSNkX9AvTnv9uyCzAAgzAw+dNAwOQAAAAASUVORK5CYII="; 30 | private CSSURLEmbedder embedder; 31 | 32 | public CSSURLEmbedderTest() { 33 | } 34 | 35 | @Before 36 | public void setUp() { 37 | 38 | } 39 | 40 | @After 41 | public void tearDown() { 42 | embedder = null; 43 | } 44 | 45 | @Test 46 | public void testAbsoluteLocalFile() throws IOException { 47 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 48 | String code = "background: url(folder.png);"; 49 | 50 | StringWriter writer = new StringWriter(); 51 | embedder = new CSSURLEmbedder(new StringReader(code), true); 52 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 53 | 54 | String result = writer.toString(); 55 | assertEquals("background: url(" + folderDataURI + ");", result); 56 | } 57 | 58 | @Test 59 | public void testAbsoluteLocalFileWithMhtml() throws IOException { 60 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 61 | String code = "background: url(folder.png);"; 62 | String mhtmlUrl = "http://www.example.com/dir/"; 63 | 64 | StringWriter writer = new StringWriter(); 65 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.MHTML_OPTION, true); 66 | embedder.setMHTMLRoot(mhtmlUrl); 67 | embedder.setFilename("styles_ie.css"); 68 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 69 | 70 | String result = writer.toString(); 71 | assertEquals("/*\nContent-Type: multipart/related; boundary=\"" + CSSURLEmbedder.MHTML_SEPARATOR + 72 | "\"\n\n--" + CSSURLEmbedder.MHTML_SEPARATOR + "\nContent-Location:folder.png\n" + 73 | "Content-Transfer-Encoding:base64\n\n" + folderDataURI.substring(folderDataURI.indexOf(",")+1) + 74 | "\n\n--" + CSSURLEmbedder.MHTML_SEPARATOR + "--\n" + 75 | "*/\nbackground: url(mhtml:" + mhtmlUrl + "styles_ie.css!folder.png);", result); 76 | } 77 | 78 | @Test 79 | public void testAbsoluteLocalFileMultipleOneLine() throws IOException { 80 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 81 | String code = "background: url(folder.png); background: url(folder.png);"; 82 | 83 | StringWriter writer = new StringWriter(); 84 | embedder = new CSSURLEmbedder(new StringReader(code), true); 85 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 86 | 87 | String result = writer.toString(); 88 | assertEquals("background: url(" + folderDataURI + "); background: url(" + folderDataURI + ");", result); 89 | } 90 | 91 | @Test 92 | public void testAbsoluteLocalFileWithDoubleQuotes() throws IOException { 93 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 94 | String code = "background: url(\"folder.png\");"; 95 | 96 | StringWriter writer = new StringWriter(); 97 | embedder = new CSSURLEmbedder(new StringReader(code), true); 98 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 99 | 100 | String result = writer.toString(); 101 | assertEquals("background: url(" + folderDataURI + ");", result); 102 | } 103 | 104 | @Test 105 | public void testAbsoluteLocalFileWithSingleQuotes() throws IOException { 106 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 107 | String code = "background: url('folder.png');"; 108 | 109 | StringWriter writer = new StringWriter(); 110 | embedder = new CSSURLEmbedder(new StringReader(code), true); 111 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 112 | 113 | String result = writer.toString(); 114 | assertEquals("background: url(" + folderDataURI + ");", result); 115 | } 116 | 117 | @Test (expected=IOException.class) 118 | public void testAbsoluteLocalFileWithMissingFile() throws IOException { 119 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 120 | String code = "background: url(fooga.png);"; 121 | 122 | StringWriter writer = new StringWriter(); 123 | embedder = new CSSURLEmbedder(new StringReader(code),true); 124 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 125 | 126 | String result = writer.toString(); 127 | assertEquals(code, result); 128 | } 129 | 130 | @Test 131 | public void testAbsoluteLocalFileWithMissingFilesEnabled() throws IOException { 132 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 133 | String code = "background: url(fooga.png);"; 134 | 135 | StringWriter writer = new StringWriter(); 136 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.SKIP_MISSING_OPTION, true); 137 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 138 | 139 | String result = writer.toString(); 140 | assertEquals(code, result); 141 | } 142 | 143 | 144 | 145 | @Test 146 | public void testAbsoluteLocalFileUnderMaxLength() throws IOException { 147 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 148 | String code = "background: url(folder.png);"; 149 | 150 | StringWriter writer = new StringWriter(); 151 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.DATAURI_OPTION, true, 1000); 152 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 153 | 154 | String result = writer.toString(); 155 | assertEquals("background: url(" + folderDataURI + ");", result); 156 | } 157 | 158 | @Test 159 | public void testAbsoluteLocalFileOverMaxLength() throws IOException { 160 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 161 | String code = "background: url(folder.png);"; 162 | 163 | StringWriter writer = new StringWriter(); 164 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.DATAURI_OPTION, true, 200); 165 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 166 | 167 | String result = writer.toString(); 168 | assertEquals(code, result); 169 | } 170 | 171 | @Test 172 | public void testAbsoluteLocalFileUnderMaxImageSize() throws IOException { 173 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 174 | String code = "background: url(folder.png);"; 175 | 176 | StringWriter writer = new StringWriter(); 177 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.DATAURI_OPTION, true, 0, 300); 178 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 179 | 180 | String result = writer.toString(); 181 | assertEquals("background: url(" + folderDataURI + ");", result); 182 | } 183 | 184 | @Test 185 | public void testAbsoluteLocalFileOverMaxImageSize() throws IOException { 186 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 187 | String code = "background: url(folder.png);"; 188 | 189 | StringWriter writer = new StringWriter(); 190 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.DATAURI_OPTION, true, 0, 200); 191 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 192 | 193 | String result = writer.toString(); 194 | assertEquals(code, result); 195 | } 196 | 197 | @Test 198 | public void testReadFromAndWriteToSameFile() throws IOException { 199 | String filename = CSSURLEmbedderTest.class.getResource("samefiletest.css").getPath().replace("%20", " "); 200 | File file = new File(filename); 201 | Reader in = new InputStreamReader(new FileInputStream(file)); 202 | 203 | embedder = new CSSURLEmbedder(in, true); 204 | in.close(); 205 | 206 | Writer writer = new OutputStreamWriter(new FileOutputStream(file)); 207 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 208 | writer.close(); 209 | 210 | in = new InputStreamReader(new FileInputStream(file)); 211 | char[] chars = new char[(int)file.length()]; 212 | in.read(chars, 0, (int)file.length()); 213 | in.close(); 214 | 215 | String result = new String(chars); 216 | assertEquals("background: url(" + folderDataURI + ");", result); 217 | } 218 | 219 | @Test 220 | public void testRegularUrlWithMhtml() throws IOException { 221 | String filename = CSSURLEmbedderTest.class.getResource("folder.png").getPath().replace("%20", " "); 222 | String code = "background: url(folder.txt);"; 223 | String mhtmlUrl = "http://www.example.com/dir/"; 224 | 225 | StringWriter writer = new StringWriter(); 226 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.MHTML_OPTION, true); 227 | embedder.setMHTMLRoot(mhtmlUrl); 228 | embedder.setFilename("styles_ie.css"); 229 | embedder.embedImages(writer, filename.substring(0, filename.lastIndexOf("/")+1)); 230 | 231 | 232 | String result = writer.toString(); 233 | assertEquals("background: url(folder.txt);", result); 234 | } 235 | 236 | @Test 237 | public void testRemoteUrlWithQueryString() throws IOException { 238 | final String expectedUrl = "http://some-http-server.com/image/with/query/parameters/image.png?a=b&c=d"; 239 | String code = "background : url(/image/with/query/parameters/image.png?a=b&c=d)"; 240 | 241 | StringWriter writer = new StringWriter(); 242 | embedder = new CSSURLEmbedder(new StringReader(code), CSSURLEmbedder.DATAURI_OPTION, true, 0, 200) { 243 | /* 244 | * Override method to prevent a network call during unit tests 245 | */ 246 | @Override 247 | String getImageURIString(String url, String originalUrl) throws IOException { 248 | if(url.equals("")) { 249 | throw new IllegalArgumentException("Expected URL " + expectedUrl + ", but found " + url); 250 | } 251 | return "data:image/gif;base64,AAAABBBBCCCCDDDD"; 252 | } 253 | }; 254 | embedder.embedImages(writer, "http://some-http-server.com/"); 255 | 256 | String result = writer.toString(); 257 | assertEquals("background : url(data:image/gif;base64,AAAABBBBCCCCDDDD)", result); 258 | } 259 | 260 | @Test 261 | public void testImageDetection() { 262 | String tests[] = { 263 | "file://path/to/image.png", 264 | "http://some.server.com/image.png", 265 | "http://some.server.com/image.png?param=legalvalue&anotherparam=anothervalue", 266 | "http://some.server.com/image.png?param=illegal.value.with.period" 267 | }; 268 | boolean expectedImage[] = { 269 | true, true, true, false 270 | }; 271 | 272 | for(int i=0; i