package magic.cardBuilder.renderers; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.Transparency; import java.awt.font.FontRenderContext; import java.awt.font.GraphicAttribute; import java.awt.font.ImageGraphicAttribute; import java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.awt.image.BufferedImage; import java.awt.image.WritableRaster; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.util.ArrayList; import java.util.Collection; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import magic.awt.MagicFont; import magic.cardBuilder.ResourceManager; import magic.cardBuilder.CardResource; import magic.model.IRenderableCard; import magic.model.MagicColor; import magic.model.MagicType; import magic.ui.MagicImages; import magic.ui.helpers.ImageHelper; public class OracleText { public static final char NEWLINE = '\n'; private static final int maxDistance = 260100; private static final double transparencyTolerance = 0.001; private static final int padding = 1; private static final Font cardTextFont = MagicFont.MPlantin.get().deriveFont(Font.PLAIN, 18);//scale down when long string private static final int topPadding = 7; private static final int leftPadding = 3; static void drawOracleText(BufferedImage cardImage, IRenderableCard cardDef) { Rectangle textBoxBounds = getTextBoxSize(cardDef); if (cardDef.hasText()) { //29,327 = top left textbox position. int xPos = 30; int yPos = getYPosition(cardDef); drawTextToCard( cardImage, xPos, yPos, cardDef.getText(), textBoxBounds ); } else if (cardDef.hasType(MagicType.Land)) { //Currently ignoring colorless basic lands with no type - no watermark for colorless yet Set<MagicColor> landColors = Frame.getLandColors(cardDef); if (landColors.equals(Frame.getBasicLandColors(cardDef))) { BufferedImage landImage = null; if (landColors.size() == 1) { landImage = getLandImage(landColors.iterator().next()); } else if (landColors.size() == 2) { landImage = getHybridLandImage(landColors); } if (landImage != null) { Graphics2D g2d = cardImage.createGraphics(); int heightPadding = (int)((textBoxBounds.getHeight() - landImage.getHeight()) / 2); int widthPadding = (int)((textBoxBounds.getWidth() - landImage.getWidth()) / 2); g2d.drawImage(landImage, 30 + widthPadding, 327 + heightPadding, null); g2d.dispose(); } } } } static void drawPlaneswalkerOracleText(BufferedImage cardImage, IRenderableCard cardDef) { int lines = getPlaneswalkerAbilityCount(cardDef); int yPosOffset = cardDef.hasText() && lines <= 3 ? 330 : 289; String[] abilityActivation = getPlaneswalkerActivationText(cardDef); Rectangle textBoxBounds = new Rectangle(0, 0, 282, 49); for (int i = 0; i < lines; i++) { int xPos = 63; int yPos = (int)(yPosOffset + i * textBoxBounds.getHeight()); drawTextToCard( cardImage, xPos, yPos, abilityActivation[i], textBoxBounds ); } } static void drawLevellerOracleText(BufferedImage cardImage, IRenderableCard cardDef) { String[] abilities = getLevellerText(cardDef); Rectangle textBoxBounds = new Rectangle(0, 0, 185, 49); for (int i = 0; i < 3; i++) { String oracleText = abilities[i]; if (!oracleText.isEmpty()) { //Not all levels have text int xPos = i == 0 ? 30 : 104; // xpos 104 for level arrows, normal for first line int yPos = (int)(330 + i * textBoxBounds.getHeight()); drawTextToCard( cardImage, xPos, yPos, oracleText, textBoxBounds ); } } } private static int getYPosition(IRenderableCard cardDef) { if (cardDef.isToken()) { return 388; } if (cardDef.isPlaneswalker()) { return 330; } return 327; } private static Rectangle getTextBoxSize(IRenderableCard cardDef) { if (cardDef.isToken()) { return new Rectangle(0, 0, 314, 94); } if (cardDef.isPlaneswalker()) { return new Rectangle(0, 0, 282, 148); } return new Rectangle(0, 0, 314, 154); } private static BufferedImage getLandImage(MagicColor color) { switch (color) { case Black: return ResourceManager.getImage(CardResource.blackLandImage); case Blue: return ResourceManager.getImage(CardResource.blueLandImage); case Green: return ResourceManager.getImage(CardResource.greenLandImage); case Red: return ResourceManager.getImage(CardResource.redLandImage); case White: return ResourceManager.getImage(CardResource.whiteLandImage); } return null; } private static BufferedImage getHybridLandImage(Collection<MagicColor> colors) { if (colors.contains(MagicColor.Black) && colors.contains(MagicColor.Green)) { return ResourceManager.getImage(CardResource.bgLandImage); } if (colors.contains(MagicColor.Black) && colors.contains(MagicColor.Red)) { return ResourceManager.getImage(CardResource.brLandImage); } if (colors.contains(MagicColor.Green) && colors.contains(MagicColor.Blue)) { return ResourceManager.getImage(CardResource.guLandImage); } if (colors.contains(MagicColor.Green) && colors.contains(MagicColor.White)) { return ResourceManager.getImage(CardResource.gwLandImage); } if (colors.contains(MagicColor.Red) && colors.contains(MagicColor.Green)) { return ResourceManager.getImage(CardResource.rgLandImage); } if (colors.contains(MagicColor.Red) && colors.contains(MagicColor.White)) { return ResourceManager.getImage(CardResource.rwLandImage); } if (colors.contains(MagicColor.Blue) && colors.contains(MagicColor.Black)) { return ResourceManager.getImage(CardResource.ubLandImage); } if (colors.contains(MagicColor.Blue) && colors.contains(MagicColor.Red)) { return ResourceManager.getImage(CardResource.urLandImage); } if (colors.contains(MagicColor.White) && colors.contains(MagicColor.Black)) { return ResourceManager.getImage(CardResource.wbLandImage); } if (colors.contains(MagicColor.White) && colors.contains(MagicColor.Blue)) { return ResourceManager.getImage(CardResource.wuLandImage); } return null; } public static String[] getOracleAsLines(IRenderableCard cardDef) { String text = cardDef.getText(); return text.split("\n"); } static int getPlaneswalkerAbilityCount(IRenderableCard cardDef) { String[] abilities = getOracleAsLines(cardDef); return abilities.length; } static String[] getPlaneswalkerActivationText(IRenderableCard cardDef) { String[] abilities = getOracleAsLines(cardDef); String[] text = new String[abilities.length]; for (int i = 0; i < abilities.length; i++) { String[] fulltext = abilities[i].split(": ", 2); text[i] = fulltext[fulltext.length - 1]; } return text; } static String[] getLevellerText(IRenderableCard cardDef) { //Some levels contain /n as well String[] abilities = getOracleAsLines(cardDef); ArrayList<String> text = new ArrayList<>(3); for (int i = 0; i < abilities.length; i++) { if (i == 0) { text.add(abilities[0]); } else { if (abilities[i].contains("LEVEL") && abilities[i + 2].contains("LEVEL")) { //Catch empty level text text.add(""); } else { if (!abilities[i].contains("LEVEL") && !abilities[i].matches("\\d+/\\d+")) { if (i + 1 <= abilities.length - 1 && !abilities[i + 1].contains("LEVEL") && !abilities[i + 1].matches("\\d+/\\d+")) { //Catch multi-line level text text.add(abilities[i] + NEWLINE + abilities[i + 1]); } else { text.add(abilities[i]); } } } } } return text.toArray(new String[3]); } static void drawTextToCard( BufferedImage cardImage, int xPos, int yPos, String oracleText, Rectangle textBoxBounds) { AttributedString realOracle = textIconReplace(oracleText); BufferedImage textBoxText = drawTextToBox( realOracle, cardTextFont, textBoxBounds, leftPadding, topPadding ); Graphics2D g2d = cardImage.createGraphics(); BufferedImage trimmedTextBox = trimTransparency(textBoxText); int heightPadding = (int)((textBoxBounds.getHeight() - trimmedTextBox.getHeight()) / 2); int widthPadding = (int)Math.min((textBoxBounds.getWidth() - trimmedTextBox.getWidth()) / 2, 3); g2d.drawImage(trimmedTextBox, xPos + widthPadding, yPos + heightPadding, null); g2d.dispose(); } private static SortedMap<Float, TextLayout> tryTextLayout( AttributedString attrString, FontRenderContext frc, Rectangle box, int leftPadding, int topPadding ) { final SortedMap<Float, TextLayout> lines = new TreeMap<>(); AttributedCharacterIterator text = attrString.getIterator(); int paragraphStart = text.getBeginIndex(); int paragraphEnd = text.getEndIndex(); LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(text, frc); float boxWidth = (float)box.getWidth(); float boxHeight = (float)box.getHeight(); float posY = topPadding; lineMeasurer.setPosition(paragraphStart); //Measure length of string to fit in box final AttributedCharacterIterator iter = attrString.getIterator(); while (lineMeasurer.getPosition() < paragraphEnd) { //Check for ptPanel overlap int next = lineMeasurer.nextOffset(posY >= 123 ? boxWidth - (leftPadding << 1) - 100 : boxWidth - (leftPadding << 1)); int limit = next; //Check for newlines for (int i = lineMeasurer.getPosition(); i < next; ++i) { char c = iter.setIndex(i); if (c == NEWLINE && i > lineMeasurer.getPosition()) { limit = i; break; } } //get+draw measured length TextLayout layout = lineMeasurer.nextLayout(boxWidth, limit, false); posY += layout.getAscent(); lines.put(posY, layout); //add extra space between paragraphs if (limit < next) { posY += layout.getLeading() + layout.getDescent(); } //move to next line posY += layout.getDescent(); //check if out of room if (posY > boxHeight) { lines.clear(); break; } } return lines; } private static SortedMap<Float, TextLayout> fitTextLayout( AttributedString attrString, Font font, FontRenderContext frc, Rectangle box, int leftPadding, int topPadding ) { // decrease font by 0.5 points each time until lines can fit SortedMap<Float, TextLayout> lines = new TreeMap<>(); Font f = font; while (lines.isEmpty()) { attrString.addAttribute(TextAttribute.FONT, f); lines = tryTextLayout(attrString, frc, box, leftPadding, topPadding); f = f.deriveFont(f.getSize2D() - 0.5f); } return lines; } private static BufferedImage drawTextToBox( AttributedString attrString, Font font, Rectangle box, int leftPadding, int topPadding ) { //setUp baseImage and Graphics2D BufferedImage baseImage = ImageHelper.getCompatibleBufferedImage( (int)box.getWidth(), (int)box.getHeight(), Transparency.TRANSLUCENT); Graphics2D g2d = baseImage.createGraphics(); g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2d.setColor(Color.BLACK); FontRenderContext frc = g2d.getFontRenderContext(); // find largest font that can fit text to box final SortedMap<Float, TextLayout> lines = fitTextLayout(attrString, font, frc, box, leftPadding, topPadding); // draw the text for (final Entry<Float, TextLayout> entry : lines.entrySet()) { final TextLayout layout = entry.getValue(); final float posY = entry.getKey(); layout.draw(g2d, leftPadding, posY); } // cleanup + return g2d.dispose(); return baseImage; } private static BufferedImage trimTransparency(BufferedImage image) { WritableRaster raster = image.getAlphaRaster(); int width = raster.getWidth(); int height = raster.getHeight(); int left = 0; int top = 0; int right = width - 1; int bottom = height - 1; int minRight = width - 1; int minBottom = height - 1; top: for (;top < bottom; top++){ for (int x = 0; x < width; x++){ if (raster.getSample(x, top, 0) != 0){ minRight = x; minBottom = top; break top; } } } left: for (;left < minRight; left++){ for (int y = height - 1; y > top; y--){ if (raster.getSample(left, y, 0) != 0){ minBottom = y; break left; } } } bottom: for (;bottom > minBottom; bottom--){ for (int x = width - 1; x >= left; x--){ if (raster.getSample(x, bottom, 0) != 0){ minRight = x; break bottom; } } } right: for (;right > minRight; right--){ for (int y = bottom; y >= top; y--){ if (raster.getSample(right, y, 0) != 0){ break right; } } } return image.getSubimage(left, top, right - left + 1, bottom - top + 1); } public static AttributedString textIconReplace(final String text) { final String compacted = text.replaceAll("\\{[^}]+}", "M"); final AttributedString attrString = new AttributedString(compacted); for (int i = 0, j = 0; i < text.length(); i++, j++) { char c = text.charAt(i); if (c == '{') { final int endSymbol = text.indexOf('}', i); // get mana-string, substring returns at -1 value String iconString = text.substring(i, endSymbol + 1); // get related Icon as Image Image iconImage = MagicImages.getIcon(iconString).getImage(); // define replacement icon ImageGraphicAttribute icon = new ImageGraphicAttribute(iconImage, GraphicAttribute.BOTTOM_ALIGNMENT); // replace M with icon attrString.addAttribute( TextAttribute.CHAR_REPLACEMENT, icon, j, j + 1 ); // advance i to the end of symbol i = endSymbol; } } return attrString; } }