diff --git a/src/main/java/tech/nevets/vcardgen/Card.java b/src/main/java/tech/nevets/vcardgen/Card.java index d81afd3..6aa8eac 100644 --- a/src/main/java/tech/nevets/vcardgen/Card.java +++ b/src/main/java/tech/nevets/vcardgen/Card.java @@ -10,13 +10,16 @@ import javax.imageio.stream.ImageOutputStream; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; -import java.util.HashMap; -import java.util.Map; +import java.util.*; public class Card { + public static final Map CARD_SESSIONS = new HashMap<>(); + + // ---------------- COLORS ---------------- // private static final Color WHITE = new Color(255, 255, 255); private static final Color GREEN = new Color(101, 142, 61); + // ---------------- FONTS ---------------- // private static final Font ARIAL65 = new Font("Arial", Font.PLAIN, 65); private static final Font ARIAL55 = new Font("Arial", Font.PLAIN, 55); private static final Font ARIAL45 = new Font("Arial", Font.PLAIN, 45); @@ -25,106 +28,216 @@ public class Card { private static final Font ARIAL40I = new Font("Arial", Font.ITALIC, 40); private static final Font ARIAL38I = new Font("Arial", Font.ITALIC, 38); - private static final String[] KEYS = {"Name", "Title", "Email", "Location", "Address", "SchoolNumber", "Extension", "DirectNumber", "CellNumber"}; + // ---------------- LAYERS ---------------- // + private BufferedImage background; + private BufferedImage nameLayer = new BufferedImage(975, 90, BufferedImage.TYPE_INT_ARGB); + private BufferedImage titleLayer = new BufferedImage(975, 55, BufferedImage.TYPE_INT_ARGB); + private BufferedImage emailLayer = new BufferedImage(1000, 55, BufferedImage.TYPE_INT_ARGB); + private BufferedImage locationLayer = new BufferedImage(700, 134, BufferedImage.TYPE_INT_ARGB); + private BufferedImage phoneNumbersLayer = new BufferedImage(700, 90, BufferedImage.TYPE_INT_ARGB); + // ---------------- Working Images ---------------- // + private Graphics2D graphics; private BufferedImage rawImage = new BufferedImage(1080, 602, BufferedImage.TYPE_INT_ARGB); private BufferedImage workingImage = new BufferedImage(1080, 602, BufferedImage.TYPE_INT_ARGB); private IIOImage finalImage; - private final String[] data = new String[9]; - private final boolean[] flags = new boolean[3]; // 0 - Long Address, 1 - Has Direct Number, 2 - Has Cell Phone - private final BufferedImage background; - private final int size; + // ---------------- DATA ---------------- // + public String id; + private String name; + private String title; + private String email; + private Location location; + private String extension; + private String directNumber; + private String cellNumber; + private int size; + private boolean hasLongAddress; + private boolean hasDirectNumber; + private boolean hasCellNumber; - public Card(JsonObject json) { - this.data[0] = json.get("name").getAsString(); //name - this.data[1] = json.get("title").getAsString(); //title - this.data[2] = json.get("email").getAsString(); //email - Location location = Location.getLocation(json.get("locationId").getAsString()); - this.data[3] = location.getName(); //locationName - this.data[4] = location.getAddress(); //address - this.data[5] = location.getNumber(); //schoolNumber - background = location.getBackground(); - this.data[6] = json.get("extension").getAsString(); //extension - this.data[7] = json.get("directNumber").getAsString(); //directNumber - this.data[8] = json.get("cellNumber").getAsString(); //cellNumber - this.flags[1] = this.data[7].length() > 0; //hasDirectNumber - this.flags[2] = this.data[8].length() > 0; //hasCellNumber - size = json.get("size").getAsInt(); - renderImage(); - resizeImage(); - try { - addMetadata(); - } catch (IOException e) { - System.out.println("Error adding metadata"); - e.printStackTrace(); - } + /** + * Constructs an empty card.
+ * Calls:
{@link tech.nevets.vcardgen.Card#Card(String, String, String, String, String, String, String, int)} with empty strings and full size image + */ + public Card() { + this("", "", "", "", "", "", "", 1); } - public Card(String name, String title, String email, String locationId, String extension, String directNumber, String cellNumber, int size) { - this.data[0] = name; //name - this.data[1] = title; //title - this.data[2] = email; //email - Location location = Location.getLocation(locationId); - this.data[3] = location.getName(); //locationName - this.data[4] = location.getAddress(); //address - this.data[5] = location.getName(); //schoolNumber - background = location.getBackground(); - this.data[6] = extension; //extension - this.data[7] = directNumber; //directNumber - this.data[8] = cellNumber; //cellNumber - this.flags[1] = this.data[7].length() > 0; //hasDirectNumber - this.flags[2] = this.data[8].length() > 0; //hasCellNumber - this.size = size; + /** + * Constructor that fills out entire Card details.
+ * Calls:
{@link tech.nevets.vcardgen.Card#Card(String, String, String, String, String, String, String, int)} + * @param json Json object from frontend + */ + public Card(JsonObject json) { + this( + json.get("name").getAsString(), //name + json.get("title").getAsString(), //title + json.get("email").getAsString(), //email + json.get("locationId").getAsString(), //locationId + json.get("extension").getAsString(), //extension + json.get("directNumber").getAsString(), //directNumber + json.get("cellNumber").getAsString(), //cellNumber + json.get("size").getAsInt() //size + ); + } - renderImage(); - resizeImage(); - try { - addMetadata(); - } catch (IOException e) { - System.out.println("Error adding metadata"); - e.printStackTrace(); + /** + * Constructor that fills out entire Card details. + * @param name User's Name + * @param title User's Title + * @param email User's Email + * @param locationId User's Location ID + * @param extension User's Extension (can be empty if {@link tech.nevets.vcardgen.Card#directNumber} is populated) + * @param directNumber User's Direct Number (can be empty if {@link tech.nevets.vcardgen.Card#extension} is populated) + * @param cellNumber User's Cell Phone Number (can be empty) + * @param size Size to set the Card to (0 for Outlook 2016 size, 1 for full size) + */ + public Card(String name, String title, String email, String locationId, String extension, String directNumber, String cellNumber, int size) { + this.id = getUniqueId(); + this.name = name; + this.title = title; + this.email = email; + Location location = Location.getLocation(locationId); + background = location.getBackground(); + this.extension = extension; + this.directNumber = directNumber; + this.cellNumber = cellNumber; + this.hasDirectNumber = this.directNumber.length() > 0; + this.hasCellNumber = this.cellNumber.length() > 0; + this.size = size; + CARD_SESSIONS.put(id, this); + } + + public void setName(String name) { + this.name = name; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setLocation(Location location) { + this.location = location; + } + + public void setExtension(String extension) { + this.extension = extension; + hasDirectNumber = false; + this.directNumber = ""; + } + + public void setDirectNumber(String directNumber) { + this.directNumber = directNumber; + hasDirectNumber = directNumber.length() > 0; + this.extension = ""; + } + + public void setCellNumber(String cellNumber) { + this.cellNumber = cellNumber; + hasCellNumber = cellNumber.length() > 0; + } + + public BufferedImage getBackground() { + return background; + } + + private BufferedImage renderLayer(BufferedImage layer, String content, Color textColor, Font font) { + return renderLayer(layer, content, textColor, font, 0, 0); + } + + private BufferedImage renderLayer(BufferedImage layer, String content, Color textColor, Font font, int x, int y) { + graphics = layer.createGraphics(); + setAntiAlias(graphics); + graphics.setColor(textColor); + graphics.drawString(content, x, y); + graphics.dispose(); + return layer; + } + + private BufferedImage renderResizableLayer(BufferedImage layer, String content, Color textColor, int maxLength, Font defaultFont, Font smallFont) { + graphics = layer.createGraphics(); + setAntiAlias(graphics); + graphics.setColor(textColor); + if (fitsDimensions(content, defaultFont, maxLength)) graphics.setFont(defaultFont); + else graphics.setFont(smallFont); + graphics.drawString(content, 0, 0); + graphics.dispose(); + return layer; + } + + public BufferedImage renderNameLayer() { + return renderResizableLayer(nameLayer, name, WHITE, 970, ARIAL65, ARIAL55); + } + + public BufferedImage renderTitleLayer() { + return renderResizableLayer(titleLayer, title, WHITE, 970, ARIAL45I, ARIAL40I); + } + + public BufferedImage renderEmailLayer() { + return renderLayer(emailLayer, email, WHITE, ARIAL45); + } + + public BufferedImage renderLocationLayer() { + StringBuilder sb = new StringBuilder(); + String address = location.getAddress(); + String[] splitAddr = address.split(","); + if (fitsDimensions(address, ARIAL38I, 700)) { + sb.append(address); + } else { + int lineOneLength = 0; + int newLineIndex = 0; + for (String addrPart : splitAddr) { + if (fitsDimensions(addrPart, ARIAL38I, (700 - lineOneLength))) { + sb.append(addrPart).append(","); + lineOneLength += stringWidth(addrPart, ARIAL38I); + newLineIndex += addrPart.length(); + } else { + sb.append(addrPart).append(","); + } + } + sb.delete(newLineIndex - 2, newLineIndex); + sb.insert(newLineIndex - 2, "\n"); + sb.deleteCharAt(sb.length() - 1); } + renderLayer(locationLayer, location.getName(), GREEN, ARIAL44I); + return renderLayer(locationLayer, sb.toString(), GREEN, ARIAL38I, 0, 44); + } + + public BufferedImage renderPhoneNumbersLayer() { + return null; } private void renderImage() { - Graphics2D g = workingImage.createGraphics(); - g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + graphics = rawImage.createGraphics(); + setAntiAlias(graphics); - g.drawImage(background, 0, 0, null); + graphics.drawImage(background, 0, 0, null); + graphics.drawImage(renderNameLayer(), 85, 112, null); + graphics.drawImage(renderTitleLayer(), 89, 176, null); - g.setColor(WHITE); - if (fitsDimensions(data[0], ARIAL65, 969)) g.setFont(ARIAL65); - else g.setFont(ARIAL55); - g.drawString(data[0], 85, 112); + int doubleNumOffset = hasDirectNumber ? 40 : 0; + int longAddrOffset = fitsDimensions(location.getAddress(), ARIAL38I, 700) ? 0 : 40; - if (fitsDimensions(data[1], ARIAL45I, 970)) g.setFont(ARIAL45I); - else g.setFont(ARIAL40I); - g.drawString(data[1], 89, 176); + graphics.drawImage(renderEmailLayer(), 62, (380 - doubleNumOffset - longAddrOffset), null); - int doubleNumOffset = 0; - if (flags[2]) { - doubleNumOffset = 40; - } + graphics.setColor(GREEN); + graphics.setFont(ARIAL44I); + graphics.drawString(location.getName(), 59, 447 - doubleNumOffset - longAddrOffset); - int longAddrOffset = fitsDimensions(data[4], ARIAL38I, 694) ? 0 : 40; - - g.setFont(ARIAL45); - g.drawString(data[2], 62, 380 - doubleNumOffset - longAddrOffset); - - g.setColor(GREEN); - g.setFont(ARIAL44I); - g.drawString(data[3], 59, 447 - doubleNumOffset - longAddrOffset); - - if (!fitsDimensions(data[4], ARIAL38I, 694)) { + if (!fitsDimensions(location.getAddress(), ARIAL38I, 694)) { StringBuilder addrLineOne = new StringBuilder(); StringBuilder addrLineTwo = new StringBuilder(); - String[] splitAddr = data[4].split(","); + String[] splitAddr = location.getAddress().split(","); int i = 0; for (int width = 0; width < 694; i++) { - int splitSize = g.getFontMetrics().stringWidth(splitAddr[i]); + int splitSize = graphics.getFontMetrics().stringWidth(splitAddr[i]); if ((width + splitSize) < 694) { width += splitSize; addrLineOne.append(splitAddr[i]); @@ -141,21 +254,21 @@ public class Card { } addrLineTwo.deleteCharAt(addrLineTwo.length() - 1); - g.setFont(ARIAL38I); - g.drawString(addrLineOne.toString().trim(), 59, 491 - doubleNumOffset - longAddrOffset); - g.drawString(addrLineTwo.toString().trim(), 59, 491 - doubleNumOffset); + graphics.setFont(ARIAL38I); + graphics.drawString(addrLineOne.toString().trim(), 59, 491 - doubleNumOffset - longAddrOffset); + graphics.drawString(addrLineTwo.toString().trim(), 59, 491 - doubleNumOffset); } else { - g.drawString(data[4], 59, 491 - doubleNumOffset); + graphics.drawString(location.getAddress(), 59, 491 - doubleNumOffset); } String number; - int numY = flags[2] ? 496 : 540; - if (!flags[1]) number = "W: " + data[5] + " x" + data[6]; - else number = "W: " + data[7]; - g.drawString(number, 59, numY); - if (flags[2]) g.drawString("C: " + data[8], 59, 540); + int numY = hasDirectNumber ? 496 : 540; + if (!hasDirectNumber) number = "W: " + location.getNumber() + " x" + extension; + else number = "W: " + directNumber; + graphics.drawString(number, 59, numY); + if (hasCellNumber) graphics.drawString("C: " + cellNumber, 59, 540); - g.dispose(); + graphics.dispose(); rawImage = workingImage; } @@ -192,12 +305,12 @@ public class Card { finalImage = reader.readAll(0, null); IIOMetadataNode text = new IIOMetadataNode("tEXt"); - for (int i = 0; i < KEYS.length; i++) { - IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); - textEntry.setAttribute("keyword", KEYS[i]); - textEntry.setAttribute("value", data[i]); - text.appendChild(textEntry); - } +// for (int i = 0; i < KEYS.length; i++) { +// IIOMetadataNode textEntry = new IIOMetadataNode("tEXtEntry"); +// textEntry.setAttribute("keyword", KEYS[i]); +// textEntry.setAttribute("value", data[i]); +// text.appendChild(textEntry); +// } IIOMetadataNode root = new IIOMetadataNode("javax_imageio_png_1.0"); root.appendChild(text); @@ -213,10 +326,17 @@ public class Card { return baos.toByteArray(); } + private void setAntiAlias(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + } + private static final Canvas C = new Canvas(); private static boolean fitsDimensions(String text, Font font, int maxLength) { return C.getFontMetrics(font).stringWidth(text) < maxLength; } + private static int stringWidth(String text, Font font) { + return C.getFontMetrics(font).stringWidth(text); + } public static String getDataFromVCard(InputStream rawImage) throws IOException { ImageReader reader = ImageIO.getImageReadersByMIMEType("image/png").next(); @@ -246,4 +366,15 @@ public class Card { return new Gson().toJson(keyValueMap); } + + private static String getUniqueId() { + String uuid = UUID.randomUUID().toString().split("-")[4]; + while (true) { + String finalUuid = uuid; + if (CARD_SESSIONS.entrySet().stream().noneMatch(card -> card.getKey().equals(finalUuid))) { + return uuid; + } + uuid = UUID.randomUUID().toString().split("-")[4]; + } + } } diff --git a/src/main/java/tech/nevets/vcardgen/GenerateRoute.java b/src/main/java/tech/nevets/vcardgen/GenerateRoute.java index 77e3cd4..81bb579 100644 --- a/src/main/java/tech/nevets/vcardgen/GenerateRoute.java +++ b/src/main/java/tech/nevets/vcardgen/GenerateRoute.java @@ -7,9 +7,10 @@ import spark.Request; import spark.Response; import spark.Route; +import java.io.IOException; + public class GenerateRoute implements Route { private Card card; - @Override public Object handle(Request req, Response res) { JsonObject data; @@ -17,11 +18,18 @@ public class GenerateRoute implements Route { data = new Gson().fromJson(req.body(), JsonObject.class); } catch (JsonSyntaxException e) { res.status(422); + return "Not valid json: " + e.getMessage(); } res.type("image/png"); res.header("Access-Control-Expose-Headers", "Id"); card = new Card(data); - return new Card(data).toByteArray(); + try { + res.status(200); + return new Card(data).toByteArray(); + } catch (IOException e) { + res.status(500); + return "Error getting image stream: " + e.getMessage(); + } } } diff --git a/src/main/java/tech/nevets/vcardgen/LiveGenWebSocket.java b/src/main/java/tech/nevets/vcardgen/LiveGenWebSocket.java new file mode 100644 index 0000000..ed84b64 --- /dev/null +++ b/src/main/java/tech/nevets/vcardgen/LiveGenWebSocket.java @@ -0,0 +1,85 @@ +package tech.nevets.vcardgen; + +import org.eclipse.jetty.websocket.api.*; +import org.eclipse.jetty.websocket.api.annotations.*; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +@WebSocket +public class LiveGenWebSocket { + + private Card card; + + @OnWebSocketConnect + public void connected(Session session) {} + + @OnWebSocketClose + public void closed(Session session, int statusCode, String reason) {} + + @OnWebSocketMessage + public void message(Session session, String message) throws IOException { + String[] splitMsg = message.split(";"); + String messageType = splitMsg[0]; + String messageContent = splitMsg[1]; + + RemoteEndpoint client = session.getRemote(); + + switch (messageType) { + case "start" -> { + card = new Card(); + client.sendString("success;Card successfully created."); + } + case "continue" -> { + card = Card.CARD_SESSIONS.get(messageContent); + if (card == null) { + card = new Card(); + client.sendString("notExist;Card with that id does not exist, a new card has been created."); + } else { + client.sendString("success;Card loaded successfully."); + } + } + case "location" -> { + card.setLocation(Location.getLocation(messageContent)); + client.sendString("background"); + client.sendBytes(byteBufferFromImage(card.getBackground())); + client.sendString("location"); + client.sendBytes(byteBufferFromImage(card.renderLocationLayer())); + } + case "name" -> { + + } + case "title" -> { + + } + case "email" -> { + + } + case "extension" -> { + + } + case "directNumber" -> { + + } + case "cellNumber" -> { + + } + default -> session.getRemote().sendString("Unknown Message Type: " + messageType); + } + } + + private ByteBuffer byteBufferFromImage(BufferedImage image) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + ImageIO.write(image, "png", baos); + } catch (IOException e) { + e.printStackTrace(); + return ByteBuffer.wrap(("Error writing image: " + e.getMessage()).getBytes(StandardCharsets.UTF_8)); + } + return ByteBuffer.wrap(baos.toByteArray()); + } +} diff --git a/src/main/java/tech/nevets/vcardgen/Location.java b/src/main/java/tech/nevets/vcardgen/Location.java index 59e509c..c00d641 100644 --- a/src/main/java/tech/nevets/vcardgen/Location.java +++ b/src/main/java/tech/nevets/vcardgen/Location.java @@ -55,6 +55,11 @@ public class Location { background = ImageIO.read(backgroundURL); } + @Override + public String toString() { + return "{ \"id\": " + id + ", \"name\": " + name + ", \"address\": " + address + ", \"number\": " + number + " }"; + } + public static void loadLocations () { try { Gson gson = new Gson(); @@ -87,9 +92,5 @@ public class Location { return null; } - @Override - public String toString() { - return "{ \"id\": " + id + ", \"name\": " + name + ", \"address\": " + address + ", \"number\": " + number + " }"; - } } \ No newline at end of file diff --git a/src/main/java/tech/nevets/vcardgen/Main.java b/src/main/java/tech/nevets/vcardgen/Main.java index 56b696c..a565964 100644 --- a/src/main/java/tech/nevets/vcardgen/Main.java +++ b/src/main/java/tech/nevets/vcardgen/Main.java @@ -1,8 +1,5 @@ package tech.nevets.vcardgen; -import com.google.gson.Gson; -import com.google.gson.JsonObject; - import java.io.*; import static spark.Spark.*; @@ -12,6 +9,9 @@ public class Main { Location.loadLocations(); port(8080); + + webSocket("/backend/generate/live", LiveGenWebSocket.class); + path("/backend", () -> { before("/*", (request, response) -> response.header("Access-Control-Allow-Origin", "*")); @@ -25,7 +25,6 @@ public class Main { return "OK"; }); - get("/heartbeat", (req, res) -> { res.status(200); res.type("application/json"); @@ -38,6 +37,7 @@ public class Main { return new FileInputStream("locations.json"); }); + post("/generate", new GenerateRoute()); post("/edit", new EditRoute()); diff --git a/src/main/java/tech/nevets/vcardgen/TestMain.java b/src/main/java/tech/nevets/vcardgen/TestMain.java index d58bbc2..3544aa8 100644 --- a/src/main/java/tech/nevets/vcardgen/TestMain.java +++ b/src/main/java/tech/nevets/vcardgen/TestMain.java @@ -2,5 +2,6 @@ package tech.nevets.vcardgen; public class TestMain { public static void main(String[] args) { + } }