From 088054817a9bee323a183ba941e9e4a5ed817c67 Mon Sep 17 00:00:00 2001 From: Klemek Date: Sat, 21 Apr 2018 14:34:36 +0200 Subject: [PATCH] Initial commit --- .gitattributes | 2 + .gitignore | 5 + README.md | 1 + src/META-INF/MANIFEST.MF | 3 + src/fr/klemek/marble/Color.java | 86 +++++++++ src/fr/klemek/marble/Generator.java | 190 +++++++++++++++++++ src/fr/klemek/marble/ImageUtils.java | 87 +++++++++ src/fr/klemek/marble/LocalTests.java | 12 ++ src/fr/klemek/marble/MarbleViewer.java | 44 +++++ src/fr/klemek/marble/Utils.java | 91 +++++++++ src/fr/klemek/marble/WallpaperGenerator.java | 73 +++++++ test/fr/klemek/marble/UtilsTest.java | 72 +++++++ 12 files changed, 666 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 README.md create mode 100644 src/META-INF/MANIFEST.MF create mode 100644 src/fr/klemek/marble/Color.java create mode 100644 src/fr/klemek/marble/Generator.java create mode 100644 src/fr/klemek/marble/ImageUtils.java create mode 100644 src/fr/klemek/marble/LocalTests.java create mode 100644 src/fr/klemek/marble/MarbleViewer.java create mode 100644 src/fr/klemek/marble/Utils.java create mode 100644 src/fr/klemek/marble/WallpaperGenerator.java create mode 100644 test/fr/klemek/marble/UtilsTest.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2fbb1f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.git/ +/.idea/ +/out/ +/task/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..021a3b7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# MarbleGenerator diff --git a/src/META-INF/MANIFEST.MF b/src/META-INF/MANIFEST.MF new file mode 100644 index 0000000..a591361 --- /dev/null +++ b/src/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Main-Class: fr.klemek.marble.WallpaperGenerator + diff --git a/src/fr/klemek/marble/Color.java b/src/fr/klemek/marble/Color.java new file mode 100644 index 0000000..f41efbd --- /dev/null +++ b/src/fr/klemek/marble/Color.java @@ -0,0 +1,86 @@ +package fr.klemek.marble; + +import java.util.Random; + +class Color { + final byte r; + final byte g; + final byte b; + + Color(byte r, byte g, byte b) { + this.r = r; + this.g = g; + this.b = b; + } + + Color(int r, int g, int b) { + this.r = (byte) r; + this.g = (byte) g; + this.b = (byte) b; + } + + static Color fromRgb(int r, int g, int b){ + return new Color(r + Byte.MIN_VALUE, g + Byte.MIN_VALUE, b + Byte.MIN_VALUE); + } + + static Color random(Random rand) { + return new Color( + Utils.randomByte(rand), + Utils.randomByte(rand), + Utils.randomByte(rand)); + } + + Color plus(Color other) { + return new Color(Utils.bound((short) r + other.r), + Utils.bound((short) g + other.g), + Utils.bound((short) b + other.b)); + } + + Color times(float factor) { + return new Color((byte) (r * factor), + (byte) (g * factor), + (byte) (b * factor)); + } + + static Color add(float[] factors, Color[] colors) { + float r = 0f; + float g = 0f; + float b = 0f; + for (int i = 0; i < factors.length; i++) { + r += factors[i] * colors[i].r; + g += factors[i] * colors[i].g; + b += factors[i] * colors[i].b; + } + return new Color(Utils.bound(Math.round(r)),Utils.bound(Math.round(g)), Utils.bound(Math.round(b))); + } + + static Color add(Color... colors) { + float[] factors = new float[colors.length]; + for (int i = 0; i < colors.length; i++) + factors[i] = 1f; + return add(factors, colors); + } + + Color diverge(Random rand) { + return new Color(Utils.bound(Utils.div(rand, r)), + Utils.div(rand, g), + Utils.div(rand, b)); + } + + int sum() { + return r + g + b; + } + + public java.awt.Color toColor() { + return new java.awt.Color(r - Byte.MIN_VALUE, g - Byte.MIN_VALUE, b - Byte.MIN_VALUE); + } + + @Override + public String toString() { + return toString(false); + } + + public String toString(boolean unsigned) { + return "(" + (unsigned ? r - Byte.MIN_VALUE : r) + "," + (unsigned ? g - Byte.MIN_VALUE : g) + "," + (unsigned ? b - Byte.MIN_VALUE : b) + ")"; + } +} diff --git a/src/fr/klemek/marble/Generator.java b/src/fr/klemek/marble/Generator.java new file mode 100644 index 0000000..ad09808 --- /dev/null +++ b/src/fr/klemek/marble/Generator.java @@ -0,0 +1,190 @@ +package fr.klemek.marble; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +class Generator { + + private final int width; + private final int height; + private final int width2; + private final int height2; + private final int size; + + private float slope; + private Color source; + private Color divergence; + + private long seed = 0L; + + private Color[][] table; + private Random rand; + + Generator(int width, int height) { + this(width, height, Utils.randInt(3, 12)); + } + + Generator(int width, int height, int size) { + this(width, height, size, (float) Utils.randInt(40, 60) / 100f); + } + + Generator(int width, int height, int size, float slope) { + this(width, height, size, slope, new Color(Utils.randInt(0, 30), + Utils.randInt(0, 30), + Utils.randInt(0, 30))); + } + + Generator(int width, int height, int size, float slope, Color divergence){ + this(width, height, size, slope, divergence, null); + } + + Generator(int width, int height, int size, float slope, Color divergence, Color source) { + this.width = width; + this.height = height; + this.size = size; + this.width2 = width % size == 0 ? (width / size) : (width / size) + 1; + this.height2 = height % size == 0 ? (height / size) : (height / size) + 1; + this.slope = slope; + this.divergence = divergence; + this.source = source; + } + + public Color[][] getTable() { + return table; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getWidth2() { + return width2; + } + + public int getHeight2() { + return height2; + } + + public int getSize() { + return size; + } + + void generate() { + if (seed == 0L) + seed = ThreadLocalRandom.current().nextLong(); + System.out.println("\t\tseed : " + seed); + System.out.println("\t\tsize : " + size); + System.out.println("\t\tslope : " + slope); + System.out.println("\t\tdivergence : " + divergence + " = " + divergence.sum()); + rand = new Random(seed); + table = new Color[width2][height2]; + table[0][0] = source == null ? Color.random(rand) : source; + System.out.println("\t\tsource : " + source); + for (int y = 0; y < height2; y++) + generateLine(y); + } + + private static Color getDivergence(int width, int height, int size) { + int size2 = (int) Math.min(Math.max(width / size, height / size), Byte.MAX_VALUE * 1.5f); + Color c; + do { + c = new Color(Utils.randInt(0, Byte.MAX_VALUE), + Utils.randInt(0, Byte.MAX_VALUE), + Utils.randInt(0, Byte.MAX_VALUE)); + } while (c.sum() * 2 < size2 || c.sum() > Byte.MAX_VALUE * 2); + return c; + } + + private void generateLine(int y) { + for (int x = 0; x < width2; x++) { + Color div = divergence.diverge(rand); + if (x > 0 && y == 0) { + table[x][y] = Color.add(table[x - 1][y], div); + } else if (x == 0 && y > 0) { + table[x][y] = Color.add(table[x][y - 1], div); + } else if (x > 0 && y > 0) { + table[x][y] = Color.add(new float[]{slope, 1f - slope, 1f}, new Color[]{table[x][y - 1], table[x - 1][y], div}); + } + } + } + + void inspect(int x, int y, int size, boolean unsigned) { + + System.out.println(String.format("Inspect area : %d-%d x %d-%d", x, x + size, y, y + size)); + + int sumr = 0; + int sumg = 0; + int sumb = 0; + for (int i = x; i < x + size; i++) { + for (int j = y; j < y + size; j++) { + System.out.print(String.format("%1$-12s %2$-4d ", table[i][j].toString(unsigned), table[i][j].sum())); + sumr += unsigned ? table[i][j].r - Byte.MIN_VALUE : table[i][j].r; + sumg += unsigned ? table[i][j].g - Byte.MIN_VALUE : table[i][j].g; + sumb += unsigned ? table[i][j].b - Byte.MIN_VALUE : table[i][j].b; + } + System.out.println(); + } + + System.out.println(String.format("mean : (%d,%d,%d) %d", sumr / (size * size), sumg / (size * size), sumb / (size * size), (sumr + sumg + sumb) / (size * size))); + } + + void inspectDivergence(int x0, int y0, int size) { + System.out.println(String.format("Inspect divergence in area : %d-%d x %d-%d", x0, x0 + size, y0, y0 + size)); + + int sumr = 0; + int sumg = 0; + int sumb = 0; + for (int x = x0; x < x0 + size; x++) { + for (int y = y0; y < y0 + size; y++) { + + Color div = table[x][y]; + + if (x > 0 && y == 0) { + div = Color.add(new float[]{1f,-1f},new Color[]{table[x][y], table[x - 1][y]}); + } else if (x == 0 && y > 0) { + div = Color.add(new float[]{1f,-1f},new Color[]{table[x][y], table[x][y-1]}); + } else if (x > 0 && y > 0) { + div = Color.add(new float[]{1f,-slope,slope-1f},new Color[]{table[x][y], table[x][y - 1], table[x - 1][y]}); + } + + System.out.print(String.format("%1$-12s %2$-4d ", div, div.sum())); + + sumr += div.r; + sumg += div.g; + sumb += div.b; + } + System.out.println(); + } + System.out.println(String.format("mean : (%d,%d,%d) %d", sumr / (size * size), sumg / (size * size), sumb / (size * size), (sumr + sumg + sumb) / (size * size))); + } + + byte[] getData() { + byte[] data = new byte[width * height * 3]; + int k = 0; + for (int y = 0; y < height2; y++) { + for (int x = 0; x < width2; x++) { + for (int j = 0; j < Math.min(size, width - x * size); j++) { + if (k >= data.length) { + System.err.println("\t\toverflow at x:" + x + " y:" + y + " j:" + j + " k:" + k); + return data; + } + data[k++] = table[x][y].r; + data[k++] = table[x][y].g; + data[k++] = table[x][y].b; + } + } + for (int i = 0; i < Math.min(size, height - y * size) - 1; i++) { + Utils.writeArray(data, data, k, k + width * 3, k - width * 3); + k += width * 3; + } + } + return data; + } + + +} + diff --git a/src/fr/klemek/marble/ImageUtils.java b/src/fr/klemek/marble/ImageUtils.java new file mode 100644 index 0000000..9cbbcf0 --- /dev/null +++ b/src/fr/klemek/marble/ImageUtils.java @@ -0,0 +1,87 @@ +package fr.klemek.marble; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +final class ImageUtils { + + private static final int HEADER_SIZE = 54; + + private ImageUtils() { + + } + + static byte[] generateBmpFile(int width, byte[] data) { + int height = data.length / (width * 3); + + data = fixBmpData(data, width, height); + + byte[] output = new byte[HEADER_SIZE + data.length]; + + Utils.writeArray(output, getBmpHeader(data.length, width, height), 0); + Utils.writeArray(output, data, HEADER_SIZE); + + return output; + } + + private static byte[] fixBmpData(byte[] data, int width, int height) { + if (data.length > height * width * 3) + data = Utils.subArray(data, 0, height * width * 3); + + Utils.translateUnsigned(data); + + int linePadding = (width * 3) % 4; + if (linePadding > 0) { + byte[] tail = new byte[4-linePadding]; + tail[tail.length-1] = (byte) 255; + data = Utils.interlaceArrays(data, tail, width * 3, height); + } + return data; + } + + private static byte[] getBmpHeader(int dataSize, int width, int height) { + byte[] header = new byte[HEADER_SIZE]; + + + Utils.writeArray(header, new byte[]{(int) 66, (int) 77}, 0); // BM + Utils.writeArray(header, Utils.num2bytes(dataSize + HEADER_SIZE), 2); // file size + //4 bytes application reserved + Utils.writeArray(header, Utils.num2bytes(HEADER_SIZE), 10); // offset of data + Utils.writeArray(header, Utils.num2bytes(40), 14); // real size of header + Utils.writeArray(header, Utils.num2bytes(width), 18); //width + Utils.writeArray(header, Utils.num2bytes(height), 22); //height + Utils.writeArray(header, Utils.num2bytes(1, 2), 26); //color panes + Utils.writeArray(header, Utils.num2bytes(24, 2), 28); //color depth + //4 bytes compression method + //4 bytes optional image size + Utils.writeArray(header, Utils.num2bytes(3780), 38); //horizontal resolution + Utils.writeArray(header, Utils.num2bytes(3780), 42); //vertical resolution + //4 bytes number of colors + //4 bytes number of important colors + return header; + } + + static boolean saveBmpFile(byte[] data, File bmpFile){ + try (FileOutputStream fos = new FileOutputStream(bmpFile.getPath())) { + fos.write(data); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + static boolean convertBmpToJpg(File bmpFile, File jpgFile){ + try { + BufferedImage inputImage = ImageIO.read(bmpFile); + ImageIO.write(inputImage, "JPG", jpgFile); + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/src/fr/klemek/marble/LocalTests.java b/src/fr/klemek/marble/LocalTests.java new file mode 100644 index 0000000..8a27b56 --- /dev/null +++ b/src/fr/klemek/marble/LocalTests.java @@ -0,0 +1,12 @@ +package fr.klemek.marble; + +public class LocalTests { + + public static void main(String[] args) { + Generator gen = new Generator(800, 800, 2); + gen.generate(); + + new MarbleViewer(gen); + } + +} diff --git a/src/fr/klemek/marble/MarbleViewer.java b/src/fr/klemek/marble/MarbleViewer.java new file mode 100644 index 0000000..0753c42 --- /dev/null +++ b/src/fr/klemek/marble/MarbleViewer.java @@ -0,0 +1,44 @@ +package fr.klemek.marble; + +import javax.swing.*; +import java.awt.*; + +public class MarbleViewer extends JFrame { + + public MarbleViewer(Generator generator){ + this.setLocation(0,0); + this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + this.setResizable(false); + this.add(new Panel(generator)); + this.pack(); + this.setVisible(true); + } + + private class Panel extends JPanel{ + + private Generator generator; + + Panel(Generator generator){ + this.generator = generator; + Dimension size= new Dimension(generator.getWidth(), generator.getHeight()); + this.setMinimumSize(size); + this.setPreferredSize(size); + this.setMaximumSize(size); + } + + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + + int size = generator.getSize(); + + for(int x = 0; x < generator.getWidth2(); x++){ + for(int y = 0; y < generator.getHeight2(); y++){ + g.setColor(generator.getTable()[x][y].toColor()); + g.fillRect(x*size, y*size, size, size); + } + } + } + } + +} diff --git a/src/fr/klemek/marble/Utils.java b/src/fr/klemek/marble/Utils.java new file mode 100644 index 0000000..2c91425 --- /dev/null +++ b/src/fr/klemek/marble/Utils.java @@ -0,0 +1,91 @@ +package fr.klemek.marble; + +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +final class Utils { + + private Utils() { + + } + + /* + * byte utils + */ + + static byte randomByte(Random rand) { + return (byte) (rand.nextInt(Byte.MAX_VALUE - Byte.MIN_VALUE) + Byte.MIN_VALUE); + } + + static byte bound(int value) { + if (value > Byte.MAX_VALUE) + return Byte.MAX_VALUE; + if (value < Byte.MIN_VALUE) + return Byte.MIN_VALUE; + return (byte) value; + } + + /* + * byte array utils + */ + + static byte[] subArray(byte[] array, int start, int stop) { + byte[] out = new byte[stop - start]; + for (int i = start; i < stop; i++) { + out[i - start] = array[i]; + } + return out; + } + + static void translateUnsigned(byte[] data){ + for(int i = 0; i < data.length; i++){ + data[i] = (byte) (data[i] - Byte.MIN_VALUE); + } + } + + static byte[] interlaceArrays(byte[] data, byte[] added, int size, int times){ + byte[] out = new byte[data.length + added.length*times]; + int size2 = size+added.length; + for(int i = 0; i < times; i++){ + writeArray(out, data, size2*i, size2*i+size, size*i); + writeArray(out, added, size2*i+size, size2*(i+1), 0); + } + writeArray(out, data, size2*times, out.length-size2*times, size*times); + return out; + } + + static void writeArray(byte[] out, byte[] data, int start, int stop, int padding){ + for(int i = 0; i < Math.min(stop, out.length)-start; i++){ + out[i+start] = data[i+padding]; + } + } + + static void writeArray(byte[] out, byte[] data, int start, int padding){ + writeArray(out, data, start, start+data.length-padding, padding); + } + + static void writeArray(byte[] out, byte[] data, int start){ + writeArray(out, data, start, 0); + } + + static byte[] num2bytes(int number, int nbyte){ + byte[] b = new byte[nbyte]; + for(int i = 0; i < nbyte; i++){ + b[i] = (byte)(number%256); + number = number/256; + } + return b; + } + + static byte[] num2bytes(int number){ + return num2bytes(number, 4); + } + + static int randInt(int min, int max){ + return ThreadLocalRandom.current().nextInt(max-min)+min; + } + + static int div(Random rand, byte src){ + return Math.round(rand.nextFloat()*2*src-src); + } +} diff --git a/src/fr/klemek/marble/WallpaperGenerator.java b/src/fr/klemek/marble/WallpaperGenerator.java new file mode 100644 index 0000000..d85c66d --- /dev/null +++ b/src/fr/klemek/marble/WallpaperGenerator.java @@ -0,0 +1,73 @@ +package fr.klemek.marble; + +import java.awt.*; +import java.io.File; + +class WallpaperGenerator { + + public static void main(String[] args) { + + String file = "wallpaper"; + Dimension screen = getScreenSizes(); + int width = screen.width; + int height = screen.height; + int size = 0; + + if (args.length > 0) { + file = args[0]; + } + if (args.length > 1) { + width = Integer.parseInt(args[1]); + height = Integer.parseInt(args[1]); + } + if (args.length > 2) { + height = Integer.parseInt(args[2]); + } + if (args.length > 3) { + size = Integer.parseInt(args[3]); + } + makeWallpaper(file, width, height, size); + } + + static Dimension getScreenSizes() { + Dimension dim = new Dimension(0, 0); + for (GraphicsDevice gd : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) { + dim.width = Math.max(dim.width, gd.getDisplayMode().getWidth()); + dim.height = Math.max(dim.height, gd.getDisplayMode().getHeight()); + } + return dim; + } + + static void makeWallpaper(String name, int width, int height, int size) { + System.out.println("Making wallpaper '" + name + "' " + width + "x" + height + "px"); + + File bmpFile = new File(name + ".bmp"); + File outputFile = new File(name + ".jpg"); + + long t0 = System.currentTimeMillis(); + Generator gen = size == 0 ? new Generator(width, height) : new Generator(width, height, size); + long t1 = System.currentTimeMillis(); + gen.generate(); + System.out.println("\tGeneration done in " + (System.currentTimeMillis() - t1) + " ms"); + + t1 = System.currentTimeMillis(); + byte[] file = ImageUtils.generateBmpFile(width, gen.getData()); + System.out.println("\t\t" + file.length + " bytes"); + System.out.println("\tData writing done in " + (System.currentTimeMillis() - t1) + " ms"); + + t1 = System.currentTimeMillis(); + if(!ImageUtils.saveBmpFile(file, bmpFile)) + return; + System.out.println("\tFile writing done in " + (System.currentTimeMillis() - t1) + " ms"); + + t1 = System.currentTimeMillis(); + if(!ImageUtils.convertBmpToJpg(bmpFile, outputFile)) + return; + System.out.println("\tFile converting done in " + (System.currentTimeMillis() - t1) + " ms"); + + if (bmpFile.delete()) + System.out.println("\tFile '" + bmpFile.getName() + "' deleted"); + + System.out.println("Wallpaper done in " + (System.currentTimeMillis() - t0) + " ms"); + } +} diff --git a/test/fr/klemek/marble/UtilsTest.java b/test/fr/klemek/marble/UtilsTest.java new file mode 100644 index 0000000..85dcf06 --- /dev/null +++ b/test/fr/klemek/marble/UtilsTest.java @@ -0,0 +1,72 @@ +package fr.klemek.marble; + +import static org.junit.Assert.*; + +public class UtilsTest { + + @org.junit.Test + public void bound() { + assertEquals((byte)-128, Utils.bound(-200)); + assertEquals((byte)-128, Utils.bound(-129)); + assertEquals((byte)-128, Utils.bound(-128)); + assertEquals((byte)-64, Utils.bound(-64)); + assertEquals((byte)0, Utils.bound(0)); + assertEquals((byte)64, Utils.bound(64)); + assertEquals((byte)127, Utils.bound(127)); + assertEquals((byte)127, Utils.bound(128)); + assertEquals((byte)127, Utils.bound(200)); + } + + @org.junit.Test + public void subArray() { + byte[] array0 = new byte[]{0,1,2,3,4,5,6,7,8,9}; + assertArrayEquals(new byte[]{2,3,4}, Utils.subArray(array0, 2, 5)); + } + + @org.junit.Test + public void translateUnsigned() { + byte[] array0 = new byte[]{-128,-64,0,64,127}; + Utils.translateUnsigned(array0); + assertArrayEquals(new byte[]{0,64,(byte)128,(byte)192,(byte)255}, array0); + } + + @org.junit.Test + public void interlaceArrays() { + byte[] array0 = new byte[10]; + byte[] array1 = new byte[]{1,1}; + assertArrayEquals(new byte[]{0,0,0,1,1,0,0,0,1,1,0,0,0,0}, Utils.interlaceArrays(array0, array1, 3,2)); + } + + @org.junit.Test + public void writeArray() { + byte[] array0 = new byte[10]; + byte[] array1 = new byte[]{0,1,2,3,4,5,6,7,8,9}; + Utils.writeArray(array0,array1,2,5,3); + assertArrayEquals(new byte[]{0,0,3,4,5,0,0,0,0,0}, array0); + } + + @org.junit.Test + public void writeArray1() { + byte[] array0 = new byte[10]; + byte[] array1 = new byte[]{0,1,2,3,4,5,6,7,8,9}; + Utils.writeArray(array0,array1,2,3); + assertArrayEquals(new byte[]{0,0,3,4,5,6,7,8,9,0}, array0); + } + + @org.junit.Test + public void writeArray2() { + byte[] array0 = new byte[10]; + byte[] array1 = new byte[]{0,1,2,3,4,5,6,7,8,9}; + Utils.writeArray(array0,array1,2); + assertArrayEquals(new byte[]{0,0,0,1,2,3,4,5,6,7}, array0); + } + + @org.junit.Test + public void num2bytes() { + } + + @org.junit.Test + public void num2bytes1() { + } + +} \ No newline at end of file