30 ? '...' : ''}" -> ${svg.length} chars`);
+ } catch (error) {
+ console.error(`✗ Case ${i}: "${input}" failed:`, error.message);
+ results.push({
+ id: i,
+ input: input,
+ sansEnv: sansEnv,
+ version: version,
+ error: error.message
+ });
+ }
+}
+
+// Write results to JSON file
+const outputFile = 'test-vectors.json';
+fs.writeFileSync(outputFile, JSON.stringify(results, null, 2));
+
+console.log(`\n✓ Test vectors written to ${outputFile}`);
+console.log(` Total: ${results.length} test cases`);
+console.log(` Success: ${results.filter(r => !r.error).length}`);
+console.log(` Failed: ${results.filter(r => r.error).length}`);
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..6842dba
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,86 @@
+
+
+ 4.0.0
+
+ com.multiavatar
+ multiavatar
+ 1.0.7
+ jar
+
+ Multiavatar
+ Multicultural Avatar Generator
+ https://multiavatar.com
+
+
+
+ Custom License
+ https://github.com/multiavatar/Multiavatar/blob/main/LICENSE
+
+
+
+
+
+ Gie Katon
+ git@giekaton.com
+ http://giekaton.com/
+
+
+
+
+ scm:git:https://github.com/multiavatar/Multiavatar.git
+ scm:git:https://github.com/multiavatar/Multiavatar.git
+ https://github.com/multiavatar/Multiavatar
+
+
+
+ 8
+ 8
+ UTF-8
+
+
+
+
+ junit
+ junit
+ 4.13.2
+ test
+
+
+
+
+
+
+ ${basedir}
+
+ LICENSE
+
+ META-INF
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 8
+ 8
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ true
+
+
+
+
+
+
+
diff --git a/src/main/java/com/multiavatar/Avatar.java b/src/main/java/com/multiavatar/Avatar.java
new file mode 100644
index 0000000..0c8ed4e
--- /dev/null
+++ b/src/main/java/com/multiavatar/Avatar.java
@@ -0,0 +1,175 @@
+package com.multiavatar;
+
+import java.security.MessageDigest;
+import java.util.Random;
+
+/**
+ * The avatar configuration
+ */
+public class Avatar {
+
+ private static final String SVG_START = "";
+ private static final String SVG_END = " ";
+ private static final String STROKE = "stroke-linecap:round;stroke-linejoin:round;stroke-width:";
+
+ Coordinate env;
+ Coordinate clo;
+ Coordinate head;
+ Coordinate mouth;
+ Coordinate eyes;
+ Coordinate top;
+
+ Coordinate getValue(AvatarPart part) {
+ switch (part) {
+ case ENV: return env;
+ case CLO: return clo;
+ case HEAD: return head;
+ case MOUTH: return mouth;
+ case EYES: return eyes;
+ case TOP: return top;
+ default: return null;
+ }
+ }
+
+ /**
+ * Creates a pure avatar where all parts use the same character and theme.
+ *
+ * @param character The {@link CharacterType} to use for all parts
+ * @param theme The {@link Theme} to use for all parts
+ * @return A new {@link Avatar} with all parts using the same character and theme
+ */
+ public static Avatar pure(CharacterType character, Theme theme) {
+ Avatar avatar = new Avatar();
+ Coordinate coordinate = Coordinate.coordinate(character, theme);
+ avatar.env = coordinate;
+ avatar.clo = coordinate;
+ avatar.head = coordinate;
+ avatar.mouth = coordinate;
+ avatar.eyes = coordinate;
+ avatar.top = coordinate;
+ return avatar;
+ }
+
+ /**
+ * Creates an avatar from an identifier string using SHA-256 hashing.
+ *
+ * @param id The identifier to generate the avatar from (e.g., username, email)
+ * @return A new {@link Avatar} generated deterministically from the id
+ */
+ public static Avatar fromId(String id) {
+ // Get SHA-256 hash
+ String hash = Avatar.sha256(id);
+
+ // Remove all non-digits from hash (JavaScript compatibility)
+ String hashDigitsOnly = hash.replaceAll("\\D", "");
+
+ // Extract first 12 digits
+ String hashString = hashDigitsOnly.substring(0, Math.min(12, hashDigitsOnly.length()));
+
+ // Convert hash string to parts (6 parts, 2 digits each)
+ return Avatar.fromHash(hashString);
+ }
+
+ /**
+ * Calculates SHA-256 hash of a string
+ */
+ private static String sha256(String input) {
+ try {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(input.getBytes("UTF-8"));
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ return hexString.toString();
+ } catch (Exception e) {
+ throw new RuntimeException("Error calculating SHA-256", e);
+ }
+ }
+
+ private static Avatar fromHash(String hashString) {
+ Avatar avatar = new Avatar();
+ // Get parts (range 0-47) and convert to {@link Coordinate} (0-15 + theme)
+ avatar.env = Avatar.fromPartNumber(getPartNumber(hashString.substring(0, 2)));
+ avatar.clo = Avatar.fromPartNumber(getPartNumber(hashString.substring(2, 4)));
+ avatar.head = Avatar.fromPartNumber(getPartNumber(hashString.substring(4, 6)));
+ avatar.mouth = Avatar.fromPartNumber(getPartNumber(hashString.substring(6, 8)));
+ avatar.eyes = Avatar.fromPartNumber(getPartNumber(hashString.substring(8, 10)));
+ avatar.top = Avatar.fromPartNumber(getPartNumber(hashString.substring(10, 12)));
+ return avatar;
+ }
+
+ /**
+ * Creates a random avatar using the provided {@link Random} instance.
+ *
+ * @param rnd The {@link Random} instance to use for generating random parts
+ * @return A new {@link Avatar} with randomly selected parts
+ */
+ public static Avatar fromRandom(Random rnd) {
+ Avatar avatar = new Avatar();
+ // Generate random part numbers (0-47) for each avatar part
+ avatar.env = Avatar.fromPartNumber(rnd.nextInt(48));
+ avatar.clo = Avatar.fromPartNumber(rnd.nextInt(48));
+ avatar.head = Avatar.fromPartNumber(rnd.nextInt(48));
+ avatar.mouth = Avatar.fromPartNumber(rnd.nextInt(48));
+ avatar.eyes = Avatar.fromPartNumber(rnd.nextInt(48));
+ avatar.top = Avatar.fromPartNumber(rnd.nextInt(48));
+ return avatar;
+ }
+
+ /**
+ * Creates a {@link Coordinate} from a part number (0-47)
+ */
+ private static Coordinate fromPartNumber(int nr) {
+ Theme theme;
+
+ if (nr > 31) {
+ nr = nr - 32;
+ theme = Theme.C;
+ } else if (nr > 15) {
+ nr = nr - 16;
+ theme = Theme.B;
+ } else {
+ theme = Theme.A;
+ }
+
+ CharacterType character = CharacterType.fromIndex(nr);
+ return Coordinate.coordinate(character, theme);
+ }
+
+ /**
+ * Converts a 2-digit decimal string (0-99) to a part number (0-47)
+ */
+ private static int getPartNumber(String digitPair) {
+ int value = Integer.parseInt(digitPair);
+ return Math.round((47f / 100f) * value);
+ }
+
+ /**
+ * Renders this avatar to SVG format
+ *
+ * @param sansEnv If true, renders without the circular background
+ * @return The complete SVG code as a string
+ */
+ public String render(boolean sansEnv) {
+ StringBuilder result = new StringBuilder(SVG_START);
+
+ // Add generator attribution (fulfills license requirement)
+ result.append("Multiavatar https://multiavatar.com ");
+
+ for (AvatarPart part : AvatarPart.values()) {
+ if (part == AvatarPart.ENV && sansEnv) {
+ continue; // Skip environment if sansEnv is true
+ }
+
+ Coordinate coordinate = getValue(part);
+ coordinate.renderPart(result, part);
+ }
+
+ result.append(SVG_END);
+ return result.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/multiavatar/AvatarPart.java b/src/main/java/com/multiavatar/AvatarPart.java
new file mode 100644
index 0000000..b6a0f3d
--- /dev/null
+++ b/src/main/java/com/multiavatar/AvatarPart.java
@@ -0,0 +1,25 @@
+package com.multiavatar;
+
+/**
+ * Avatar part names in the order they should be rendered.
+ * Each part represents a layer of the avatar SVG.
+ */
+public enum AvatarPart {
+ /** Environment - the circular background layer */
+ ENV,
+
+ /** Head - the face/head shape layer */
+ HEAD,
+
+ /** Clothes - the clothing/body layer */
+ CLO,
+
+ /** Top - the hair/headwear layer */
+ TOP,
+
+ /** Eyes - the eyes layer */
+ EYES,
+
+ /** Mouth - the mouth/expression layer */
+ MOUTH;
+}
\ No newline at end of file
diff --git a/src/main/java/com/multiavatar/CharacterType.java b/src/main/java/com/multiavatar/CharacterType.java
new file mode 100644
index 0000000..7416a43
--- /dev/null
+++ b/src/main/java/com/multiavatar/CharacterType.java
@@ -0,0 +1,61 @@
+package com.multiavatar;
+
+/**
+ * Avatar character types (16 base characters, 00-15)
+ */
+public enum CharacterType {
+ ROBO("00", "Robo"),
+ GIRL("01", "Girl"),
+ BLONDE("02", "Blonde"),
+ GUY("03", "Guy"),
+ COUNTRY("04", "Country"),
+ GEEKNOT("05", "Geeknot"),
+ ASIAN("06", "Asian"),
+ PUNK("07", "Punk"),
+ AFROHAIR("08", "Afrohair"),
+ NORMIE_FEMALE("09", "Normie Female"),
+ OLDER("10", "Older"),
+ FIREHAIR("11", "Firehair"),
+ BLOND("12", "Blond"),
+ ATEAM("13", "Ateam"),
+ RASTA("14", "Rasta"),
+ STREET("15", "Street");
+
+ private final String id;
+ private final String displayName;
+
+ CharacterType(String id, String displayName) {
+ this.id = id;
+ this.displayName = displayName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ /**
+ * Get character by ID string (e.g., "00", "15")
+ */
+ public static CharacterType fromId(String id) {
+ for (CharacterType character : values()) {
+ if (character.id.equals(id)) {
+ return character;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get character by numeric index (0-15)
+ */
+ public static CharacterType fromIndex(int index) {
+ if (index < 0 || index >= values().length) {
+ return null;
+ }
+ return values()[index];
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/multiavatar/Coordinate.java b/src/main/java/com/multiavatar/Coordinate.java
new file mode 100644
index 0000000..a2052f9
--- /dev/null
+++ b/src/main/java/com/multiavatar/Coordinate.java
@@ -0,0 +1,50 @@
+package com.multiavatar;
+
+import com.multiavatar.SvgData.Template;
+
+/**
+ * Value class holding an avatar {@link CharacterType} and {@link Theme} together
+ */
+public class Coordinate {
+ public static Coordinate coordinate(CharacterType character, Theme theme) {
+ return new Coordinate(character, theme);
+ }
+
+ final CharacterType character;
+ final Theme theme;
+
+ private Coordinate(CharacterType character, Theme theme) {
+ this.character = character;
+ this.theme = theme;
+ }
+
+ /**
+ * Produces the final SVG string for a part with colors applied from the {@link Theme}
+ *
+ * @param result The {@link StringBuilder} to append the SVG content to
+ * @param part The {@link AvatarPart} to render
+ */
+ public void renderPart(StringBuilder result, AvatarPart part) {
+ if (character == null) {
+ return;
+ }
+
+ // Get theme colors
+ ThemeData.CharacterThemes characterThemes = ThemeData.getCharacterThemes(character);
+ if (characterThemes == null) {
+ return;
+ }
+
+ ThemeData.Colors themeData = characterThemes.getTheme(theme);
+ if (themeData == null) {
+ return;
+ }
+
+ String[] colors = themeData.getColors(part);
+
+ // Get SVG template
+ Template svgTemplate = SvgData.getSvgTemplate(character, part);
+
+ svgTemplate.render(result, colors);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/multiavatar/Multiavatar.java b/src/main/java/com/multiavatar/Multiavatar.java
new file mode 100644
index 0000000..8fd3ee6
--- /dev/null
+++ b/src/main/java/com/multiavatar/Multiavatar.java
@@ -0,0 +1,135 @@
+package com.multiavatar;
+
+import java.util.Random;
+
+/**
+ * Multiavatar - Multicultural Avatar Generator
+ *
+ * Generates unique SVG avatars from text strings, predefined characters, or random generation.
+ * Can generate 12,230,590,464 unique avatars (48^6).
+ *
+ * Usage:
+ *
+ * // Generate from identifier (deterministic)
+ * String svgCode = Multiavatar.generate("Binx Bond");
+ * String svgWithoutBackground = Multiavatar.generate("Binx Bond", true);
+ *
+ * // Generate predefined avatar
+ * String predefined = Multiavatar.generate(CharacterType.GIRL, Theme.A);
+ *
+ * // Generate random avatar with seed (reproducible)
+ * String random = Multiavatar.generate(12345L);
+ *
+ * // Generate random avatar with Random instance
+ * Random rnd = new Random();
+ * String random2 = Multiavatar.generate(rnd);
+ *
+ *
+ * @author Gie Katon
+ * @version 1.0.7
+ */
+public class Multiavatar {
+
+ /**
+ * Generates an avatar SVG from the given identifier.
+ *
+ * @param id The identifier to generate the avatar from (e.g., username, email)
+ * @return The complete SVG code as a string
+ */
+ public static String generate(String id) {
+ return generate(id, false);
+ }
+
+ /**
+ * Generates an avatar SVG from the given identifier.
+ *
+ * @param id The identifier to generate the avatar from (e.g., username, email)
+ * @param sansEnv If true, returns the avatar without the circular background
+ * @return The complete SVG code as a string
+ */
+ public static String generate(String id, boolean sansEnv) {
+ if (id == null) {
+ id = "";
+ }
+
+ // Return empty string for empty input (JavaScript compatibility)
+ if (id.length() == 0) {
+ return "";
+ }
+
+ Avatar avatar = Avatar.fromId(id);
+ return avatar.render(sansEnv);
+ }
+
+ /**
+ * Generates a predefined avatar SVG with specific character and theme.
+ *
+ * @param character The {@link CharacterType} to use (e.g., GIRL, ROBO)
+ * @param theme The {@link Theme} to use (A, B, or C)
+ * @return The complete SVG code as a string
+ */
+ public static String generate(CharacterType character, Theme theme) {
+ return generate(character, theme, false);
+ }
+
+ /**
+ * Generates a predefined avatar SVG with specific character and theme.
+ *
+ * @param character The {@link CharacterType} to use (e.g., GIRL, ROBO)
+ * @param theme The {@link Theme} to use (A, B, or C)
+ * @param sansEnv If true, returns the avatar without the circular background
+ * @return The complete SVG code as a string
+ */
+ public static String generate(CharacterType character, Theme theme, boolean sansEnv) {
+ Avatar avatar = Avatar.pure(character, theme);
+ return avatar.render(sansEnv);
+ }
+
+ /**
+ * Generates a random avatar SVG using the provided {@link Random} instance.
+ *
+ * @param rnd The {@link Random} instance to use for generating random parts
+ * @return The complete SVG code as a string
+ */
+ public static String generate(Random rnd) {
+ return generate(rnd, false);
+ }
+
+ /**
+ * Generates a random avatar SVG using the provided {@link Random} instance.
+ *
+ * @param rnd The {@link Random} instance to use for generating random parts
+ * @param sansEnv If true, returns the avatar without the circular background
+ * @return The complete SVG code as a string
+ */
+ public static String generate(Random rnd, boolean sansEnv) {
+ Avatar avatar = Avatar.fromRandom(rnd);
+ return avatar.render(sansEnv);
+ }
+
+ /**
+ * Generates a random avatar SVG using a seed value.
+ * Creates a {@link Random} instance with the provided seed for reproducible randomness.
+ *
+ * @param seed The seed value for the random number generator
+ * @return The complete SVG code as a string
+ */
+ public static String generate(long seed) {
+ return generate(seed, false);
+ }
+
+ /**
+ * Generates a random avatar SVG using a seed value.
+ * Creates a {@link Random} instance with the provided seed for reproducible randomness.
+ *
+ * @param seed The seed value for the random number generator
+ * @param sansEnv If true, returns the avatar without the circular background
+ * @return The complete SVG code as a string
+ */
+ public static String generate(long seed, boolean sansEnv) {
+ Random rnd = new Random(seed);
+ Avatar avatar = Avatar.fromRandom(rnd);
+ return avatar.render(sansEnv);
+ }
+
+}
diff --git a/src/main/java/com/multiavatar/SvgData.java b/src/main/java/com/multiavatar/SvgData.java
new file mode 100644
index 0000000..f431251
--- /dev/null
+++ b/src/main/java/com/multiavatar/SvgData.java
@@ -0,0 +1,294 @@
+package com.multiavatar;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Contains all SVG template strings for the 16 base characters (00-15).
+ * Each character has 6 parts: env, clo, head, mouth, eyes, top.
+ * Color placeholders (#01;, #000;, etc.) are replaced with theme colors at runtime.
+ */
+class SvgData {
+
+ private static final String ENV = "";
+
+ private static final String HEAD = "";
+
+ private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("#(?:[^;]*)(?=;)");
+
+ private static final EnumMap> SVG_PARTS = new EnumMap<>(CharacterType.class);
+
+ static {
+ EnumMap charMap;
+
+ // Character 00: Robo
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.ROBO, charMap);
+
+ // Character 01: Girl
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.GIRL, charMap);
+
+ // Character 02: Blonde
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.BLONDE, charMap);
+
+ // Character 03: Guy
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.GUY, charMap);
+
+ // Character 04: Country
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.COUNTRY, charMap);
+
+ // Character 05: Geeknot
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.GEEKNOT, charMap);
+
+ // Character 06: Asian
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.ASIAN, charMap);
+
+ // Character 07: Punk
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.PUNK, charMap);
+
+ // Character 08: Afrohair
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.AFROHAIR, charMap);
+
+ // Character 09: Normie Female
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.NORMIE_FEMALE, charMap);
+
+ // Character 10: Older
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.OLDER, charMap);
+
+ // Character 11: Firehair
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.FIREHAIR, charMap);
+
+ // Character 12: Blond
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.BLOND, charMap);
+
+ // Character 13: Ateam
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.ATEAM, charMap);
+
+ // Character 14: Rasta
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.RASTA, charMap);
+
+ // Character 15: Street
+ charMap = new EnumMap<>(AvatarPart.class);
+ putTemplate(charMap, AvatarPart.ENV, ENV);
+ putTemplate(charMap, AvatarPart.CLO, "");
+ putTemplate(charMap, AvatarPart.HEAD, HEAD);
+ putTemplate(charMap, AvatarPart.MOUTH, "");
+ putTemplate(charMap, AvatarPart.EYES, "");
+ putTemplate(charMap, AvatarPart.TOP, "");
+ SVG_PARTS.put(CharacterType.STREET, charMap);
+
+ }
+
+ static Template getSvgTemplate(CharacterType character, AvatarPart part) {
+ EnumMap parts = SVG_PARTS.get(character);
+ return parts != null ? parts.getOrDefault(part, Template.EMPTY) : Template.EMPTY;
+ }
+
+ private static void putTemplate(EnumMap charMap, AvatarPart part, String templateSource) {
+ charMap.put(part, new Template(templateSource));
+ }
+
+ static class Template {
+
+ public static final Template EMPTY = new Template("");
+
+ private static abstract class Segment {
+
+ public abstract String expand(String[] values);
+
+ public abstract Object source();
+
+ }
+
+ private static class Literal extends Segment {
+
+ private String _svgFragment;
+
+ public Literal(String svgFragment) {
+ _svgFragment = svgFragment;
+ }
+
+ @Override
+ public String expand(String[] values) {
+ return _svgFragment;
+ }
+
+ @Override
+ public Object source() {
+ return _svgFragment;
+ }
+
+ }
+
+ private static class Param extends Segment {
+
+ private String _value;
+ private int _index;
+
+ public Param(String value, int index) {
+ _value = value;
+ _index = index;
+ }
+
+ @Override
+ public String expand(String[] values) {
+ return values[_index];
+ }
+
+ @Override
+ public Object source() {
+ return _value;
+ }
+
+ }
+
+ private List _segments = new ArrayList<>();
+
+ public Template(String source) {
+ Matcher matcher = PLACEHOLDER_PATTERN.matcher(source);
+
+ int paramIndex = 0;
+ int start = 0;
+ while (matcher.find(start)) {
+ int stop = matcher.start();
+
+ if (stop > start) {
+ _segments.add(new Literal(source.substring(start, stop)));
+ }
+
+ String value = matcher.group();
+ Param param = new Param(value, paramIndex++);
+
+ _segments.add(param);
+
+ start = matcher.end();
+ }
+
+ if (start < source.length()) {
+ _segments.add(new Literal(source.substring(start)));
+ }
+ }
+
+ public String toSource() {
+ StringBuilder result = new StringBuilder();
+ for (Segment segment : _segments) {
+ result.append(segment.source());
+ }
+ return result.toString();
+ }
+
+ public void render(StringBuilder result, String[] colors) {
+ for (Segment segment : _segments) {
+ result.append(segment.expand(colors));
+ }
+ }
+
+ }
+}
diff --git a/src/main/java/com/multiavatar/Theme.java b/src/main/java/com/multiavatar/Theme.java
new file mode 100644
index 0000000..a03ce12
--- /dev/null
+++ b/src/main/java/com/multiavatar/Theme.java
@@ -0,0 +1,32 @@
+package com.multiavatar;
+
+/**
+ * Theme variants for each character
+ */
+public enum Theme {
+ A('A'),
+ B('B'),
+ C('C');
+
+ private final char code;
+
+ Theme(char code) {
+ this.code = code;
+ }
+
+ public char getCode() {
+ return code;
+ }
+
+ /**
+ * Get theme by character code ('A', 'B', or 'C')
+ */
+ public static Theme fromCode(char code) {
+ for (Theme theme : values()) {
+ if (theme.code == code) {
+ return theme;
+ }
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/multiavatar/ThemeData.java b/src/main/java/com/multiavatar/ThemeData.java
new file mode 100644
index 0000000..21e9fbc
--- /dev/null
+++ b/src/main/java/com/multiavatar/ThemeData.java
@@ -0,0 +1,519 @@
+package com.multiavatar;
+
+import java.util.EnumMap;
+
+/**
+ * Contains all theme color data for the 16 base characters.
+ * Each character has 3 themes (A, B, C) with colors for 6 parts:
+ * env, clo, head, mouth, eyes, top
+ */
+class ThemeData {
+
+ static class Colors {
+ String[] env;
+ String[] clo;
+ String[] head;
+ String[] mouth;
+ String[] eyes;
+ String[] top;
+
+ Colors(String[] env, String[] clo, String[] head, String[] mouth, String[] eyes, String[] top) {
+ this.env = env;
+ this.clo = clo;
+ this.head = head;
+ this.mouth = mouth;
+ this.eyes = eyes;
+ this.top = top;
+ }
+
+ /**
+ * Gets the color array for a specific part from theme data
+ */
+ public String[] getColors(AvatarPart avatarPart) {
+ switch (avatarPart) {
+ case ENV: return env;
+ case CLO: return clo;
+ case HEAD: return head;
+ case MOUTH: return mouth;
+ case EYES: return eyes;
+ case TOP: return top;
+ default: return new String[0];
+ }
+ }
+ }
+
+ static class CharacterThemes {
+ private final EnumMap themes;
+
+ CharacterThemes(Colors A, Colors B, Colors C) {
+ this.themes = new EnumMap<>(Theme.class);
+ this.themes.put(Theme.A, A);
+ this.themes.put(Theme.B, B);
+ this.themes.put(Theme.C, C);
+ }
+
+ Colors getTheme(Theme theme) {
+ return themes.get(theme);
+ }
+ }
+
+ private static final EnumMap THEMES = createThemes();
+
+ static CharacterThemes getCharacterThemes(CharacterType character) {
+ return THEMES.get(character);
+ }
+
+ private static EnumMap createThemes() {
+ EnumMap themes = new EnumMap<>(CharacterType.class);
+
+ // Character 00 - Robo
+ themes.put(CharacterType.ROBO, new CharacterThemes(
+ new Colors(
+ new String[]{"#ff2f2b"},
+ new String[]{"#fff", "#000"},
+ new String[]{"#fff"},
+ new String[]{"#fff", "#000", "#000"},
+ new String[]{"#000", "none", "#00FFFF"},
+ new String[]{"#fff", "#fff"}
+ ),
+ new Colors(
+ new String[]{"#ff1ec1"},
+ new String[]{"#000", "#fff"},
+ new String[]{"#ffc1c1"},
+ new String[]{"#fff", "#000", "#000"},
+ new String[]{"#FF2D00", "#fff", "none"},
+ new String[]{"#a21d00", "#fff"}
+ ),
+ new Colors(
+ new String[]{"#0079b1"},
+ new String[]{"#0e00b1", "#d1fffe"},
+ new String[]{"#f5aa77"},
+ new String[]{"#fff", "#000", "#000"},
+ new String[]{"#0c00de", "#fff", "none"},
+ new String[]{"#acfffd", "#acfffd"}
+ )
+ ));
+
+ // Character 01 - Girl
+ themes.put(CharacterType.GIRL, new CharacterThemes(
+ new Colors(
+ new String[]{"#a50000"},
+ new String[]{"#f06", "#8e0039"},
+ new String[]{"#85492C"},
+ new String[]{"#000"},
+ new String[]{"#000", "#ff9809"},
+ new String[]{"#ff9809", "#ff9809", "none", "none"}
+ ),
+ new Colors(
+ new String[]{"#40E83B"},
+ new String[]{"#00650b", "#62ce5a"},
+ new String[]{"#f7c1a6"},
+ new String[]{"#6e1c1c"},
+ new String[]{"#000", "#ff833b"},
+ new String[]{"#67FFCC", "none", "none", "#ecff3b"}
+ ),
+ new Colors(
+ new String[]{"#ff2c2c"},
+ new String[]{"#fff", "#000"},
+ new String[]{"#ffce8b"},
+ new String[]{"#000"},
+ new String[]{"#000", "#0072ff"},
+ new String[]{"#ff9809", "none", "#ffc809", "none"}
+ )
+ ));
+
+ // Character 02 - Blonde
+ themes.put(CharacterType.BLONDE, new CharacterThemes(
+ new Colors(
+ new String[]{"#ff7520"},
+ new String[]{"#d12823"},
+ new String[]{"#fee3c5"},
+ new String[]{"#d12823"},
+ new String[]{"#000", "none"},
+ new String[]{"#000", "none", "none", "#FFCC00", "red"}
+ ),
+ new Colors(
+ new String[]{"#ff9700"},
+ new String[]{"#000"},
+ new String[]{"#d2ad6d"},
+ new String[]{"#000"},
+ new String[]{"#000", "#00ffdc"},
+ new String[]{"#fdff00", "#fdff00", "none", "none", "none"}
+ ),
+ new Colors(
+ new String[]{"#26a7ff"},
+ new String[]{"#d85cd7"},
+ new String[]{"#542e02"},
+ new String[]{"#f70014"},
+ new String[]{"#000", "magenta"},
+ new String[]{"#FFCC00", "#FFCC00", "#FFCC00", "#ff0000", "yellow"}
+ )
+ ));
+
+ // Character 03 - Evilnormie
+ themes.put(CharacterType.GUY, new CharacterThemes(
+ new Colors(
+ new String[]{"#6FC30E"},
+ new String[]{"#b4e1fa", "#5b5d6e", "#515262", "#a0d2f0", "#a0d2f0"},
+ new String[]{"#fae3b9"},
+ new String[]{"#fff", "#000"},
+ new String[]{"#000"},
+ new String[]{"#8eff45", "#8eff45", "none", "none"}
+ ),
+ new Colors(
+ new String[]{"#00a58c"},
+ new String[]{"#000", "none", "none", "none", "none"},
+ new String[]{"#FAD2B9"},
+ new String[]{"#fff", "#000"},
+ new String[]{"#000"},
+ new String[]{"#FFC600", "none", "#FFC600", "none"}
+ ),
+ new Colors(
+ new String[]{"#ff501f"},
+ new String[]{"#000", "#ff0000", "#ff0000", "#7d7d7d", "#7d7d7d"},
+ new String[]{"#fff3dc"},
+ new String[]{"#d2001b", "none"},
+ new String[]{"#000"},
+ new String[]{"#D2001B", "none", "none", "#D2001B"}
+ )
+ ));
+
+ // Character 04 - Country
+ themes.put(CharacterType.COUNTRY, new CharacterThemes(
+ new Colors(
+ new String[]{"#fc0"},
+ new String[]{"#901e0e", "#ffbe1e", "#ffbe1e", "#c55f54"},
+ new String[]{"#f8d9ad"},
+ new String[]{"#000", "none", "#000", "none"},
+ new String[]{"#000"},
+ new String[]{"#583D00", "#AF892E", "#462D00", "#a0a0a0"}
+ ),
+ new Colors(
+ new String[]{"#386465"},
+ new String[]{"#fff", "#333", "#333", "#333"},
+ new String[]{"#FFD79D"},
+ new String[]{"#000", "#000", "#000", "#000"},
+ new String[]{"#000"},
+ new String[]{"#27363C", "#5DCAD4", "#314652", "#333"}
+ ),
+ new Colors(
+ new String[]{"#DFFF00"},
+ new String[]{"#304267", "#aab0b1", "#aab0b1", "#aab0b1"},
+ new String[]{"#e6b876"},
+ new String[]{"#50230a", "#50230a", "#50230a", "#50230a"},
+ new String[]{"#000"},
+ new String[]{"#333", "#afafaf", "#222", "#6d3a1d"}
+ )
+ ));
+
+ // Character 05 - Geeknot
+ themes.put(CharacterType.GEEKNOT, new CharacterThemes(
+ new Colors(
+ new String[]{"#a09300"},
+ new String[]{"#c7d4e2", "#435363", "#435363", "#141720", "#141720", "#e7ecf2", "#e7ecf2"},
+ new String[]{"#f5d4a6"},
+ new String[]{"#000", "#cf9f76"},
+ new String[]{"#000", "#000", "#000", "#000", "#000", "#000", "#000", "#000", "#fff", "#fff", "#fff", "#fff"},
+ new String[]{"none", "#fdff00"}
+ ),
+ new Colors(
+ new String[]{"#b3003e"},
+ new String[]{"#000", "#435363", "#435363", "#000", "none", "#e7ecf2", "#e7ecf2"},
+ new String[]{"#f5d4a6"},
+ new String[]{"#000", "#af9f94"},
+ new String[]{"#9ff3ff;opacity:0.96", "#000", "#9ff3ff;opacity:0.96", "#000", "#2f508a", "#000", "#000", "#000", "none", "none", "none", "none"},
+ new String[]{"#ff9a00", "#ff9a00"}
+ ),
+ new Colors(
+ new String[]{"#884f00"},
+ new String[]{"#ff0000", "#fff", "#fff", "#141720", "#141720", "#e7ecf2", "#e7ecf2"},
+ new String[]{"#c57b14"},
+ new String[]{"#000", "#cf9f76"},
+ new String[]{"none", "#000", "none", "#000", "#5a0000", "#000", "#000", "#000", "none", "none", "none", "none"},
+ new String[]{"#efefef", "none"}
+ )
+ ));
+
+ // Character 06 - Asian
+ themes.put(CharacterType.ASIAN, new CharacterThemes(
+ new Colors(
+ new String[]{"#8acf00"},
+ new String[]{"#ee2829", "#ff0"},
+ new String[]{"#ffce73"},
+ new String[]{"#fff", "#000"},
+ new String[]{"#000"},
+ new String[]{"#000", "#000", "none", "#000", "#ff4e4e", "#000"}
+ ),
+ new Colors(
+ new String[]{"#00d2a3"},
+ new String[]{"#0D0046", "#ffce73"},
+ new String[]{"#ffce73"},
+ new String[]{"#000", "none"},
+ new String[]{"#000"},
+ new String[]{"#000", "#000", "#000", "none", "#ffb358", "#000", "none", "none"}
+ ),
+ new Colors(
+ new String[]{"#ff184e"},
+ new String[]{"#000", "none"},
+ new String[]{"#ffce73"},
+ new String[]{"#ff0000", "none"},
+ new String[]{"#000"},
+ new String[]{"none", "none", "none", "none", "none", "#ffc107", "none", "none"}
+ )
+ ));
+
+ // Character 07 - Punk
+ themes.put(CharacterType.PUNK, new CharacterThemes(
+ new Colors(
+ new String[]{"#00deae"},
+ new String[]{"#ff0000"},
+ new String[]{"#ffce94"},
+ new String[]{"#f73b6c", "#000"},
+ new String[]{"#e91e63", "#000", "#e91e63", "#000", "#000", "#000"},
+ new String[]{"#dd104f", "#dd104f", "#f73b6c", "#dd104f"}
+ ),
+ new Colors(
+ new String[]{"#181284"},
+ new String[]{"#491f49", "#ff9809", "#491f49"},
+ new String[]{"#f6ba97"},
+ new String[]{"#ff9809", "#000"},
+ new String[]{"#c4ffe4", "#000", "#c4ffe4", "#000", "#000", "#000"},
+ new String[]{"none", "none", "#d6f740", "#516303"}
+ ),
+ new Colors(
+ new String[]{"#bcf700"},
+ new String[]{"#ff14e4", "#000", "#14fffd"},
+ new String[]{"#7b401e"},
+ new String[]{"#666", "#000"},
+ new String[]{"#00b5b4", "#000", "#00b5b4", "#000", "#000", "#000"},
+ new String[]{"#14fffd", "#14fffd", "#14fffd", "#0d3a62"}
+ )
+ ));
+
+ // Character 08 - Afrohair
+ themes.put(CharacterType.AFROHAIR, new CharacterThemes(
+ new Colors(
+ new String[]{"#0df"},
+ new String[]{"#571e57", "#ff0"},
+ new String[]{"#f2c280"},
+ new String[]{"#ff0000"},
+ new String[]{"#795548", "#000"},
+ new String[]{"#de3b00", "none"}
+ ),
+ new Colors(
+ new String[]{"#B400C2"},
+ new String[]{"#0D204A", "#00ffdf"},
+ new String[]{"#ca8628"},
+ new String[]{"#1a1a1a"},
+ new String[]{"#cbbdaf", "#000"},
+ new String[]{"#000", "#000"}
+ ),
+ new Colors(
+ new String[]{"#ffe926"},
+ new String[]{"#00d6af", "#000"},
+ new String[]{"#8c5100"},
+ new String[]{"#7d0000"},
+ new String[]{"none", "#000"},
+ new String[]{"#f7f7f7", "none"}
+ )
+ ));
+
+ // Character 09 - Normie Female
+ themes.put(CharacterType.NORMIE_FEMALE, new CharacterThemes(
+ new Colors(
+ new String[]{"#4aff0c"},
+ new String[]{"#101010", "#fff", "#fff"},
+ new String[]{"#dbbc7f"},
+ new String[]{"#000"},
+ new String[]{"#000", "none", "none"},
+ new String[]{"#531148", "#531148", "#531148", "none"}
+ ),
+ new Colors(
+ new String[]{"#FFC107"},
+ new String[]{"#033c58", "#fff", "#fff"},
+ new String[]{"#dbc97f"},
+ new String[]{"#000"},
+ new String[]{"none", "#000", "#fff"},
+ new String[]{"#FFEB3B", "#FFEB3B", "none", "#FFEB3B"}
+ ),
+ new Colors(
+ new String[]{"#FF9800"},
+ new String[]{"#b40000", "#fff", "#fff"},
+ new String[]{"#E2AF6B"},
+ new String[]{"#000"},
+ new String[]{"none", "#000", "#fff"},
+ new String[]{"#ec0000", "#ec0000", "none", "none"}
+ )
+ ));
+
+ // Character 10 - Older
+ themes.put(CharacterType.OLDER, new CharacterThemes(
+ new Colors(
+ new String[]{"#104c8c"},
+ new String[]{"#354B65", "#3D8EBB", "#89D0DA", "#00FFFD"},
+ new String[]{"#cc9a5c"},
+ new String[]{"#222", "#fff"},
+ new String[]{"#000", "#000"},
+ new String[]{"#fff", "#fff", "none"}
+ ),
+ new Colors(
+ new String[]{"#0DC15C"},
+ new String[]{"#212121", "#fff", "#212121", "#fff"},
+ new String[]{"#dca45f"},
+ new String[]{"#111", "#633b1d"},
+ new String[]{"#000", "#000"},
+ new String[]{"none", "#792B74", "#792B74"}
+ ),
+ new Colors(
+ new String[]{"#ffe500"},
+ new String[]{"#1e5e80", "#fff", "#1e5e80", "#fff"},
+ new String[]{"#e8bc86"},
+ new String[]{"#111", "none"},
+ new String[]{"#000", "#000"},
+ new String[]{"none", "none", "#633b1d"}
+ )
+ ));
+
+ // Character 11 - Firehair
+ themes.put(CharacterType.FIREHAIR, new CharacterThemes(
+ new Colors(
+ new String[]{"#4a3f73"},
+ new String[]{"#e6e9ee", "#f1543f", "#ff7058", "#fff", "#fff"},
+ new String[]{"#b27e5b"},
+ new String[]{"#191919", "#191919"},
+ new String[]{"#000", "#000", "#57FFFD"},
+ new String[]{"#ffc", "#ffc", "#ffc"}
+ ),
+ new Colors(
+ new String[]{"#00a08d"},
+ new String[]{"#FFBA32", "#484848", "#4e4e4e", "#fff", "#fff"},
+ new String[]{"#ab5f2c"},
+ new String[]{"#191919", "#191919"},
+ new String[]{"#000", "#ff23fa;opacity:0.39", "#000"},
+ new String[]{"#ff90f4", "#ff90f4", "#ff90f4"}
+ ),
+ new Colors(
+ new String[]{"#22535d"},
+ new String[]{"#000", "#ff2500", "#ff2500", "#fff", "#fff"},
+ new String[]{"#a76c44"},
+ new String[]{"#191919", "#191919"},
+ new String[]{"#000", "none", "#000"},
+ new String[]{"none", "#00efff", "none"}
+ )
+ ));
+
+ // Character 12 - Blond
+ themes.put(CharacterType.BLOND, new CharacterThemes(
+ new Colors(
+ new String[]{"#2668DC"},
+ new String[]{"#2385c6", "#b8d0e0", "#b8d0e0"},
+ new String[]{"#ad8a60"},
+ new String[]{"#000", "#4d4d4d"},
+ new String[]{"#7fb5a2", "#d1eddf", "#301e19"},
+ new String[]{"#fff510", "#fff510"}
+ ),
+ new Colors(
+ new String[]{"#643869"},
+ new String[]{"#D67D1B", "#b8d0e0", "#b8d0e0"},
+ new String[]{"#CC985A", "none0000"},
+ new String[]{"#000", "#ececec"},
+ new String[]{"#1f2644", "#9b97ce", "#301e19"},
+ new String[]{"#00eaff", "none"}
+ ),
+ new Colors(
+ new String[]{"#F599FF"},
+ new String[]{"#2823C6", "#b8d0e0", "#b8d0e0"},
+ new String[]{"#C7873A"},
+ new String[]{"#000", "#4d4d4d"},
+ new String[]{"#581b1b", "#FF8B8B", "#000"},
+ new String[]{"none", "#9c0092"}
+ )
+ ));
+
+ // Character 13 - Ateam
+ themes.put(CharacterType.ATEAM, new CharacterThemes(
+ new Colors(
+ new String[]{"#d10084"},
+ new String[]{"#efedee", "#00a1e0", "#00a1e0", "#efedee", "#ffce1c"},
+ new String[]{"#b35f49"},
+ new String[]{"#3a484a", "#000"},
+ new String[]{"#000"},
+ new String[]{"#000", "none", "#000", "none"}
+ ),
+ new Colors(
+ new String[]{"#E6C117"},
+ new String[]{"#efedee", "#ec0033", "#ec0033", "#efedee", "#f2ff05"},
+ new String[]{"#ffc016"},
+ new String[]{"#4a3737", "#000"},
+ new String[]{"#000"},
+ new String[]{"#ffe900", "#ffe900", "none", "#ffe900"}
+ ),
+ new Colors(
+ new String[]{"#1d8c00"},
+ new String[]{"#e000cb", "#fff", "#fff", "#e000cb", "#ffce1c"},
+ new String[]{"#b96438"},
+ new String[]{"#000", "#000"},
+ new String[]{"#000"},
+ new String[]{"#53ffff", "#53ffff", "none", "none"}
+ )
+ ));
+
+ // Character 14 - Rasta
+ themes.put(CharacterType.RASTA, new CharacterThemes(
+ new Colors(
+ new String[]{"#fc0065"},
+ new String[]{"#708913", "#fdea14", "#708913", "#fdea14", "#708913"},
+ new String[]{"#DEA561"},
+ new String[]{"#444", "#000"},
+ new String[]{"#000"},
+ new String[]{"#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f", "#32393f"}
+ ),
+ new Colors(
+ new String[]{"#81f72e"},
+ new String[]{"#ff0000", "#ffc107", "#ff0000", "#ffc107", "#ff0000"},
+ new String[]{"#ef9831"},
+ new String[]{"#6b0000", "#000"},
+ new String[]{"#000"},
+ new String[]{"#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "#FFFAAD", "none", "none", "none", "none"}
+ ),
+ new Colors(
+ new String[]{"#00D872"},
+ new String[]{"#590D00", "#FD1336", "#590D00", "#FD1336", "#590D00"},
+ new String[]{"#c36c00"},
+ new String[]{"#56442b", "#000"},
+ new String[]{"#000"},
+ new String[]{"#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "#004E4C", "none", "none", "none", "none", "none", "none", "none", "none"}
+ )
+ ));
+
+ // Character 15 - Meta
+ themes.put(CharacterType.STREET, new CharacterThemes(
+ new Colors(
+ new String[]{"#111"},
+ new String[]{"#000", "#00FFFF"},
+ new String[]{"#755227"},
+ new String[]{"#fff", "#000"},
+ new String[]{"black", "#008;opacity:0.67", "aqua"},
+ new String[]{"#fff", "#fff", "#fff", "#fff", "#fff"}
+ ),
+ new Colors(
+ new String[]{"#00D0D4"},
+ new String[]{"#000", "#fff"},
+ new String[]{"#755227"},
+ new String[]{"#fff", "#000"},
+ new String[]{"black", "#1df7ff;opacity:0.64", "#fcff2c"},
+ new String[]{"#fff539", "none", "#fff539", "none", "#fff539"}
+ ),
+ new Colors(
+ new String[]{"#DC75FF"},
+ new String[]{"#000", "#FFBDEC"},
+ new String[]{"#997549"},
+ new String[]{"#fff", "#000"},
+ new String[]{"black", "black", "aqua"},
+ new String[]{"#00fffd", "none", "none", "none", "none"}
+ )
+ ));
+
+ return themes;
+ }
+}
diff --git a/src/test/java/com/multiavatar/CompareHash.java b/src/test/java/com/multiavatar/CompareHash.java
new file mode 100644
index 0000000..ce53e5e
--- /dev/null
+++ b/src/test/java/com/multiavatar/CompareHash.java
@@ -0,0 +1,52 @@
+package com.multiavatar;
+
+import java.security.MessageDigest;
+
+/**
+ * Debug utility to verify SHA-256 hash implementation matches between Java and JavaScript.
+ * Run with: mvn test-compile exec:java -Dexec.mainClass="com.multiavatar.CompareHash" -Dexec.classpathScope=test
+ */
+public class CompareHash {
+ public static void main(String[] args) throws Exception {
+ String[] testInputs = {"Binx Bond", "Test", "Alice", "", "test"};
+
+ System.out.println("=== SHA-256 Hash Comparison ===\n");
+
+ for (String input : testInputs) {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(input.getBytes("UTF-8"));
+
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+
+ String hashStr = hexString.toString();
+ String digitsOnly = hashStr.replaceAll("\\D", "");
+ String first12 = digitsOnly.substring(0, Math.min(12, digitsOnly.length()));
+
+ System.out.printf("Input: \"%s\"%n", input);
+ System.out.printf(" Full hash: %s%n", hashStr);
+ System.out.printf(" Digits only: %s%n", digitsOnly);
+ System.out.printf(" First 12 digits: %s%n", first12);
+
+ // Show how these map to part numbers
+ if (first12.length() >= 12) {
+ for (int i = 0; i < 6; i++) {
+ String digitPair = first12.substring(i * 2, i * 2 + 2);
+ int value = Integer.parseInt(digitPair);
+ int partNum = Math.round((47f / 100f) * value);
+ System.out.printf(" Part %d: %s (dec) -> %d (part 0-47)%n",
+ i, digitPair, partNum);
+ }
+ }
+ System.out.println();
+ }
+
+ System.out.println("\nExpected hashes from JavaScript (verify these match):");
+ System.out.println(" \"Binx Bond\" -> c24e86de81fa... (first 12: c24e86de81fa)");
+ System.out.println(" \"Test\" -> 532eaabd9574... (first 12: 532eaabd9574)");
+ }
+}
diff --git a/src/test/java/com/multiavatar/CompareJsJavaOutput.java b/src/test/java/com/multiavatar/CompareJsJavaOutput.java
new file mode 100644
index 0000000..4da3a78
--- /dev/null
+++ b/src/test/java/com/multiavatar/CompareJsJavaOutput.java
@@ -0,0 +1,123 @@
+package com.multiavatar;
+
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Debug utility to compare JavaScript and Java outputs directly.
+ * Run with: mvn test-compile exec:java -Dexec.mainClass="com.multiavatar.CompareJsJavaOutput" -Dexec.classpathScope=test
+ */
+public class CompareJsJavaOutput {
+ public static void main(String[] args) throws Exception {
+ String input = "Test";
+
+ // Generate using Java (predefined avatar)
+ String javaOutput = Multiavatar.generate(CharacterType.GEEKNOT, Theme.A);
+
+ // Debug: Print which parts are being used
+ System.out.println("=== DEBUG: Hash and Parts for: \"" + input + "\" ===");
+ java.security.MessageDigest digest = java.security.MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(input.getBytes("UTF-8"));
+ StringBuilder hexString = new StringBuilder();
+ for (byte b : hash) {
+ String hex = Integer.toHexString(0xff & b);
+ if (hex.length() == 1) hexString.append('0');
+ hexString.append(hex);
+ }
+ String hashStr = hexString.toString();
+ String digitsOnly = hashStr.replaceAll("\\D", "");
+ String first12 = digitsOnly.substring(0, Math.min(12, digitsOnly.length()));
+ System.out.println("Hash: " + hashStr);
+ System.out.println("Digits only: " + digitsOnly);
+ System.out.println("First 12: " + first12);
+ System.out.println();
+
+ System.out.println("=== Comparing Output for: \"" + input + "\" [05A] ===\n");
+
+ System.out.println("Java output length: " + javaOutput.length());
+ System.out.println("Java output (first 300 chars):");
+ System.out.println(javaOutput.substring(0, Math.min(300, javaOutput.length())));
+ System.out.println();
+
+ // Load JavaScript output from test vectors
+ try {
+ String jsData = new String(Files.readAllBytes(Paths.get("test-vectors.json")));
+
+ // Find the entry for case 15 (Test with version 05A)
+ int idx = jsData.indexOf("\"id\": 15");
+ if (idx > 0) {
+ int outputIdx = jsData.indexOf("\"output\":", idx);
+ int outputStart = jsData.indexOf("\"", outputIdx + 9) + 1;
+
+ // Find the end quote (accounting for escaped quotes)
+ int outputEnd = outputStart;
+ while (outputEnd < jsData.length()) {
+ if (jsData.charAt(outputEnd) == '"' && jsData.charAt(outputEnd - 1) != '\\') {
+ break;
+ }
+ outputEnd++;
+ }
+
+ String jsOutput = jsData.substring(outputStart, outputEnd);
+ // Unescape JSON
+ jsOutput = jsOutput.replace("\\\"", "\"")
+ .replace("\\\\", "\\")
+ .replace("\\n", "\n")
+ .replace("\\r", "\r")
+ .replace("\\t", "\t");
+
+ System.out.println("JavaScript output length: " + jsOutput.length());
+ System.out.println("JavaScript output (first 300 chars):");
+ System.out.println(jsOutput.substring(0, Math.min(300, jsOutput.length())));
+ System.out.println();
+
+ // Character-by-character comparison
+ System.out.println("=== Character-by-Character Comparison (first 100 chars) ===");
+ int compareLen = Math.min(100, Math.min(javaOutput.length(), jsOutput.length()));
+ for (int i = 0; i < compareLen; i++) {
+ char javaChar = javaOutput.charAt(i);
+ char jsChar = jsOutput.charAt(i);
+ if (javaChar != jsChar) {
+ System.out.printf("Position %d: Java='%c' (0x%02x), JS='%c' (0x%02x)\n",
+ i, javaChar, (int)javaChar, jsChar, (int)jsChar);
+ }
+ }
+
+ // Compare structure
+ System.out.println("\n=== Structural Comparison ===");
+ System.out.println("Java starts with: " + javaOutput.substring(0, Math.min(80, javaOutput.length())));
+ System.out.println("JS starts with: " + jsOutput.substring(0, Math.min(80, jsOutput.length())));
+
+ int len = Math.min(javaOutput.length(), jsOutput.length());
+ int firstDiff = -1;
+ for (int i = 0; i < len; i++) {
+ if (javaOutput.charAt(i) != jsOutput.charAt(i)) {
+ firstDiff = i;
+ break;
+ }
+ }
+
+ if (firstDiff >= 0) {
+ System.out.println("\nFirst difference at position " + firstDiff + ":");
+ int start = Math.max(0, firstDiff - 20);
+ int end = Math.min(len, firstDiff + 60);
+ System.out.println("Java: ..." + javaOutput.substring(start, end) + "...");
+ System.out.println("JS: ..." + jsOutput.substring(start, end) + "...");
+ } else {
+ System.out.println("\nNo character differences found in common length!");
+ if (javaOutput.length() != jsOutput.length()) {
+ System.out.println("But lengths differ: Java=" + javaOutput.length() + ", JS=" + jsOutput.length());
+ }
+ }
+
+ System.out.println("\nMatch: " + javaOutput.equals(jsOutput));
+
+ } else {
+ System.out.println("Could not find case 15 in test vectors");
+ }
+ } catch (Exception e) {
+ System.out.println("Error reading test vectors: " + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java b/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java
new file mode 100644
index 0000000..7950e04
--- /dev/null
+++ b/src/test/java/com/multiavatar/CrossPlatformCompatibilityTest.java
@@ -0,0 +1,254 @@
+package com.multiavatar;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Cross-platform compatibility test.
+ * Verifies that the Java implementation produces identical output
+ * to the JavaScript implementation for the same inputs.
+ *
+ * Test vectors are generated by running: node generate-test-vectors.cjs
+ */
+public class CrossPlatformCompatibilityTest {
+
+ private static class TestVector {
+ int id;
+ String input;
+ boolean sansEnv;
+ Version version;
+ String output;
+ int length;
+ String error;
+
+ static class Version {
+ String part;
+ char theme;
+ }
+ }
+
+ private static List testVectors;
+
+ @BeforeClass
+ public static void loadTestVectors() throws IOException {
+ testVectors = parseTestVectors("test-vectors.json");
+ System.out.println("Loaded " + testVectors.size() + " test vectors from JavaScript implementation");
+ }
+
+ @Test
+ public void testAllVectorsMatch() {
+ int passed = 0;
+ int failed = 0;
+ List failures = new ArrayList<>();
+
+ for (TestVector vector : testVectors) {
+ // Skip vectors that had errors in JS
+ if (vector.error != null) {
+ continue;
+ }
+
+ try {
+ // Generate avatar using Java implementation
+ String javaOutput;
+ if (vector.version != null) {
+ // Use predefined character/theme
+ CharacterType character = CharacterType.fromId(vector.version.part);
+ Theme theme = Theme.fromCode(vector.version.theme);
+ javaOutput = Multiavatar.generate(character, theme, vector.sansEnv);
+ } else {
+ // Generate from string ID
+ javaOutput = Multiavatar.generate(vector.input, vector.sansEnv);
+ }
+
+ // Strip attribution metadata from Java output for comparison
+ // (Java adds metadata for license compliance)
+ String javaOutputStripped = javaOutput.replace("Multiavatar https://multiavatar.com ", "");
+
+ // Compare with JavaScript output
+ if (javaOutputStripped.equals(vector.output)) {
+ passed++;
+ } else {
+ failed++;
+ String versionStr = vector.version != null ?
+ " [" + vector.version.part + vector.version.theme + "]" : "";
+ String failureMsg = String.format(
+ "Case %d FAILED: \"%s\"%s sansEnv=%b\n" +
+ " Expected length: %d, Got: %d (stripped: %d)\n" +
+ " Output differs from JavaScript implementation\n" +
+ " Expected: %s\n" +
+ " Actual: %s\n",
+ vector.id,
+ vector.input.length() > 30 ? vector.input.substring(0, 30) + "..." : vector.input,
+ versionStr,
+ vector.sansEnv,
+ vector.output.length(),
+ javaOutput.length(),
+ javaOutputStripped.length(),
+ vector.output,
+ javaOutputStripped
+ );
+ failures.add(failureMsg);
+ System.err.println(failureMsg);
+
+ File failureDir = new File("target/failures");
+ if (!failureDir.exists()) {
+ failureDir.mkdirs();
+ }
+
+ GenerateExamplesTest.writeToFile(new File(failureDir, vector.id + "-expected.svg"), vector.output);
+ GenerateExamplesTest.writeToFile(new File(failureDir, vector.id + "-failure.svg"), javaOutputStripped);
+ }
+ } catch (Exception e) {
+ failed++;
+ String failureMsg = String.format(
+ "Case %d EXCEPTION: \"%s\" - %s",
+ vector.id,
+ vector.input,
+ e.getMessage()
+ );
+ failures.add(failureMsg);
+ System.err.println(failureMsg);
+ }
+ }
+
+ // Print summary
+ System.out.println("\n=== Cross-Platform Compatibility Test Results ===");
+ System.out.println("Total vectors tested: " + (passed + failed));
+ System.out.println("Passed: " + passed);
+ System.out.println("Failed: " + failed);
+
+ if (failed > 0) {
+ System.err.println("\n=== Failures ===");
+ for (String failure : failures) {
+ System.err.println(failure);
+ }
+ }
+
+ // Assert all tests passed
+ assertEquals("Java implementation must match JavaScript output for all test vectors",
+ 0, failed);
+ }
+
+ /**
+ * Simple JSON parser for test vectors (avoiding external dependencies)
+ */
+ private static List parseTestVectors(String filename) throws IOException {
+ String content = new String(Files.readAllBytes(Paths.get(filename)));
+ List vectors = new ArrayList<>();
+
+ // Parse JSON array manually (simple approach for our specific format)
+ String[] items = content.substring(content.indexOf('[') + 1, content.lastIndexOf(']'))
+ .split("\\},\\s*\\{");
+
+ for (String item : items) {
+ item = item.trim();
+ if (!item.startsWith("{")) item = "{" + item;
+ if (!item.endsWith("}")) item = item + "}";
+
+ TestVector vector = new TestVector();
+
+ // Extract fields
+ vector.id = extractInt(item, "\"id\"");
+ vector.input = extractString(item, "\"input\"");
+ vector.sansEnv = extractBoolean(item, "\"sansEnv\"");
+ vector.length = extractInt(item, "\"length\"");
+ vector.output = extractString(item, "\"output\"");
+ vector.error = extractString(item, "\"error\"");
+
+ // Extract version if present
+ int versionStart = item.indexOf("\"version\"");
+ if (versionStart > 0) {
+ int versionObjStart = item.indexOf('{', versionStart);
+ int versionObjEnd = item.indexOf('}', versionObjStart);
+ if (versionObjStart > 0 && versionObjEnd > 0) {
+ String versionStr = item.substring(versionObjStart, versionObjEnd + 1);
+ if (!versionStr.contains("null")) {
+ TestVector.Version version = new TestVector.Version();
+ version.part = extractString(versionStr, "\"part\"");
+ String themeStr = extractString(versionStr, "\"theme\"");
+ if (themeStr != null && !themeStr.isEmpty()) {
+ version.theme = themeStr.charAt(0);
+ }
+ vector.version = version;
+ }
+ }
+ }
+
+ vectors.add(vector);
+ }
+
+ return vectors;
+ }
+
+ private static String extractString(String json, String key) {
+ int keyIndex = json.indexOf(key);
+ if (keyIndex < 0) return null;
+
+ int colonIndex = json.indexOf(':', keyIndex);
+ int quoteStart = json.indexOf('"', colonIndex);
+ if (quoteStart < 0) return null;
+
+ int quoteEnd = quoteStart + 1;
+ while (quoteEnd < json.length()) {
+ if (json.charAt(quoteEnd) == '"' && json.charAt(quoteEnd - 1) != '\\') {
+ break;
+ }
+ quoteEnd++;
+ }
+
+ if (quoteEnd >= json.length()) return null;
+
+ String value = json.substring(quoteStart + 1, quoteEnd);
+ // Unescape JSON strings
+ value = value.replace("\\\"", "\"")
+ .replace("\\\\", "\\")
+ .replace("\\n", "\n")
+ .replace("\\r", "\r")
+ .replace("\\t", "\t");
+
+ return value;
+ }
+
+ private static int extractInt(String json, String key) {
+ int keyIndex = json.indexOf(key);
+ if (keyIndex < 0) return 0;
+
+ int colonIndex = json.indexOf(':', keyIndex);
+ int numStart = colonIndex + 1;
+
+ // Skip whitespace
+ while (numStart < json.length() && Character.isWhitespace(json.charAt(numStart))) {
+ numStart++;
+ }
+
+ int numEnd = numStart;
+ while (numEnd < json.length() && (Character.isDigit(json.charAt(numEnd)) || json.charAt(numEnd) == '-')) {
+ numEnd++;
+ }
+
+ if (numEnd > numStart) {
+ return Integer.parseInt(json.substring(numStart, numEnd));
+ }
+
+ return 0;
+ }
+
+ private static boolean extractBoolean(String json, String key) {
+ int keyIndex = json.indexOf(key);
+ if (keyIndex < 0) return false;
+
+ int colonIndex = json.indexOf(':', keyIndex);
+ String afterColon = json.substring(colonIndex + 1).trim();
+
+ return afterColon.startsWith("true");
+ }
+}
diff --git a/src/test/java/com/multiavatar/DebugTest.java b/src/test/java/com/multiavatar/DebugTest.java
new file mode 100644
index 0000000..d9a0536
--- /dev/null
+++ b/src/test/java/com/multiavatar/DebugTest.java
@@ -0,0 +1,25 @@
+package com.multiavatar;
+
+import com.multiavatar.SvgData.Template;
+
+public class DebugTest {
+ public static void main(String[] args) {
+ System.out.println("Testing Multiavatar...");
+
+ String svg = Multiavatar.generate("Test");
+ System.out.println("Generated SVG length: " + svg.length());
+ System.out.println("SVG content:\n" + svg);
+
+ // Test SVG data
+ Template svgPart = SvgData.getSvgTemplate(CharacterType.ROBO, AvatarPart.ENV);
+ System.out.println("\nSVG Part ROBO/ENV: " + (svgPart != null ? svgPart.toSource().substring(0, Math.min(100, svgPart.toSource().length())) : "NULL"));
+
+ // Test theme data
+ ThemeData.CharacterThemes themes = ThemeData.getCharacterThemes(CharacterType.ROBO);
+ System.out.println("\nThemes for ROBO: " + (themes != null ? "Found" : "NULL"));
+ if (themes != null) {
+ ThemeData.Colors themeA = themes.getTheme(Theme.A);
+ System.out.println("Theme A env colors: " + java.util.Arrays.toString(themeA.env));
+ }
+ }
+}
diff --git a/src/test/java/com/multiavatar/GenerateExamplesTest.java b/src/test/java/com/multiavatar/GenerateExamplesTest.java
new file mode 100644
index 0000000..ee201ec
--- /dev/null
+++ b/src/test/java/com/multiavatar/GenerateExamplesTest.java
@@ -0,0 +1,92 @@
+package com.multiavatar;
+
+import org.junit.Test;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.file.Files;
+
+/**
+ * Test that generates example SVG files for manual inspection.
+ * The generated files are placed in target/examples/
+ */
+public class GenerateExamplesTest {
+
+ @Test
+ public void generateExampleAvatars() throws IOException {
+ // Create examples directory
+ File examplesDir = new File("target/examples");
+ if (!examplesDir.exists()) {
+ examplesDir.mkdirs();
+ }
+
+ // Sample input strings to generate avatars from
+ String[] inputs = {
+ "Binx Bond",
+ "Test User",
+ "Alice",
+ "Bob",
+ "Charlie",
+ "Diana",
+ "Emma",
+ "Frank",
+ "Grace",
+ "Henry",
+ "测试用户",
+ "user@example.com",
+ "John Doe",
+ "Jane Smith",
+ "Admin123",
+ "guest",
+ "developer",
+ "designer",
+ "manager",
+ "support"
+ };
+
+ // Generate avatars with background
+ for (String input : inputs) {
+ String svg = Multiavatar.generate(input);
+ String filename = sanitizeFilename(input) + ".svg";
+ writeToFile(new File(examplesDir, filename), svg);
+ }
+
+ // Generate some examples without background
+ String[] sansEnvInputs = {"Binx Bond", "Alice", "Charlie", "Emma"};
+ for (String input : sansEnvInputs) {
+ String svg = Multiavatar.generate(input, true);
+ String filename = sanitizeFilename(input) + "_no_background.svg";
+ writeToFile(new File(examplesDir, filename), svg);
+ }
+
+ // Generate examples with forced versions (different themes for same character)
+ String testInput = "Version Test";
+ for (int characterIndex = 0; characterIndex < 3; characterIndex++) {
+ CharacterType character = CharacterType.fromIndex(characterIndex);
+ for (Theme theme : Theme.values()) {
+ String svg = Multiavatar.generate(character, theme);
+ String filename = "version_char" + character.getId() + "_theme" + theme.getCode() + ".svg";
+ writeToFile(new File(examplesDir, filename), svg);
+ }
+ }
+
+ System.out.println("Generated " + (inputs.length + sansEnvInputs.length + 9) + " example SVG files in target/examples/");
+ System.out.println("Open these files in a web browser to view the avatars.");
+ }
+
+ /**
+ * Sanitize filename to be filesystem-safe
+ */
+ private String sanitizeFilename(String input) {
+ return input.replaceAll("[^a-zA-Z0-9_-]", "_").toLowerCase();
+ }
+
+ /**
+ * Write content to file
+ */
+ static void writeToFile(File file, String content) throws IOException {
+ try (FileWriter writer = new FileWriter(file)) {
+ writer.write(content);
+ }
+ }
+}
diff --git a/src/test/java/com/multiavatar/MultiavatarTest.java b/src/test/java/com/multiavatar/MultiavatarTest.java
new file mode 100644
index 0000000..abd2752
--- /dev/null
+++ b/src/test/java/com/multiavatar/MultiavatarTest.java
@@ -0,0 +1,124 @@
+package com.multiavatar;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for Multiavatar
+ */
+public class MultiavatarTest {
+
+ @Test
+ public void testGenerateBasic() {
+ String svg = Multiavatar.generate("Binx Bond");
+ assertNotNull("SVG should not be null", svg);
+ assertTrue("SVG should start with ", svg.endsWith(" "));
+ assertTrue("SVG should contain viewBox", svg.contains("viewBox"));
+ assertTrue("SVG should contain metadata with creator", svg.contains("Multiavatar "));
+ assertTrue("SVG should contain metadata with source", svg.contains("https://multiavatar.com "));
+ }
+
+ @Test
+ public void testGenerateSansEnv() {
+ String svgWithEnv = Multiavatar.generate("Test User", false);
+ String svgWithoutEnv = Multiavatar.generate("Test User", true);
+
+ assertNotNull("SVG with env should not be null", svgWithEnv);
+ assertNotNull("SVG without env should not be null", svgWithoutEnv);
+
+ // Both should be valid SVGs
+ assertTrue("SVG with env should be valid", svgWithEnv.startsWith("");
+ assertEquals("Should have matching svg tags", openTags, closeTags);
+ }
+
+ private int countOccurrences(String str, String substr) {
+ int count = 0;
+ int index = 0;
+ while ((index = str.indexOf(substr, index)) != -1) {
+ count++;
+ index += substr.length();
+ }
+ return count;
+ }
+}
diff --git a/src/test/java/com/multiavatar/TestChar05Debug.java b/src/test/java/com/multiavatar/TestChar05Debug.java
new file mode 100644
index 0000000..a675260
--- /dev/null
+++ b/src/test/java/com/multiavatar/TestChar05Debug.java
@@ -0,0 +1,54 @@
+package com.multiavatar;
+
+public class TestChar05Debug {
+ public static void main(String[] args) {
+ // Get theme for character GEEKNOT (05) Theme A
+ ThemeData.CharacterThemes ct = ThemeData.getCharacterThemes(CharacterType.GEEKNOT);
+ ThemeData.Colors theme = ct.getTheme(Theme.A);
+ String[] eyes = theme.eyes;
+ System.out.println("Character GEEKNOT (05) Theme A eyes colors (" + eyes.length + " total):");
+ for (int i = 0; i < eyes.length; i++) {
+ System.out.println(" " + i + ": " + eyes[i]);
+ }
+
+ // Generate and check output
+ String svg = Multiavatar.generate(CharacterType.GEEKNOT, Theme.A);
+
+ // Extract just the eyes SVG for comparison
+ int eyesStart = svg.indexOf(" 0) {
+ // Find the last in the eyes section (ends with the last 4;stroke:#fff;"/>)
+ int searchStart = eyesStart + 1500;
+ int eyesEnd = -1;
+ for (int i = 0; i < 5; i++) {
+ eyesEnd = svg.indexOf("stroke:#fff;\"/>", searchStart);
+ if (eyesEnd > 0) {
+ eyesEnd += 14; // length of "stroke:#fff;"/>"
+ break;
+ }
+ searchStart -= 200;
+ }
+ if (eyesEnd > eyesStart) {
+ String eyesSvg = svg.substring(eyesStart, eyesEnd);
+ try {
+ java.nio.file.Files.write(java.nio.file.Paths.get("/tmp/java_eyes_output.txt"), eyesSvg.getBytes());
+ System.out.println("\nWrote eyes SVG to /tmp/java_eyes_output.txt (" + eyesSvg.length() + " chars)");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ // Find the stroke color before "m109.32"
+ int m109Pos = svg.indexOf("m109.32");
+ if (m109Pos > 0) {
+ int strokePos = svg.lastIndexOf("stroke:#", m109Pos);
+ if (strokePos > 0) {
+ String strokeColor = svg.substring(strokePos, strokePos + 13);
+ System.out.println("\nStroke color before 'm109.32': " + strokeColor);
+ System.out.println("Expected: stroke:#000;");
+ System.out.println("Match: " + strokeColor.equals("stroke:#000;"));
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/multiavatar/TestColorReplacement.java b/src/test/java/com/multiavatar/TestColorReplacement.java
new file mode 100644
index 0000000..43a9339
--- /dev/null
+++ b/src/test/java/com/multiavatar/TestColorReplacement.java
@@ -0,0 +1,36 @@
+package com.multiavatar;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TestColorReplacement {
+ public static void main(String[] args) {
+ // Simplified version of character 05 eyes template (last part)
+ String template = "stroke-width:4;stroke:#fff;\"/>";
+
+ // Last 4 colors from theme A eyes (indices 8-11)
+ String[] colors = new String[]{"#fff", "#fff", "#fff", "#fff", "#000", "#000"};
+
+ // Java replacement logic from Multiavatar.java
+ Pattern pattern = Pattern.compile("#(.*?);");
+ Matcher matcher = pattern.matcher(template);
+
+ int colorIndex = 0;
+ StringBuffer sb = new StringBuffer();
+ System.out.println("=== Replacements ===");
+ while (matcher.find() && colorIndex < colors.length) {
+ String matched = matcher.group(0);
+ String replacement = colors[colorIndex] + ";";
+ System.out.println((colorIndex + 1) + ". \"" + matched + "\" -> \"" + replacement + "\"");
+ matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
+ colorIndex++;
+ }
+ matcher.appendTail(sb);
+
+ System.out.println("\n=== Result ===");
+ System.out.println(sb.toString());
+
+ System.out.println("\n=== Expected (last stroke should be #000) ===");
+ System.out.println("...stroke:#000;\"/>");
+ }
+}
diff --git a/src/test/java/com/multiavatar/TestRegex.java b/src/test/java/com/multiavatar/TestRegex.java
new file mode 100644
index 0000000..902bc16
--- /dev/null
+++ b/src/test/java/com/multiavatar/TestRegex.java
@@ -0,0 +1,27 @@
+package com.multiavatar;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class TestRegex {
+ public static void main(String[] args) {
+ String template = "";
+
+ Pattern pattern = Pattern.compile("#(.*?);");
+ Matcher matcher = pattern.matcher(template);
+
+ System.out.println("=== Regex Matches ===");
+ int count = 0;
+ while (matcher.find()) {
+ count++;
+ String match = matcher.group(0);
+ if (match.length() > 50) {
+ System.out.println(count + ". " + match.substring(0, 50) + "... (" + match.length() + " chars total)");
+ } else {
+ System.out.println(count + ". " + match);
+ }
+ }
+ System.out.println("\nTotal matches: " + count);
+ System.out.println("Expected: 2 (two #1a1a1a; and two #333;)");
+ }
+}
diff --git a/test-vectors.json b/test-vectors.json
new file mode 100644
index 0000000..864bc5f
--- /dev/null
+++ b/test-vectors.json
@@ -0,0 +1,1403 @@
+[
+ {
+ "id": 0,
+ "input": "Binx Bond",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 2077
+ },
+ {
+ "id": 1,
+ "input": "Test User",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 5633
+ },
+ {
+ "id": 2,
+ "input": "Alice",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 4519
+ },
+ {
+ "id": 3,
+ "input": "Bob",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 2706
+ },
+ {
+ "id": 4,
+ "input": "Example",
+ "sansEnv": true,
+ "version": null,
+ "output": " ",
+ "length": 4601
+ },
+ {
+ "id": 5,
+ "input": "No Background",
+ "sansEnv": true,
+ "version": null,
+ "output": " ",
+ "length": 4892
+ },
+ {
+ "id": 6,
+ "input": "",
+ "sansEnv": false,
+ "version": null,
+ "output": "",
+ "length": 0
+ },
+ {
+ "id": 7,
+ "input": "test",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 3598
+ },
+ {
+ "id": 8,
+ "input": "TEST",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 4038
+ },
+ {
+ "id": 9,
+ "input": "user@example.com",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 2814
+ },
+ {
+ "id": 10,
+ "input": "User Name 123!",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 3449
+ },
+ {
+ "id": 11,
+ "input": "测试用户",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 4239
+ },
+ {
+ "id": 12,
+ "input": "user-with-dashes",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 3964
+ },
+ {
+ "id": 13,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "00",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3643
+ },
+ {
+ "id": 14,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3788
+ },
+ {
+ "id": 15,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "02",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 5034
+ },
+ {
+ "id": 16,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "03",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2896
+ },
+ {
+ "id": 17,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "04",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3816
+ },
+ {
+ "id": 18,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "05",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4718
+ },
+ {
+ "id": 19,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "06",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2897
+ },
+ {
+ "id": 20,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "07",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2756
+ },
+ {
+ "id": 21,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "08",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4529
+ },
+ {
+ "id": 22,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "09",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3673
+ },
+ {
+ "id": 23,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "10",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3875
+ },
+ {
+ "id": 24,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "11",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 5690
+ },
+ {
+ "id": 25,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "12",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3142
+ },
+ {
+ "id": 26,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "13",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2833
+ },
+ {
+ "id": 27,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "14",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4695
+ },
+ {
+ "id": 28,
+ "input": "Test",
+ "sansEnv": false,
+ "version": {
+ "part": "15",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4920
+ },
+ {
+ "id": 29,
+ "input": "Theme",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3788
+ },
+ {
+ "id": 30,
+ "input": "Theme",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3794
+ },
+ {
+ "id": 31,
+ "input": "Theme",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3785
+ },
+ {
+ "id": 32,
+ "input": "This is a longer test string with multiple words",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 3362
+ },
+ {
+ "id": 33,
+ "input": "john.doe@example.com",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 3539
+ },
+ {
+ "id": 34,
+ "input": "12345",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 4481
+ },
+ {
+ "id": 35,
+ "input": "User123",
+ "sansEnv": false,
+ "version": null,
+ "output": " ",
+ "length": 4491
+ },
+ {
+ "id": 36,
+ "input": "Robo - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "00",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3643
+ },
+ {
+ "id": 37,
+ "input": "Robo - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "00",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3540
+ },
+ {
+ "id": 38,
+ "input": "Robo - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "00",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3649
+ },
+ {
+ "id": 39,
+ "input": "Robo - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "00",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3546
+ },
+ {
+ "id": 40,
+ "input": "Robo - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "00",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3658
+ },
+ {
+ "id": 41,
+ "input": "Robo - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "00",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3555
+ },
+ {
+ "id": 42,
+ "input": "Girl - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3788
+ },
+ {
+ "id": 43,
+ "input": "Girl - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "01",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3685
+ },
+ {
+ "id": 44,
+ "input": "Girl - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3794
+ },
+ {
+ "id": 45,
+ "input": "Girl - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "01",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3691
+ },
+ {
+ "id": 46,
+ "input": "Girl - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "01",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3785
+ },
+ {
+ "id": 47,
+ "input": "Girl - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "01",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3682
+ },
+ {
+ "id": 48,
+ "input": "Blonde - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "02",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 5034
+ },
+ {
+ "id": 49,
+ "input": "Blonde - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "02",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4931
+ },
+ {
+ "id": 50,
+ "input": "Blonde - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "02",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 5035
+ },
+ {
+ "id": 51,
+ "input": "Blonde - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "02",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4932
+ },
+ {
+ "id": 52,
+ "input": "Blonde - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "02",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 5049
+ },
+ {
+ "id": 53,
+ "input": "Blonde - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "02",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4946
+ },
+ {
+ "id": 54,
+ "input": "Guy - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "03",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2896
+ },
+ {
+ "id": 55,
+ "input": "Guy - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "03",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2793
+ },
+ {
+ "id": 56,
+ "input": "Guy - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "03",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2881
+ },
+ {
+ "id": 57,
+ "input": "Guy - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "03",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2778
+ },
+ {
+ "id": 58,
+ "input": "Guy - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "03",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2896
+ },
+ {
+ "id": 59,
+ "input": "Guy - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "03",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2793
+ },
+ {
+ "id": 60,
+ "input": "Country - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "04",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3816
+ },
+ {
+ "id": 61,
+ "input": "Country - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "04",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3716
+ },
+ {
+ "id": 62,
+ "input": "Country - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "04",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3804
+ },
+ {
+ "id": 63,
+ "input": "Country - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "04",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3701
+ },
+ {
+ "id": 64,
+ "input": "Country - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "04",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3825
+ },
+ {
+ "id": 65,
+ "input": "Country - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "04",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3722
+ },
+ {
+ "id": 66,
+ "input": "Geeknot - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "05",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4718
+ },
+ {
+ "id": 67,
+ "input": "Geeknot - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "05",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4615
+ },
+ {
+ "id": 68,
+ "input": "Geeknot - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "05",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4747
+ },
+ {
+ "id": 69,
+ "input": "Geeknot - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "05",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4644
+ },
+ {
+ "id": 70,
+ "input": "Geeknot - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "05",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4715
+ },
+ {
+ "id": 71,
+ "input": "Geeknot - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "05",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4612
+ },
+ {
+ "id": 72,
+ "input": "Asian - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "06",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2897
+ },
+ {
+ "id": 73,
+ "input": "Asian - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "06",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2794
+ },
+ {
+ "id": 74,
+ "input": "Asian - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "06",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2900
+ },
+ {
+ "id": 75,
+ "input": "Asian - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "06",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2797
+ },
+ {
+ "id": 76,
+ "input": "Asian - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "06",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2897
+ },
+ {
+ "id": 77,
+ "input": "Asian - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "06",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2794
+ },
+ {
+ "id": 78,
+ "input": "Punk - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "07",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2756
+ },
+ {
+ "id": 79,
+ "input": "Punk - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "07",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2653
+ },
+ {
+ "id": 80,
+ "input": "Punk - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "07",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2750
+ },
+ {
+ "id": 81,
+ "input": "Punk - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "07",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2647
+ },
+ {
+ "id": 82,
+ "input": "Punk - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "07",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2753
+ },
+ {
+ "id": 83,
+ "input": "Punk - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "07",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2650
+ },
+ {
+ "id": 84,
+ "input": "Afrohair - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "08",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4529
+ },
+ {
+ "id": 85,
+ "input": "Afrohair - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "08",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4429
+ },
+ {
+ "id": 86,
+ "input": "Afrohair - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "08",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4532
+ },
+ {
+ "id": 87,
+ "input": "Afrohair - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "08",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4429
+ },
+ {
+ "id": 88,
+ "input": "Afrohair - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "08",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4529
+ },
+ {
+ "id": 89,
+ "input": "Afrohair - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "08",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4426
+ },
+ {
+ "id": 90,
+ "input": "Normie Female - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "09",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3673
+ },
+ {
+ "id": 91,
+ "input": "Normie Female - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "09",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3570
+ },
+ {
+ "id": 92,
+ "input": "Normie Female - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "09",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3673
+ },
+ {
+ "id": 93,
+ "input": "Normie Female - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "09",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3570
+ },
+ {
+ "id": 94,
+ "input": "Normie Female - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "09",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3670
+ },
+ {
+ "id": 95,
+ "input": "Normie Female - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "09",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3567
+ },
+ {
+ "id": 96,
+ "input": "Older - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "10",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3875
+ },
+ {
+ "id": 97,
+ "input": "Older - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "10",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3772
+ },
+ {
+ "id": 98,
+ "input": "Older - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "10",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3878
+ },
+ {
+ "id": 99,
+ "input": "Older - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "10",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3775
+ },
+ {
+ "id": 100,
+ "input": "Older - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "10",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3872
+ },
+ {
+ "id": 101,
+ "input": "Older - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "10",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3769
+ },
+ {
+ "id": 102,
+ "input": "Firehair - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "11",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 5690
+ },
+ {
+ "id": 103,
+ "input": "Firehair - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "11",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 5587
+ },
+ {
+ "id": 104,
+ "input": "Firehair - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "11",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 5709
+ },
+ {
+ "id": 105,
+ "input": "Firehair - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "11",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 5606
+ },
+ {
+ "id": 106,
+ "input": "Firehair - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "11",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 5687
+ },
+ {
+ "id": 107,
+ "input": "Firehair - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "11",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 5584
+ },
+ {
+ "id": 108,
+ "input": "Blond - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "12",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3142
+ },
+ {
+ "id": 109,
+ "input": "Blond - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "12",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 3039
+ },
+ {
+ "id": 110,
+ "input": "Blond - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "12",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3139
+ },
+ {
+ "id": 111,
+ "input": "Blond - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "12",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 3036
+ },
+ {
+ "id": 112,
+ "input": "Blond - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "12",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3136
+ },
+ {
+ "id": 113,
+ "input": "Blond - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "12",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 3033
+ },
+ {
+ "id": 114,
+ "input": "Ateam - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "13",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2833
+ },
+ {
+ "id": 115,
+ "input": "Ateam - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "13",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 2730
+ },
+ {
+ "id": 116,
+ "input": "Ateam - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "13",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2842
+ },
+ {
+ "id": 117,
+ "input": "Ateam - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "13",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 2739
+ },
+ {
+ "id": 118,
+ "input": "Ateam - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "13",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2830
+ },
+ {
+ "id": 119,
+ "input": "Ateam - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "13",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 2727
+ },
+ {
+ "id": 120,
+ "input": "Rasta - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "14",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4695
+ },
+ {
+ "id": 121,
+ "input": "Rasta - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "14",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4592
+ },
+ {
+ "id": 122,
+ "input": "Rasta - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "14",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4686
+ },
+ {
+ "id": 123,
+ "input": "Rasta - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "14",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4583
+ },
+ {
+ "id": 124,
+ "input": "Rasta - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "14",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4674
+ },
+ {
+ "id": 125,
+ "input": "Rasta - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "14",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4571
+ },
+ {
+ "id": 126,
+ "input": "Street - Theme A",
+ "sansEnv": false,
+ "version": {
+ "part": "15",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4920
+ },
+ {
+ "id": 127,
+ "input": "Street - Theme A (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "15",
+ "theme": "A"
+ },
+ "output": " ",
+ "length": 4820
+ },
+ {
+ "id": 128,
+ "input": "Street - Theme B",
+ "sansEnv": false,
+ "version": {
+ "part": "15",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4935
+ },
+ {
+ "id": 129,
+ "input": "Street - Theme B (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "15",
+ "theme": "B"
+ },
+ "output": " ",
+ "length": 4832
+ },
+ {
+ "id": 130,
+ "input": "Street - Theme C",
+ "sansEnv": false,
+ "version": {
+ "part": "15",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4914
+ },
+ {
+ "id": 131,
+ "input": "Street - Theme C (no background)",
+ "sansEnv": true,
+ "version": {
+ "part": "15",
+ "theme": "C"
+ },
+ "output": " ",
+ "length": 4811
+ }
+]
\ No newline at end of file