diff --git a/src/main/java/eu/jonahbauer/raytracing/Main.java b/src/main/java/eu/jonahbauer/raytracing/Main.java index a9f3fbd..9334b10 100644 --- a/src/main/java/eu/jonahbauer/raytracing/Main.java +++ b/src/main/java/eu/jonahbauer/raytracing/Main.java @@ -1,7 +1,7 @@ package eu.jonahbauer.raytracing; import eu.jonahbauer.raytracing.render.Camera; -import eu.jonahbauer.raytracing.render.ImageIO; +import eu.jonahbauer.raytracing.render.ImageFormat; import eu.jonahbauer.raytracing.scene.Scene; import eu.jonahbauer.raytracing.scene.Sphere; @@ -17,6 +17,6 @@ public class Main { var camera = new Camera(512, 2, 16 / 9d); var image = camera.render(scene); - ImageIO.write(image, Path.of("scene-" + System.currentTimeMillis() + ".ppm")); + ImageFormat.PNG.write(image, Path.of("scene-" + System.currentTimeMillis() + ".png")); } } \ No newline at end of file diff --git a/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java new file mode 100644 index 0000000..722b9c7 --- /dev/null +++ b/src/main/java/eu/jonahbauer/raytracing/render/ImageFormat.java @@ -0,0 +1,130 @@ +package eu.jonahbauer.raytracing.render; + +import org.jetbrains.annotations.NotNull; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.CRC32; +import java.util.zip.CheckedOutputStream; +import java.util.zip.DeflaterOutputStream; + +public enum ImageFormat { + PPM { + @Override + public void write(@NotNull Image image, @NotNull OutputStream out) throws IOException { + try (var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.US_ASCII))) { + writer.write("P3\n"); + writer.write(String.valueOf(image.width())); + writer.write(" "); + writer.write(String.valueOf(image.height())); + writer.write("\n255\n"); + + var it = image.pixels().iterator(); + while (it.hasNext()) { + var color = it.next(); + writer.write(String.valueOf(color.red())); + writer.write(" "); + writer.write(String.valueOf(color.green())); + writer.write(" "); + writer.write(String.valueOf(color.blue())); + writer.write("\n"); + } + } + } + }, + PNG { + private static final byte[] MAGIC = new byte[] { (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; + private static final int IHDR_LENGTH = 13; + private static final int IHDR_TYPE = 0x49484452; + private static final int IDAT_TYPE = 0x49444154; + private static final int IEND_TYPE = 0x49454E44; + + @Override + public void write(@NotNull Image image, @NotNull OutputStream out) throws IOException { + try (var data = new NoCloseDataOutputStream(out); var _ = data.closeable()) { + data.write(MAGIC); + + writeIHDR(image, data); + writeIDAT(image, data); + writeIEND(image, data); + } + } + + private void writeIHDR(@NotNull Image image, @NotNull DataOutputStream data) throws IOException { + data.writeInt(IHDR_LENGTH); + try ( + var crc = new CheckedOutputStream(data, new CRC32()); + var ihdr = new DataOutputStream(crc) + ) { + ihdr.writeInt(IHDR_TYPE); + ihdr.writeInt(image.width()); // image width + ihdr.writeInt(image.height()); // image height + ihdr.writeByte(8); // bit depth + ihdr.writeByte(2); // color type + ihdr.writeByte(0); // compression method + ihdr.writeByte(0); // filter method + ihdr.writeByte(0); // interlace method + ihdr.flush(); + data.writeInt((int) crc.getChecksum().getValue()); + } + } + + private void writeIDAT(@NotNull Image image, @NotNull DataOutputStream data) throws IOException { + try ( + var baos = new ByteArrayOutputStream(); + var crc = new CheckedOutputStream(baos, new CRC32()); + var idat = new DataOutputStream(crc) + ) { + idat.writeInt(IDAT_TYPE); + + try (var deflate = new DataOutputStream(new DeflaterOutputStream(idat))) { + var pixels = image.pixels().iterator(); + for (int i = 0; pixels.hasNext(); i = (i + 1) % image.width()) { + if (i == 0) deflate.writeByte(0); // filter type + var pixel = pixels.next(); + deflate.writeByte(pixel.red()); + deflate.writeByte(pixel.green()); + deflate.writeByte(pixel.blue()); + } + } + + var bytes = baos.toByteArray(); + data.writeInt(bytes.length); + data.write(bytes); + data.writeInt((int) crc.getChecksum().getValue()); + } + } + + private void writeIEND(@NotNull Image image, @NotNull DataOutputStream data) throws IOException { + data.writeInt(0); + data.writeInt(IEND_TYPE); + data.writeInt(0); + } + + private static class NoCloseDataOutputStream extends DataOutputStream { + public NoCloseDataOutputStream(OutputStream out) { + super(out); + } + + @Override + public void close() { + // do nothing + } + + public Closeable closeable() { + return super::close; + } + } + }, + ; + + public void write(@NotNull Image image, @NotNull Path path) throws IOException { + try (var out = Files.newOutputStream(path)) { + write(image, out); + } + } + + public abstract void write(@NotNull Image image, @NotNull OutputStream out) throws IOException; +} diff --git a/src/main/java/eu/jonahbauer/raytracing/render/ImageIO.java b/src/main/java/eu/jonahbauer/raytracing/render/ImageIO.java deleted file mode 100644 index f8a3008..0000000 --- a/src/main/java/eu/jonahbauer/raytracing/render/ImageIO.java +++ /dev/null @@ -1,34 +0,0 @@ -package eu.jonahbauer.raytracing.render; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; - -public final class ImageIO { - private ImageIO() { - throw new UnsupportedOperationException(); - } - - public static void write(@NotNull Image image, @NotNull Path path) throws IOException { - try (var out = Files.newBufferedWriter(path)) { - out.write("P3\n"); - out.write(String.valueOf(image.width())); - out.write(" "); - out.write(String.valueOf(image.height())); - out.write("\n255\n"); - - var it = image.pixels().iterator(); - while (it.hasNext()) { - var color = it.next(); - out.write(String.valueOf(color.red())); - out.write(" "); - out.write(String.valueOf(color.green())); - out.write(" "); - out.write(String.valueOf(color.blue())); - out.write("\n"); - } - } - } -}