001package ezvcard.util; 002 003import java.io.UnsupportedEncodingException; 004import java.nio.charset.Charset; 005import java.util.Arrays; 006import java.util.Objects; 007 008import ezvcard.Messages; 009import ezvcard.util.org.apache.commons.codec.binary.Base64; 010 011/* 012 Copyright (c) 2012-2026, Michael Angstadt 013 All rights reserved. 014 015 Redistribution and use in source and binary forms, with or without 016 modification, are permitted provided that the following conditions are met: 017 018 1. Redistributions of source code must retain the above copyright notice, this 019 list of conditions and the following disclaimer. 020 2. Redistributions in binary form must reproduce the above copyright notice, 021 this list of conditions and the following disclaimer in the documentation 022 and/or other materials provided with the distribution. 023 024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 027 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 028 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 029 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 030 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 031 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 032 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 033 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 034 035 The views and conclusions contained in the software and documentation are those 036 of the authors and should not be interpreted as representing official policies, 037 either expressed or implied, of the FreeBSD Project. 038 */ 039 040/** 041 * <p> 042 * Represents a data URI. 043 * </p> 044 * <p> 045 * Example: {@code data:image/jpeg;base64,[base64 string]} 046 * </p> 047 * @author Michael Angstadt 048 */ 049public final class DataUri { 050 private static final String SCHEME = "data:"; 051 052 private final byte[] data; 053 private final String text; 054 private final String contentType; 055 056 /** 057 * Creates a data URI. 058 * @param contentType the content type of the data (e.g. "image/png") 059 * @param data the data 060 */ 061 public DataUri(String contentType, byte[] data) { 062 this(contentType, data, null); 063 } 064 065 /** 066 * Creates a data URI. 067 * @param contentType the content type of the text (e.g. "text/html") 068 * @param text the text 069 */ 070 public DataUri(String contentType, String text) { 071 this(contentType, null, text); 072 } 073 074 /** 075 * Creates a data URI with a content type of "text/plain". 076 * @param text the text 077 */ 078 public DataUri(String text) { 079 this("text/plain", text); 080 } 081 082 /** 083 * Copies a data URI. 084 * @param original the data URI to copy 085 */ 086 public DataUri(DataUri original) { 087 this(original.contentType, (original.data == null) ? null : original.data.clone(), original.text); 088 } 089 090 private DataUri(String contentType, byte[] data, String text) { 091 this.contentType = (contentType == null) ? "" : contentType.toLowerCase(); 092 this.data = data; 093 this.text = text; 094 } 095 096 /** 097 * Parses a data URI string. 098 * @param uri the URI string (e.g. "data:image/jpeg;base64,[base64 string]") 099 * @return the parsed data URI 100 * @throws IllegalArgumentException if the string is not a valid data URI or 101 * it cannot be parsed 102 */ 103 public static DataUri parse(String uri) { 104 return new Parser(uri).parse(); 105 } 106 107 private static class Parser { 108 private final String uri; 109 private final CharIterator it; 110 111 private String contentType; 112 private String charset; 113 private boolean base64; 114 private String dataStr; 115 private int tokenStart; 116 117 public Parser(String uri) { 118 this.uri = uri; 119 checkScheme(); 120 121 tokenStart = SCHEME.length(); 122 it = new CharIterator(uri, tokenStart); 123 } 124 125 private void checkScheme() { 126 if (uri.length() < SCHEME.length() || !uri.substring(0, SCHEME.length()).equalsIgnoreCase(SCHEME)) { 127 //not a data URI 128 throw Messages.INSTANCE.getIllegalArgumentException(18, SCHEME); 129 } 130 } 131 132 public DataUri parse() { 133 //Syntax: data:[<media type>][;charset=<character set>][;base64],<data> 134 while (it.hasNext()) { 135 char c = it.next(); 136 137 if (c == ',') { 138 handleComma(); 139 break; 140 } 141 142 if (c == ';') { 143 handleSemicolon(); 144 } 145 } 146 147 if (dataStr == null) { 148 throw Messages.INSTANCE.getIllegalArgumentException(20); 149 } 150 151 return build(); 152 } 153 154 private void handleComma() { 155 String token = uri.substring(tokenStart, it.index()); 156 if (contentType == null) { 157 contentType = token.toLowerCase(); 158 } else { 159 if (token.toLowerCase().startsWith("charset=")) { 160 int equals = token.indexOf('='); 161 charset = token.substring(equals + 1); 162 } else if ("base64".equalsIgnoreCase(token)) { 163 base64 = true; 164 } 165 } 166 167 dataStr = uri.substring(it.index() + 1); 168 } 169 170 private void handleSemicolon() { 171 String token = uri.substring(tokenStart, it.index()); 172 if (contentType == null) { 173 contentType = token.toLowerCase(); 174 } else { 175 if (token.toLowerCase().startsWith("charset=")) { 176 int equals = token.indexOf('='); 177 charset = token.substring(equals + 1); 178 } else if ("base64".equalsIgnoreCase(token)) { 179 base64 = true; 180 } 181 } 182 183 tokenStart = it.index() + 1; 184 } 185 186 private DataUri build() { 187 String text = null; 188 byte[] data = null; 189 190 if (base64) { 191 dataStr = dataStr.replaceAll("\\s", ""); 192 data = Base64.decodeBase64(dataStr); 193 if (charset != null) { 194 try { 195 text = new String(data, charset); 196 } catch (UnsupportedEncodingException e) { 197 throw new IllegalArgumentException(Messages.INSTANCE.getExceptionMessage(43, charset), e); 198 } 199 data = null; 200 } 201 } else { 202 text = dataStr; 203 } 204 205 return new DataUri(contentType, data, text); 206 } 207 } 208 209 /** 210 * Gets the binary data. 211 * @return the binary data or null if the value was text 212 */ 213 public byte[] getData() { 214 return data; 215 } 216 217 /** 218 * Gets the content type. 219 * @return the content type (e.g. "image/png") 220 */ 221 public String getContentType() { 222 return contentType; 223 } 224 225 /** 226 * Gets the text value. 227 * @return the text value or null if the value was binary 228 */ 229 public String getText() { 230 return text; 231 } 232 233 /** 234 * Creates a data URI string. 235 * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]") 236 */ 237 @Override 238 public String toString() { 239 return toString(null); 240 } 241 242 /** 243 * Creates a data URI string. 244 * @param charset only applicable if the data URI's value is text. Defines 245 * what character set to use when encoding text value in base64. Setting 246 * this to "null" will insert the text in the data URI as-is without 247 * base64-encoding it. 248 * @return the data URI (e.g. "data:image/jpeg;base64,[base64 string]") 249 */ 250 public String toString(Charset charset) { 251 StringBuilder sb = new StringBuilder(); 252 sb.append(SCHEME); 253 sb.append(contentType); 254 255 if (data != null) { 256 sb.append(";base64,"); 257 sb.append(Base64.encodeBase64String(data)); 258 } else if (text != null) { 259 if (charset == null) { 260 sb.append(',').append(text); 261 } else { 262 byte[] textBytes = text.getBytes(charset); 263 sb.append(";charset=").append(charset.name()); 264 sb.append(";base64,"); 265 sb.append(Base64.encodeBase64String(textBytes)); 266 } 267 } else { 268 sb.append(','); 269 } 270 271 return sb.toString(); 272 } 273 274 @Override 275 public int hashCode() { 276 final int prime = 31; 277 int result = 1; 278 result = prime * result + Arrays.hashCode(data); 279 result = prime * result + Objects.hash(contentType, text); 280 return result; 281 } 282 283 @Override 284 public boolean equals(Object obj) { 285 if (this == obj) return true; 286 if (obj == null) return false; 287 if (getClass() != obj.getClass()) return false; 288 DataUri other = (DataUri) obj; 289 return Objects.equals(contentType, other.contentType) && Arrays.equals(data, other.data) && Objects.equals(text, other.text); 290 } 291}