diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java index 7a3fa5c..c58b8d6 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/BitmapShape.java @@ -67,6 +67,17 @@ public class BitmapShape implements Shape { } return grid.get(y).get(x); } + + @Override + public boolean isEmpty() { + boolean isEmpty = false; + for (List row : grid) { + for (Boolean plot : row) { + isEmpty |= plot; + } + } + return isEmpty; + } @Override public BitmapShape toBitmap() { diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java index 4e89b8b..2660d48 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/Shape.java @@ -1,6 +1,10 @@ package io.github.applecommander.bastools.api.shapes; public interface Shape { + /** Indicates if this shape is empty. */ + public boolean isEmpty(); + /** Transform to a BitmapShape. */ public BitmapShape toBitmap(); + /** Transform to a VectorShape. */ public VectorShape toVector(); } diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java index 5550b44..5ce8d74 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/ShapeExporter.java @@ -7,11 +7,12 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Objects; +import io.github.applecommander.bastools.api.shapes.exporters.ImageShapeExporter; import io.github.applecommander.bastools.api.shapes.exporters.TextShapeExporter; public interface ShapeExporter { /** Export a single shape to the OutputStream. */ - public void export(Shape shape, OutputStream outputStream); + public void export(Shape shape, OutputStream outputStream) throws IOException; /** Export a single shape to the File. */ public default void export(Shape shape, File file) throws IOException { Objects.requireNonNull(shape); @@ -28,7 +29,7 @@ public interface ShapeExporter { } /** Export the entire shape table to the OutputStream. */ - public void export(ShapeTable shapeTable, OutputStream outputStream); + public void export(ShapeTable shapeTable, OutputStream outputStream) throws IOException; /** Export the entire shape table to the File. */ public default void export(ShapeTable shapeTable, File file) throws IOException { Objects.requireNonNull(shapeTable); @@ -47,4 +48,7 @@ public interface ShapeExporter { public static TextShapeExporter.Builder text() { return new TextShapeExporter.Builder(); } + public static ImageShapeExporter.Builder image() { + return new ImageShapeExporter.Builder(); + } } diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java index b470ea0..a39382c 100644 --- a/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/VectorShape.java @@ -47,6 +47,11 @@ public class VectorShape implements Shape { this.vectors.add(vectorCommand); return this; } + + @Override + public boolean isEmpty() { + return vectors.isEmpty(); + } @Override public BitmapShape toBitmap() { diff --git a/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/ImageShapeExporter.java b/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/ImageShapeExporter.java new file mode 100644 index 0000000..1a61684 --- /dev/null +++ b/api/src/main/java/io/github/applecommander/bastools/api/shapes/exporters/ImageShapeExporter.java @@ -0,0 +1,175 @@ +package io.github.applecommander.bastools.api.shapes.exporters; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.stream.Collectors; + +import javax.imageio.ImageIO; + +import io.github.applecommander.bastools.api.shapes.BitmapShape; +import io.github.applecommander.bastools.api.shapes.Shape; +import io.github.applecommander.bastools.api.shapes.ShapeExporter; +import io.github.applecommander.bastools.api.shapes.ShapeTable; + +public class ImageShapeExporter implements ShapeExporter { + private int maxWidth = 1024; + private int pixelSize = 4; + private int padding = 2; + private boolean border = true; + private boolean skipEmptyShapes; + private String imageFormat = "PNG"; + + /** Use the {@code Builder} to create a ImageShapeExporter. */ + private ImageShapeExporter() { } + + @Override + public void export(Shape shape, OutputStream outputStream) throws IOException { + Objects.requireNonNull(shape); + Objects.requireNonNull(outputStream); + + export(Arrays.asList(shape.toBitmap()), outputStream); + +// int shapeWidth = pixelSize * bshape.getWidth(); +// int shapeHeight = pixelSize * bshape.getHeight(); +// int imageWidth = shapeWidth + (border ? 2 : 0); +// int imageHeight = shapeHeight + (border ? 2 : 0); +// int borderEdgeWidth = border ? 1+padding : 0; +// BufferedImage image = new BufferedImage(imageWidth + borderEdgeWidth*2, +// imageHeight + borderEdgeWidth*2, +// BufferedImage.TYPE_INT_RGB); +// +// Graphics g = image.createGraphics(); +// if (border) drawBorders(g, shapeWidth, shapeHeight, imageWidth, imageHeight); +// drawShapeAt(g, bshape, new Point(borderEdgeWidth,borderEdgeWidth)); +// g.dispose(); +// ImageIO.write(image, imageFormat, outputStream); + } + + @Override + public void export(ShapeTable shapeTable, OutputStream outputStream) throws IOException { + Objects.requireNonNull(shapeTable); + Objects.requireNonNull(outputStream); + + List blist = shapeTable.shapes.stream() + .filter(this::displayThisShape) + .map(Shape::toBitmap) + .collect(Collectors.toList()); + export(blist, outputStream); + } + + public void export(List blist, OutputStream outputStream) throws IOException { + Objects.requireNonNull(blist); + Objects.requireNonNull(outputStream); + + int shapeWidth = pixelSize * blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt(); + int shapeHeight = pixelSize * blist.stream().mapToInt(BitmapShape::getHeight).max().getAsInt(); + int borderDividerWidth = border ? 1+padding*2 : 0; + int borderEdgeWidth = border ? 1+padding : 0; + + int columns = Math.min(blist.size(), Math.max(1, this.maxWidth / shapeWidth)); + int rows = (blist.size() + columns - 1) / columns; + int imageWidth = borderEdgeWidth*2 + columns*shapeWidth + (columns-1)*borderDividerWidth; + int imageHeight = borderEdgeWidth*2 + rows*shapeHeight + (rows-1)*borderDividerWidth; + BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); + + Queue bqueue = new LinkedList<>(blist); + Graphics g = image.createGraphics(); + if (border) drawBorders(g, shapeWidth, shapeHeight, imageWidth, imageHeight); + Point pt = new Point(borderEdgeWidth, borderEdgeWidth); + while (!bqueue.isEmpty()) { + BitmapShape bshape = bqueue.remove(); + drawShapeAt(g, bshape, pt); + pt.x += shapeWidth + borderDividerWidth; + if (pt.x > imageWidth) { + pt.y += shapeHeight + borderDividerWidth; + pt.x = borderEdgeWidth; + } + } + g.dispose(); + + ImageIO.write(image, imageFormat, outputStream); + } + + private boolean displayThisShape(Shape shape) { + return !(skipEmptyShapes && shape.isEmpty()); + } + + public void drawBorders(Graphics g, int shapeWidth, int shapeHeight, int imageWidth, int imageHeight) { + g.setColor(Color.white); + int paddingWidth = border ? padding : 0; + for (int x=0; x blist = shapeTable.shapes.stream() + .filter(this::displayThisShape) .map(Shape::toBitmap) .collect(Collectors.toList()); int width = blist.stream().mapToInt(BitmapShape::getWidth).max().getAsInt(); @@ -66,6 +68,10 @@ public class TextShapeExporter implements ShapeExporter { pw.flush(); } + private boolean displayThisShape(Shape shape) { + return !(skipEmptyShapes && shape.isEmpty()); + } + private void drawTopLine(PrintWriter pw, int columns, int width) { borderStrategy.topLeftCorner(pw); borderStrategy.horizontalLine(pw, width); @@ -241,6 +247,14 @@ public class TextShapeExporter implements ShapeExporter { textShapeExporter.borderStrategy = borderStrategy; return this; } + + public Builder skipEmptyShapes() { + return skipEmptyShapes(true); + } + public Builder skipEmptyShapes(boolean skipEmptyShapes) { + textShapeExporter.skipEmptyShapes = skipEmptyShapes; + return this; + } public ShapeExporter build() { return textShapeExporter; diff --git a/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java b/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java index d3fcb64..488fc81 100644 --- a/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java +++ b/api/src/test/java/io/github/applecommander/bastools/api/shapes/ShapesTest.java @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import java.io.ByteArrayOutputStream; +import java.io.IOException; import org.junit.Test; @@ -54,7 +55,7 @@ public class ShapesTest { } @Test - public void testTextShapeExporterNoBorder() { + public void testTextShapeExporterNoBorder() throws IOException { ShapeTable st = readStandardShapeTable(); final String expected = ".XXX.\n" @@ -72,7 +73,7 @@ public class ShapesTest { } @Test - public void testTextShapeExporterAsciiBorder() { + public void testTextShapeExporterAsciiBorder() throws IOException { ShapeTable st = readStandardShapeTable(); final String expected = "+-----+\n" @@ -92,7 +93,7 @@ public class ShapesTest { } @Test - public void testTextShapeTableExporterNoBorder() { + public void testTextShapeTableExporterNoBorder() throws IOException { ShapeTable st = readStandardShapeTable(); // Simulate 4 of these identical shapes by adding 3 more @@ -116,7 +117,7 @@ public class ShapesTest { } @Test - public void testTextShapeTableExporterAsciiBorder() { + public void testTextShapeTableExporterAsciiBorder() throws IOException { ShapeTable st = readStandardShapeTable(); // Simulate 4 of these identical shapes by adding 3 more diff --git a/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java index f2274f7..db05280 100644 --- a/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java +++ b/tools/st/src/main/java/io/github/applecommander/bastools/tools/st/ExtractCommand.java @@ -34,9 +34,15 @@ public class ExtractCommand implements Callable { @Option(names = "--border", description = "Set border style (none, simple, box)", showDefaultValue = Visibility.ALWAYS) private String borderStyle = "simple"; - @Option(names = { "-w", "--width" }, description = "Set text width", showDefaultValue = Visibility.ALWAYS) - private int textWidth = 80; + @Option(names = "--format", description = "Select output format (text, png, gif, jpeg, bmp, wbmp)", showDefaultValue = Visibility.ALWAYS) + private String outputFormat = "text"; + @Option(names = "--skip-empty", description = "Skip empty shapes") + private boolean skipEmptyShapesFlag = false; + + @Option(names = { "-w", "--width" }, description = "Set width (defaults: text=80, image=1024)") + private int width = -1; + @Option(names = "--shape", description = "Extract specific shape") private int shapeNum = 0; @@ -47,15 +53,10 @@ public class ExtractCommand implements Callable { @Override public Void call() throws IOException { - validateArguments(); + ShapeExporter exporter = validateAndParseArguments(); ShapeTable shapeTable = stdinFlag ? ShapeTable.read(System.in) : ShapeTable.read(file); - ShapeExporter exporter = ShapeExporter.text() - .borderStrategy(borderStrategy) - .maxWidth(textWidth) - .build(); - if (shapeNum > 0) { if (shapeNum <= shapeTable.shapes.size()) { Shape shape = shapeTable.shapes.get(shapeNum-1); @@ -78,7 +79,7 @@ public class ExtractCommand implements Callable { return null; } - private void validateArguments() throws IOException { + private ShapeExporter validateAndParseArguments() throws IOException { if (stdoutFlag && filename != null) { throw new IOException("Please choose one of stdout or output file"); } @@ -98,5 +99,31 @@ public class ExtractCommand implements Callable { default: throw new IOException("Please select a valid border strategy"); } + + ShapeExporter exporter = null; + switch (outputFormat) { + case "text": + exporter = ShapeExporter.text() + .borderStrategy(borderStrategy) + .maxWidth(width == -1 ? 80 : width) + .skipEmptyShapes(skipEmptyShapesFlag) + .build(); + break; + case "png": + case "jpeg": + case "gif": + case "bmp": + case "wbmp": + exporter = ShapeExporter.image() + .border(borderStrategy != BorderStrategy.NONE) + .maxWidth(width == -1 ? 1024 : width) + .imageFormat(outputFormat) + .skipEmptyShapes(skipEmptyShapesFlag) + .build(); + break; + default: + throw new IOException("Please select a valid output format"); + } + return exporter; } } \ No newline at end of file