import java.awt.image.*; import java.io.*; import java.nio.charset.*; import java.util.*; import javax.imageio.*; public class Bundle { /////////////////////////////// BundledFile /////////////////////////////// // Individual packaged resource file static class BundledFile implements Comparable { // Instance fields byte[] data; // File data loaded from disk File file; // Source file String filename; // Logical filename // Constructor BundledFile(BundledFile parent, File file) { // Configure instance fields this.file = file; filename = parent == null ? "" : parent.filename + file.getName(); // Load file data if file if (file.isFile()) { try (var stream = new FileInputStream(file)) { data = stream.readAllBytes(); } catch (Exception e) { } } // Update filename if directory else if (parent != null) filename += "/"; } // Comparator public int compareTo(BundledFile o) { return filename.equals("web/_boot.js") ? -1 : o.filename.equals("web/_boot.js") ? +1 : filename.compareTo(o.filename) ; } // Produce a list of child files or directories BundledFile[] listFiles(String name, boolean isDirectory) { // Produce a filtered list of files var files = this.file.listFiles(f->{ // Does not satisfy the directory requirement if (f.isDirectory() != isDirectory) return false; // Current directory is not root if (!filename.equals("")) return true; // Filter specific files from being bundled String filename = f.getName(); return !( filename.startsWith(".git" ) || filename.startsWith(name + "_") && filename.endsWith (".html" ) ); }); // Process all files for bundling var ret = new BundledFile[files.length]; for (int x = 0; x < files.length; x++) ret[x] = new BundledFile(this, files[x]); return ret; } } ///////////////////////////// Program Methods ///////////////////////////// // Program entry point public static void main(String[] args) { String name = name(args[0]); var files = listFiles(args[0]); var prepend = prepend(name, files); var bundle = bundle(prepend, files); var image = image(bundle); var url = url(image); patch(name, url); } // Produce a buffer of the bundled files static byte[] bundle(byte[] prepend, BundledFile[] files) { try (var stream = new ByteArrayOutputStream()) { stream.write(prepend); stream.write(files[0].data); // web/_boot.js stream.write(0); for (int x = 1; x < files.length; x++) stream.write(files[x].data); return stream.toByteArray(); } catch (Exception e) { return null; } } // Convert a bundle buffer into a PNG-encoded image buffer static byte[] image(byte[] bundle) { int width = (int) Math.ceil(Math.sqrt(bundle.length)); int height = (bundle.length + width - 1) / width; var pixels = new int[width * height]; // Encode the buffer as a pixel array for (int x = 0; x < bundle.length; x++) { int b = bundle[x] & 0xFF; pixels[x] = 0xFF000000 | b << 16 | b << 8 | b; } // Produce an image using the pixels var image = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB); image.setRGB(0, 0, width, height, pixels, 0, width); // Encode the image as a PNG buffer try (var stream = new ByteArrayOutputStream()) { ImageIO.write(image, "png", stream); return stream.toByteArray(); } catch (Exception e) { return null; } } // List all files static BundledFile[] listFiles(String name) { var dirs = new ArrayList(); var files = new ArrayList(); // Propagate down the file system tree dirs.add(new BundledFile(null, new File("."))); while (!dirs.isEmpty()) { var dir = dirs.remove(0); for (var sub : dir.listFiles(name, true )) dirs.add(sub ); for (var file : dir.listFiles(name, false)) files.add(file); } // Return the set of files as a sorted array Collections.sort(files); return files.toArray(new BundledFile[files.size()]); } // Generate a filename for the bundle static String name(String name) { var calendar = Calendar.getInstance(); return String.format("%s_%04d%02d%02d", name, calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH) + 1, calendar.get(Calendar.DAY_OF_MONTH) ); } // Produce the output HTML from the template static void patch(String name, String url) { String markup = null; try (var stream = new FileInputStream("web/template.html")) { markup = new String(stream.readAllBytes(), StandardCharsets.UTF_8); } catch (Exception e) { } markup = markup.replace("src=\"\"", "src=\"" + url + "\""); try (var stream = new FileOutputStream(name + ".html")) { stream.write(markup.getBytes(StandardCharsets.UTF_8)); } catch (Exception e) { } } // Produce source data to prepend to web/_boot.js static byte[] prepend(String name, BundledFile[] files) { var ret = new StringBuilder(); // Arguments ret.append("let " + "buffer=arguments[0]," + "image=arguments[1]," + "name=\"" + name + "\"" + ";"); // Bundle manifest ret.append("let manifest=["); for (var file : files) { if (file != files[0]) ret.append(","); ret.append("[\"" + file.filename + "\", " + file.data.length + "]"); } ret.append("];"); // Convert to byte array return ret.toString().getBytes(StandardCharsets.UTF_8); } // Convert an image buffer to a data URL static String url(byte[] image) { return "data:image/png;base64," + Base64.getMimeEncoder(0, new byte[0]).encodeToString(image); } }