package wordcloud; import ch.lambdaj.Lambda; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; import wordcloud.bg.Background; import wordcloud.bg.RectangleBackground; import wordcloud.collide.RectanglePixelCollidable; import wordcloud.collide.checkers.CollisionChecker; import wordcloud.collide.checkers.RectangleCollisionChecker; import wordcloud.collide.checkers.RectanglePixelCollisionChecker; import wordcloud.font.CloudFont; import wordcloud.font.FontWeight; import wordcloud.font.scale.FontScalar; import wordcloud.font.scale.LinearFontScalar; import wordcloud.image.AngleGenerator; import wordcloud.image.CollisionRaster; import wordcloud.image.ImageRotation; import wordcloud.padding.Padder; import wordcloud.padding.RectanglePadder; import wordcloud.padding.WordPixelPadder; import wordcloud.palette.ColorPalette; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import static ch.lambdaj.Lambda.on; /** * Created by kenny on 6/29/14. */ public class WordCloud { private static final Logger LOGGER = Logger.getLogger(WordCloud.class); protected final int width; protected final int height; protected CollisionMode collisionMode; protected CollisionChecker collisionChecker; protected Padder padder; protected int padding = 0; protected Background background; protected final RectanglePixelCollidable backgroundCollidable; protected Color backgroundColor = Color.BLACK; protected FontScalar fontScalar = new LinearFontScalar(10, 40); protected CloudFont cloudFont = new CloudFont("Comic Sans MS", FontWeight.BOLD); private CloudFont sansFont = new CloudFont(Font.SANS_SERIF, FontWeight.BOLD); protected AngleGenerator angleGenerator = new AngleGenerator(); protected final CollisionRaster collisionRaster; protected final BufferedImage bufferedImage; protected final ArrayList<Word> placedWords = new ArrayList<>(); protected final ArrayList<Word> skipped = new ArrayList<>(); protected boolean fitAll = false; protected ColorPalette colorPalette = new ColorPalette(Color.ORANGE, Color.WHITE, Color.YELLOW, Color.GRAY, Color.GREEN); public WordCloud(int width, int height, CollisionMode collisionMode) { this(width, height, collisionMode, false); } public WordCloud(int width, int height, CollisionMode collisionMode, boolean fitAll) { this.width = width; this.height = height; this.collisionMode = collisionMode; this.fitAll = fitAll; switch(collisionMode) { case PIXEL_PERFECT: this.padder = new WordPixelPadder(); this.collisionChecker = new RectanglePixelCollisionChecker(); break; case RECTANGLE: default: this.padder = new RectanglePadder(); this.collisionChecker = new RectangleCollisionChecker(); break; } this.collisionRaster = new CollisionRaster(width, height); this.bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); this.backgroundCollidable = new RectanglePixelCollidable(collisionRaster, 0, 0); this.background = new RectangleBackground(width, height); } private static String createProgress(int progress) { int a = (progress * 20) / 100; int b = 20 - a; return "[" + StringUtils.leftPad("", a, '=') + StringUtils.leftPad("", b, ' ') + "]"; } public void build(List<WordFrequency> wordFrequencies) { Collections.sort(wordFrequencies); System.out.println("Building the cloud image ..."); List<Word> words = buildwords(wordFrequencies, this.colorPalette); int i = 0; double t = 0.0; double dt = Math.PI / 180; int total = words.size(); System.out.print("\rProcessing words " + createProgress(0)); for(Word word : words) { int startX = width / 2; int startY = height / 2; if (i > 50) { int w = width / 4; int h = height / 4; if (t >= 2 * Math.PI) t = 0; startX = (int) (w * Math.sin(5 * t) + w); startY = (int) (h * Math.sin(4 * t) + h); t += dt; } place(word, startX, startY); i++; String skip = (this.skipped.size() > 0) ? " [skipped " + this.skipped.size() + "]" : ""; System.out.print("\rProcessing words " + createProgress((i * 100)/total) + " " + i + " / " + total + " (" + (i * 100)/total + "%)" + skip); } System.out.println("\r\nProcessing words. DONE"); } public boolean fill(List<WordFrequency> wordFrequencies, int pass) { this.padder = new WordPixelPadder(); this.collisionChecker = new RectanglePixelCollisionChecker(); List<Word> skipped = new ArrayList<>(this.skipped); this.skipped.clear(); int total = skipped.size(); if (total == 0) return true; System.out.print("\rProcessing skipped (pass " + (pass) + ") " + createProgress(0)); int j = 0; for (Word sw : skipped) { int freq = sw.getFrequency() / 2; if (freq <= 0) { freq = 1; } Word word = buildWord(new WordFrequency(sw.getId(), sw.getWord(), sw.getFilter(), freq), maxFrequency(wordFrequencies), colorPalette); int startX = (width / 2) + (width / 4); int startY = (height / 2) + (height / 4); place(word, startX, startY); j++; String skip = (this.skipped.size() > 0) ? " [skipped " + this.skipped.size() + "]" : ""; System.out.print("\rProcessing skipped (pass " + (pass) + ") " + createProgress((j * 100)/total) + " " + j + " / " + total + " (" + (j * 100)/total + "%)" + skip); } System.out.println("\r\nProcessing skipped (pass " + (pass) + "). DONE"); boolean ret = this.skipped.size() == 0; return ret; } public void fillWithOtherWords(List<WordFrequency> wordFrequencies, String[] extraWords) { System.out.println("Filling..."); int count = extraWords.length; this.fontScalar.reduceBy(2); for (int i = 0; i < count * 5; i++) { Word word = buildWord(new WordFrequency(-1, extraWords[i % 5], extraWords[i % 5], 0), maxFrequency(wordFrequencies), colorPalette); int startX = (width / 2) + (width / 4); int startY = (height / 2) + (height / 4); place(word, startX, startY); } for (int i = 0; i < count * 5; i++) { Word word = buildWord(new WordFrequency(-1, extraWords[i % 5], extraWords[i % 5], 0), maxFrequency(wordFrequencies), colorPalette); int startX = width; int startY = height; place(word, startX, startY); } for (int i = 0; i < count * 5; i++) { Word word = buildWord(new WordFrequency(-1, extraWords[i % 5], extraWords[i % 5], 0), maxFrequency(wordFrequencies), colorPalette); int startX = width / 2; int startY = height; place(word, startX, startY); } for (int i = 0; i < count * 5; i++) { Word word = buildWord(new WordFrequency(-1, extraWords[i % 5], extraWords[i % 5], 0), maxFrequency(wordFrequencies), colorPalette); int startX = width; int startY = height / 2; place(word, startX, startY); } for (int i = 0; i < count * 5; i++) { Word word = buildWord(new WordFrequency(-1, extraWords[i % 5], extraWords[i % 5], 0), maxFrequency(wordFrequencies), colorPalette); int startX = 0; int startY = height; place(word, startX, startY); } this.skipped.clear(); } public void printSkippedWords() { LOGGER.info("Cloud processing DONE. Skipped words: " + this.skipped.size()); } public void writeToImage(final String outputFileName) { String extension = ""; int i = outputFileName.lastIndexOf('.'); if (i > 0) { extension = outputFileName.substring(i + 1); } try { LOGGER.info("Saving WordCloud to " + outputFileName); ImageIO.write(bufferedImage, extension, new File(outputFileName)); } catch (IOException e) { LOGGER.error(e.getMessage(), e); } } public void writeWordsToFile(final String outputFileName, int size) { OutputStreamWriter fw = null; try { LOGGER.info("Saving Words to " + outputFileName); fw = new OutputStreamWriter(new FileOutputStream(outputFileName), Charset.forName("UTF-8").newEncoder()); fw.write(String.format("%d,%d\n", System.currentTimeMillis(), size)); for (Word word : placedWords) { fw.write(String.format("%d,%d,%d,%d,%d,%d,%s,%s|%s\n", word.getId(), word.getX(), word.getY(), word.getWidth(), word.getHeight(), word.getRotation(), word.getFontSize(), word.getWord(), word.getFilter())); } } catch (IOException e) { LOGGER.error(e.getMessage(), e); } finally { if (fw != null) { try { fw.close(); } catch (IOException e) { } } } } /** * Write to output stream as PNG * * @param outputStream the output stream to write the image data to */ public void writeToStreamAsPNG(final OutputStream outputStream) { writeToStream("png", outputStream); } /** * Write wordcloud image data to stream in the given format * * @param format the image format * @param outputStream the output stream to write image data to */ public void writeToStream(final String format, final OutputStream outputStream) { try { LOGGER.debug("Writing WordCloud image data to output stream"); ImageIO.write(bufferedImage, format, outputStream); LOGGER.debug("Done writing WordCloud image data to output stream"); } catch (IOException e) { LOGGER.error(e.getMessage(), e); throw new RuntimeException("Could not write wordcloud to outputstream due to an IOException", e); } } /** * create background, then draw current word cloud on top of it. * Doing it this way preserves the transparency of the this.bufferedImage's pixels * for a more flexible pixel perfect collision */ public void drawForgroundToBackground() { if(backgroundColor == null) { return; } final BufferedImage backgroundBufferedImage = new BufferedImage(width, height, this.bufferedImage.getType()); final Graphics graphics = backgroundBufferedImage.getGraphics(); // draw current color graphics.setColor(backgroundColor); graphics.fillRect(0, 0, width, height); graphics.drawImage(bufferedImage, 0, 0, null); // draw back to original final Graphics graphics2 = bufferedImage.getGraphics(); graphics2.drawImage(backgroundBufferedImage, 0, 0, null); } /** * try to place in center, build out in a spiral trying to place words for N steps * @param word The word to place * @param startX The x start position * @param startY The y start position * @return if the word was placed */ protected boolean place(final Word word, final int startX, final int startY) { final Graphics graphics = this.bufferedImage.getGraphics(); final int maxRadius = width; for(int r = 0; r < maxRadius; r += 2) { for(int x = -r; x <= r; x++) { if(startX + x < 0) { continue; } if(startX + x >= width) { continue; } boolean placed = false; word.setX(startX + x); // try positive root int y1 = (int) Math.sqrt(r * r - x * x); if(startY + y1 >= 0 && startY + y1 < height) { word.setY(startY + y1); placed = tryToPlace(word); } // try negative root int y2 = -y1; if(!placed && startY + y2 >= 0 && startY + y2 < height) { word.setY(startY + y2); placed = tryToPlace(word); } if(placed) { collisionRaster.mask(word.getCollisionRaster(), word.getX(), word.getY()); graphics.drawImage(word.getBufferedImage(), word.getX(), word.getY(), null); return true; } } } LOGGER.debug("skipped: " + word.getWord()); skipped.add(word); return false; } private boolean tryToPlace(final Word word) { if(!background.isInBounds(word)) { return false; } switch(this.collisionMode) { case RECTANGLE: for(Word placeWord : this.placedWords) { if(placeWord.collide(word)) { return false; } } LOGGER.debug("place: " + word.getWord()); placedWords.add(word); return true; case PIXEL_PERFECT: if(backgroundCollidable.collide(word)) { return false; } LOGGER.debug("place: " + word.getWord()); placedWords.add(word); return true; } return false; } protected List<Word> buildwords(final List<WordFrequency> wordFrequencies, final ColorPalette colorPalette) { final int maxFrequency = maxFrequency(wordFrequencies); final List<Word> words = new ArrayList<>(); for(final WordFrequency wordFrequency : wordFrequencies) { words.add(buildWord(wordFrequency, maxFrequency, colorPalette)); } return words; } private Word buildWord(final WordFrequency wordFrequency, int maxFrequency, final ColorPalette colorPalette) { final Graphics graphics = this.bufferedImage.getGraphics(); final int frequency = wordFrequency.getFrequency(); final float fontHeight = this.fontScalar.scale(frequency, 0, maxFrequency); final Font font = needSansFont(wordFrequency.getWord()) ? sansFont.getFont().deriveFont(fontHeight) : cloudFont.getFont().deriveFont(fontHeight); final FontMetrics fontMetrics = graphics.getFontMetrics(font); final Word word = new Word(wordFrequency.getId(), wordFrequency.getWord(), wordFrequency.getFilter(), colorPalette.next(), wordFrequency.getFrequency(), fontHeight, fontMetrics, this.collisionChecker); final double theta = angleGenerator.next(); if(theta != 0) { word.setBufferedImage(ImageRotation.rotate(word.getBufferedImage(), theta)); word.setRotation((int)theta); } if(padding > 0) { padder.pad(word, padding); } return word; } private int maxFrequency(final Collection<WordFrequency> wordFrequencies) { if(wordFrequencies.isEmpty()) { return 1; } return Lambda.max(wordFrequencies, on(WordFrequency.class).getFrequency()); } public void setBackgroundColor(Color backgroundColor) { this.backgroundColor = backgroundColor; } public void setPadding(int padding) { this.padding = padding; } public void setColorPalette(ColorPalette colorPalette) { this.colorPalette = colorPalette; } public void setBackground(Background background) { this.background = background; } public void setFontScalar(FontScalar fontScalar) { this.fontScalar = fontScalar; } public void setCloudFont(CloudFont cloudFont) { this.cloudFont = cloudFont; } public void setAngleGenerator(AngleGenerator angleGenerator) { this.angleGenerator = angleGenerator; } public BufferedImage getBufferedImage() { return bufferedImage; } public ArrayList<Word> getSkipped() { return skipped; } private boolean needSansFont(String src) { for (int i = 0; i < src.length(); ) { int c = src.codePointAt(i); i += Character.charCount(c); if ((Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_FORMS) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_RADICALS_SUPPLEMENT) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ENCLOSED_CJK_LETTERS_AND_MONTHS) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HANGUL_SYLLABLES) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HANGUL_JAMO) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HANGUL_JAMO_EXTENDED_A) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HANGUL_JAMO_EXTENDED_B) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC_PRESENTATION_FORMS_A) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC_PRESENTATION_FORMS_B) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.ARABIC_SUPPLEMENT) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.HEBREW) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.KAITHI) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.DEVANAGARI) || (Character.UnicodeBlock.of(c) == Character.UnicodeBlock.DEVANAGARI_EXTENDED)) { return true; } } return false; } }