// RetroPixel (2016) import java.awt.*; import java.awt.image.*; import java.io.*; import java.util.*; import javax.imageio.*; public class RetroPixel { // Variables public static final int MODE__C64_HIRES = 1; // "High" resolution, very limited colors public static final int MODE__C64_MULTICOLOR = 2; // Lower resolution but better colors public static final int MODE__PC_CGA_M4_P1 = 3; public static final int MODE__PC_EGA_16C = 4; // EGA with 16 colors palette public static final int MODE__PC_EGA_64CP = 5; // EGA with 64 colors palette and 16 of them used public static final int MODE__AMIGA_PAL = 6; public static final int MODE__AMIGA_NTSC = 7; public static final int MODE__AMIGA = 8; // Either PAL or NTSC, whatever is better for input image aspect ratio private int mode; private int pixelsize; private int width; private int height; private int[] fullpalette; private int pixelwidth; private int pixelheight; // Constructors /** * Create new instance in specific mode. * @mode One of the modes listed in this class */ public RetroPixel(int mode) { this(mode,1); } /** * Create new instance in specific mode and with multiplied pixel size. * @mode One of the modes listed in this class * @param pixelsize Increase to make final image bigger without changing generated image resolution (default 1, values like 1-4 makes most sense depending on mode and your own screen resolution) */ public RetroPixel(int mode, int pixelsize) { this.mode=mode; this.pixelsize=pixelsize; } // Accessors /** * Read image file, do retropixeling, save new image to file. */ public void generateFile(File inputfile, File outputfile) throws Exception { // Read image BufferedImage image=ImageIO.read(inputfile); // Create actual image BufferedImage retroimage=generateImage(image); // Save file, image type is read from 'outputfile' name suffix (png,jpg...) String outputfilename=outputfile.getName().toLowerCase(); int ind=outputfilename.indexOf('.'); String imagetype=outputfilename.substring(ind+1); ImageIO.write(retroimage,imagetype,outputfile); } /** * Turn image to retropixel image. */ public BufferedImage generateImage(BufferedImage sourceimage) { // Get image size int sourcewidth=sourceimage.getWidth(); int sourceheight=sourceimage.getHeight(); double sourceaspectratio=1.0*sourcewidth/sourceheight; setParams(sourceaspectratio); double retroaspectratio=1.0*(width*pixelwidth)/(height*pixelheight); // Create correct size image BufferedImage scaled=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB); Graphics2D g2d=scaled.createGraphics(); g2d.addRenderingHints(new RenderingHints(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY)); // Scale double ratioratio=retroaspectratio/sourceaspectratio; if (ratioratio>=0.9 && ratioratio<1.1) { // Allow small ~10% change in aspect ratio g2d.drawImage(sourceimage,0,0,width,height,null); } else { // Leave black borders if (sourceaspectratio>retroaspectratio) { // Original is wider, leave empty at top/bottom double d=1.0*sourcewidth/(width*pixelwidth); int useheight=(int)((sourceheight/d)/pixelheight+0.5); int empty=(height-useheight)/2; g2d.drawImage(sourceimage,0,empty,width,height-empty,0,0,sourcewidth,sourceheight,null); } else { // Original is narrower, leave empty at left/right double d=1.0*sourceheight/(height*pixelheight); int usewidth=(int)((sourcewidth/d)/pixelwidth+0.5); int empty=(width-usewidth)/2; g2d.drawImage(sourceimage,empty,0,width-empty,height,0,0,sourcewidth,sourceheight,null); } } // Change pixel colors using full palette int[] paletteinuse=fullpalette; int[][] imageusingpalettecolors=new int[height][width]; for (int y=0; y>16; int g=(limitedpalette[index]&0x00FF00)>>8; int b=(limitedpalette[index]&0x0000FF); if (r<96 && g<96 && b<96) { return false; // Dark color, assuming EHB colors will have something near this } return true; } private int ehb(int rgb) { int r=(rgb&0xFF0000)>>16; int g=(rgb&0x00FF00)>>8; int b=(rgb&0x0000FF); r/=17; g/=17; b/=17; r/=2; g/=2; b/=2; r*=17; g*=17; b*=17; return c(r,g,b); } private int getPaletteColor(int imagepixelrgb, int[] palette) { int paletteindex; if (Math.random()<0.6) { paletteindex=getPaletteColorIndex(imagepixelrgb,palette,true,(mode!=MODE__C64_HIRES)); } else { paletteindex=getPaletteColorIndex(imagepixelrgb,palette,false,false); } return paletteindex; } private int getPaletteColorIndex(int rgb, int[] palette, boolean rgb_hsb, boolean dither) { int r=(rgb&0xFF0000)>>16; int g=(rgb&0x00FF00)>>8; int b=(rgb&0x0000FF); int bestcolorindex=-1; double bestcolordiff=1.0; int secondbestcolorindex=-1; double secondbestcolordiff=1.0; for (int n=0; n0.2) { colortouse=bestcolorindex; } else { double bi=1.0/bestcolordiff; double sbi=1.0/secondbestcolordiff; double ti=bi+sbi; double rnd=Math.random()*ti; if (rnd>16; int pg=(palettecolor&0x00FF00)>>8; int pb=(palettecolor&0x0000FF); int rd=Math.abs(pr-r); int gd=Math.abs(pg-g); int bd=Math.abs(pb-b); return ((rd+gd+bd)/(3.0*255.0)); // 0..1 } private double getColorDiffByHSB(int palettecolor, int r, int g, int b) { int pr=(palettecolor&0xFF0000)>>16; int pg=(palettecolor&0x00FF00)>>8; int pb=(palettecolor&0x0000FF); float[] phsb=Color.RGBtoHSB(pr,pg,pb,null); float[] hsb=Color.RGBtoHSB(r,g,b,null); double hd=Math.abs(phsb[0]-hsb[0]); hd=Math.min(hd,1.0-hd)*2.0; double sd=Math.abs(phsb[1]-hsb[1]); double bd=Math.abs(phsb[2]-hsb[2]); if (phsb[2]==0.0 || hsb[2]==0.0) { return bd; } if (phsb[1]==0.0 || hsb[1]==0.0) { return (bd+sd)/2.0; } return (bd+sd+hd)/3.0; } private class PaletteColorUsage implements Comparable { private int paletteindex; private int count; protected PaletteColorUsage(int paletteindex) { this.paletteindex=paletteindex; count=0; } protected int getPaletteIndex() { return paletteindex; } protected void addCount() { count++; } protected int getCount() { return count; } public int compareTo(PaletteColorUsage another) { int anothercount=another.getCount(); if (count>anothercount) { return -1; } else if (count