From d13cc3567c89f3f64332163f130ca608a0ef07ac Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Wed, 19 Oct 2016 15:22:55 -0400 Subject: [PATCH] Hatch print base64 image experiment Signed-off-by: Bill Erickson --- src/org/evergreen_ils/hatch/Hatch.java | 207 +---------------- .../evergreen_ils/hatch/HatchWebSocketHandler.java | 18 +- src/org/evergreen_ils/hatch/PrintManager.java | 244 ++++++++++++++++----- 3 files changed, 202 insertions(+), 267 deletions(-) diff --git a/src/org/evergreen_ils/hatch/Hatch.java b/src/org/evergreen_ils/hatch/Hatch.java index 0bed93f9c..26215a677 100644 --- a/src/org/evergreen_ils/hatch/Hatch.java +++ b/src/org/evergreen_ils/hatch/Hatch.java @@ -17,221 +17,28 @@ package org.evergreen_ils.hatch; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.eclipse.jetty.server.handler.ContextHandler; -import org.eclipse.jetty.server.Handler; -import org.eclipse.jetty.server.handler.HandlerList; -import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; -import javafx.application.Application; -import javafx.application.Platform; -import javafx.scene.Scene; -import javafx.scene.layout.Region; -import javafx.scene.web.WebEngine; -import javafx.scene.web.WebView; -import javafx.scene.Node; -import javafx.stage.Stage; -import javafx.scene.transform.Scale; -import javafx.beans.value.ChangeListener; -import javafx.concurrent.Worker; -import javafx.concurrent.Worker.State; -import javafx.concurrent.Service; -import javafx.concurrent.Task; -import javafx.event.EventHandler; -import javafx.concurrent.WorkerStateEvent; -import java.util.concurrent.LinkedBlockingQueue; - -import org.eclipse.jetty.util.ajax.JSON; - import java.util.Map; - import java.io.FileInputStream; /** * Main class for Hatch. * - * This class operates as a two-headed beast, whose heads will occasionally - * communicate with each other. - * - * It runs a JavaFX thread for printing HTML documents and runs a Jetty - * server thread for handling communication with external clients. - * - * Most of the work performed happens solely in the Jetty server thread. - * Attempts to print, however, are passed into the JavaFX thread so that - * the HTML may be loaded into a WebView for printing, which must happen - * within the JavaFX thread. - * - * Messages are passed from the Jetty thread to the JavaFX thread via a - * blocking thread queue, observed by a separate Service thread, whose - * job is only to pull messages from the queue. - * - * Beware: On Mac OS, the "FX Application Thread" is renamed to - * "AppKit Thread" when the first call to print() or showPrintDialog() - * [in PrintManager] is made. This is highly confusing when viewing logs. + * Reads the Hatch/Jetty configuration file and launches the Jetty + * websockets server instance. * */ -public class Hatch extends Application { - - /** Browser Region for rendering and printing HTML */ - private BrowserView browser; +public class Hatch { - /** BrowserView requires a stage for rendering */ - private Stage primaryStage; - /** Our logger instance */ static final Logger logger = Log.getLogger("Hatch"); - /** Message queue for passing messages from the Jetty thread into - * the JavaFX Application thread */ - private static LinkedBlockingQueue requestQueue = - new LinkedBlockingQueue(); - - /** - * Printable region containing a browser - */ - class BrowserView extends Region { - WebView webView = new WebView(); - WebEngine webEngine = webView.getEngine(); - public BrowserView() { - getChildren().add(webView); - } - } - - /** - * Service task which listens for inbound messages from the - * servlet. - * - * The code blocks on the concurrent queue, so it must be - * run in a separate thread to avoid locking the main FX thread. - */ - private static class MsgListenService extends Service> { - protected Task> createTask() { - return new Task>() { - protected Map call() { - while (true) { - logger.info("MsgListenService waiting for a message..."); - try { - // take() blocks until a message is available - return requestQueue.take(); - } catch (InterruptedException e) { - // interrupted, go back and listen - continue; - } - } - } - }; - } - } - - - /** - * JavaFX startup call - */ - @Override - public void start(Stage primaryStage) { - this.primaryStage = primaryStage; - startMsgTask(); - } - - /** - * Queues a message for processing by the queue processing thread. - */ - public static void enqueueMessage(Map params) { - logger.debug("queueing print message"); - requestQueue.offer(params); - } - - /** - * Build a browser view from the print content, tell the - * browser to print itself. - */ - private void handlePrint(Map params) { - String content = (String) params.get("content"); - String contentType = (String) params.get("contentType"); - - if (content == null) { - logger.warn("handlePrint() called with no content"); - return; - } - - browser = new BrowserView(); - Scene scene = new Scene(browser); - primaryStage.setScene(scene); - - browser.webEngine.getLoadWorker() - .stateProperty() - .addListener( (ChangeListener) (obsValue, oldState, newState) -> { - logger.info("browser load state " + newState); - if (newState == State.SUCCEEDED) { - logger.info("Print browser page load completed"); - - // Avoid nested UI event loops -- runLater - Platform.runLater(new Runnable() { - @Override public void run() { - new PrintManager().print(browser.webEngine, params); - } - }); - } - }); - - logger.info("printing " + content.length() + " bytes of " + contentType); - browser.webEngine.loadContent(content, contentType); - - // After queueing up the HTML for printing, go back to listening - // for new messages. - startMsgTask(); - } - - /** - * Fire off the Service task, which checks for queued messages. - * - * When a queued message is found, it's sent off for printing. - */ - public void startMsgTask() { - - MsgListenService service = new MsgListenService(); - - logger.info("starting MsgTask"); - - service.setOnSucceeded( - new EventHandler() { - - @Override - public void handle(WorkerStateEvent t) { - logger.info("MsgTask handling message.. "); - Map message = - (Map) t.getSource().getValue(); - - // avoid nesting UI event loops by kicking off the print - // operation from the main FX loop after this event handler - // has exited. - Platform.runLater( - new Runnable() { - @Override public void run() { - handlePrint(message); - } - } - ); - } - }); - - service.start(); - } - /** * Hatch main. * - * Reads the Jetty configuration, starts the Jetty server thread, - * then launches the JavaFX Application thread. + * Read the Jetty configuration and start the Jetty server thread. */ public static void main(String[] args) throws Exception { @@ -247,9 +54,7 @@ public class Hatch extends Application { // to continue running in its own thread server.start(); - logger.info("Launching FX Application"); - - // launch the FX Application thread - launch(args); + // sit and let the server do its thing. + server.join(); } } diff --git a/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java b/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java index bb2ee9c89..f9730ff88 100644 --- a/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java +++ b/src/org/evergreen_ils/hatch/HatchWebSocketHandler.java @@ -259,15 +259,16 @@ public class HatchWebSocketHandler { break; case "print": - // pass ourselves off to the print handler so it can reply - // for us after printing has completed. - params.put("socket", this); - Hatch.enqueueMessage(params); - - // we don't want to return a response below, since the - // FX thread will handle that for us. - return; + try { + response = new PrintManager().print(params); + } catch (Exception e) { + response = e.toString(); + error = true; + } + + break; + /* case "print-config": try { response = new PrintManager().configurePrinter(params); @@ -276,6 +277,7 @@ public class HatchWebSocketHandler { error = true; } break; + */ case "get": String val = io.get(key); diff --git a/src/org/evergreen_ils/hatch/PrintManager.java b/src/org/evergreen_ils/hatch/PrintManager.java index 40a4003d7..18961d395 100644 --- a/src/org/evergreen_ils/hatch/PrintManager.java +++ b/src/org/evergreen_ils/hatch/PrintManager.java @@ -19,19 +19,21 @@ package org.evergreen_ils.hatch; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; -// printing -import javafx.print.*; -import javafx.scene.web.WebEngine; -import javafx.collections.ObservableSet; -import javafx.collections.SetChangeListener; - -import javax.print.PrintService; -import javax.print.PrintServiceLookup; -import javax.print.attribute.Attribute; -import javax.print.attribute.AttributeSet; -import javax.print.attribute.PrintRequestAttributeSet; -import javax.print.attribute.standard.Media; -import javax.print.attribute.standard.OrientationRequested; +import java.io.InputStream; +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.io.IOException; +import javax.print.*; +import javax.print.event.*; +import javax.print.attribute.*; +import javax.print.attribute.standard.*; + +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.image.*; +import javax.imageio.*; import java.lang.IllegalArgumentException; @@ -59,6 +61,7 @@ public class PrintManager { * the Printer configuration options (if any are already set). * @return A Map of printer settings extracted from the print dialog. */ + /* public Map configurePrinter( Map params) throws IllegalArgumentException { @@ -80,66 +83,141 @@ public class PrintManager { return settings; } } + */ + + class PrintableImage implements Printable { + BufferedImage image; + public PrintableImage(BufferedImage image) { + this.image = image; + } + + public int print(Graphics g, PageFormat pf, int pageIndex) { + Graphics2D g2d = (Graphics2D) g; + g.translate((int) (pf.getImageableX()), (int) (pf.getImageableY())); + if (pageIndex == 0) { + double pageWidth = pf.getImageableWidth(); + double pageHeight = pf.getImageableHeight(); + double imageWidth = image.getTileWidth(); + double imageHeight = image.getTileHeight(); + double scaleX = pageWidth / imageWidth; + double scaleY = pageHeight / imageHeight; + double scaleFactor = Math.min(scaleX, scaleY); + g2d.scale(scaleFactor, scaleFactor); + g.drawImage(image, 0, 0, null); + return Printable.PAGE_EXISTS; + } + return Printable.NO_SUCH_PAGE; + } + } /** - * Print the requested page using the provided settings + * Print the requested page using the provided settings. * - * @param engine The WebEngine instance to print * @param params Print request parameters + * @return null on success, error string on failure */ - public void print(WebEngine engine, Mapparams) { + public String print(Mapparams) { - Long msgid = (Long) params.get("msgid"); - Boolean showDialog = (Boolean) params.get("showDialog"); + String content = (String) params.get("content"); + String contentType = (String) params.get("contentType"); + String errorMessage = null; Map settings = (Map) params.get("config"); - HatchWebSocketHandler socket = - (HatchWebSocketHandler) params.get("socket"); - - PrinterJob job = null; + String name = (String) settings.get("printer"); + PrintService printer = getPrinterByName(name); + PrintJobWatcher watcher = null; + // TODO try { - job = buildPrinterJob(settings); - } catch(IllegalArgumentException e) { - socket.reply(e.toString(), msgid, false); - return; - } + DocFlavor flavor = null; + BufferedImage bufferedImage = null; + InputStream stream = null; + byte[] bytes; + + switch (contentType) { + + case "text/plain": + flavor = DocFlavor.INPUT_STREAM.TEXT_PLAIN_UTF_8; + bytes = content.getBytes("UTF8"); + stream = new ByteArrayInputStream(bytes); + break; - if (showDialog != null && showDialog.booleanValue()) { - logger.info("Print dialog requested"); + case "image/png": + //flavor = DocFlavor.INPUT_STREAM.PNG; + flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE; + bytes = javax.xml.bind.DatatypeConverter.parseBase64Binary(content); + stream = new ByteArrayInputStream(bytes); + bufferedImage = ImageIO.read(stream); + break; - if (!job.showPrintDialog(null)) { - // job canceled by user - logger.info("after dialog"); - job.endJob(); - socket.reply("Print job canceled", msgid); - return; + default: + errorMessage = "Unsupported contentType " + contentType; + logger.warn(errorMessage); + return errorMessage; } - } else { - logger.info("No print dialog requested"); - } - Thread[] all = new Thread[100]; - int count = Thread.currentThread().enumerate(all); - logger.info(count + " active threads in print"); - logger.info("Thread " + Thread.currentThread().getId() + " printing..."); + PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet(); + aset.add(new Copies(1)); + aset.add(Sides.ONE_SIDED); + + DocPrintJob job = printer.createPrintJob(); + watcher = new PrintJobWatcher(job); + SimpleDoc doc; + if (bufferedImage != null) { + doc = new SimpleDoc( + new PrintableImage(bufferedImage), flavor, null); + } else { + doc = new SimpleDoc(stream, flavor, null); + } + job.print(doc, aset); + watcher.waitForDone(); + + } catch (UnsupportedEncodingException uee) { + return "Error decoding print string"; + } catch (PrintException pe) { + return "Error communicating with printer"; + } catch (IOException ioe) { + } - engine.print(job); - logger.info("after print"); + PrintJobEvent event = watcher.getFinalEvent(); + switch(event.getPrintEventType()) { + case PrintJobEvent.JOB_COMPLETE: + logger.info("Printing completed successfully"); + break; + case PrintJobEvent.DATA_TRANSFER_COMPLETE: + logger.info("Printer data transfer completed; status unknown"); + break; + case PrintJobEvent.NO_MORE_EVENTS: + logger.info("No print events received; status unknown"); + break; + case PrintJobEvent.JOB_CANCELED: + errorMessage = "Print job canceled"; + break; + case PrintJobEvent.JOB_FAILED: + errorMessage = "Print job failed"; + break; + case PrintJobEvent.REQUIRES_ATTENTION: + errorMessage = "Printer requires attention"; + break; + default: + errorMessage = "Printing failed"; + } - job.endJob(); + if (errorMessage != null) { logger.warn(errorMessage); } - socket.reply("Print job succeeded", msgid); + return errorMessage; } + /** * Constructs a PrinterJob based on the provided settings. * * @param settings The printer configuration Map. * @return The newly created printer job. */ + /* public PrinterJob buildPrinterJob( Map settings) throws IllegalArgumentException { @@ -159,6 +237,7 @@ public class PrintManager { return job; } + */ /** * Builds a PageLayout for the requested printer, using the @@ -168,6 +247,7 @@ public class PrintManager { * @param printer The printer from which to spawn the PageLayout * @return The newly constructed PageLayout object. */ + /* protected PageLayout buildPageLayout( Map settings, Printer printer) { @@ -211,6 +291,7 @@ public class PrintManager { ((Number) layoutMap.get("bottomMargin")).doubleValue() ); } + */ /** * Applies the provided settings to the PrinterJob. @@ -218,6 +299,7 @@ public class PrintManager { * @param settings The printer configuration settings map. * @param job A PrinterJob, constructed from buildPrinterJob() */ + /* protected void applySettingsToJob( Map settings, PrinterJob job) { @@ -284,6 +366,7 @@ public class PrintManager { jobSettings.setPageRanges(builtRanges.toArray(new PageRange[0])); } } + */ /** * Extracts and flattens the various configuration values from a @@ -292,6 +375,7 @@ public class PrintManager { * @param job The PrinterJob whose attributes are to be extracted. * @return The extracted printer settings map. */ + /* protected Map extractSettingsFromJob(PrinterJob job) { Map settings = new HashMap(); JobSettings jobSettings = job.getJobSettings(); @@ -361,18 +445,17 @@ public class PrintManager { logger.info("compiled printer properties: " + settings.toString()); return settings; } + */ /** * Returns all known Printer's. * * @return Array of all printers */ - protected Printer[] getPrinters() { - ObservableSet printerObserver = Printer.getAllPrinters(); - - if (printerObserver == null) return new Printer[0]; - - return (Printer[]) printerObserver.toArray(new Printer[0]); + protected PrintService[] getPrinters() { + DocFlavor flavor = DocFlavor.INPUT_STREAM.AUTOSENSE; + PrintRequestAttributeSet aset = new HashPrintRequestAttributeSet(); + return PrintServiceLookup.lookupPrintServices(flavor, aset); } /** @@ -382,14 +465,15 @@ public class PrintManager { * @return Map of printer information. */ protected List> getPrintersAsMaps() { - Printer[] printers = getPrinters(); + PrintService[] printers = getPrinters(); List> printerMaps = new LinkedList>(); - Printer defaultPrinter = Printer.getDefaultPrinter(); + PrintService defaultPrinter = + PrintServiceLookup.lookupDefaultPrintService(); - for (Printer printer : printers) { + for (PrintService printer : printers) { HashMap printerMap = new HashMap(); printerMaps.add(printerMap); printerMap.put("name", printer.getName()); @@ -411,13 +495,57 @@ public class PrintManager { * @return The printer whose name matches the provided name, or null * if no such printer is found. */ - protected Printer getPrinterByName(String name) { - Printer[] printers = getPrinters(); - for (Printer printer : printers) { + protected PrintService getPrinterByName(String name) { + PrintService[] printers = getPrinters(); + for (PrintService printer : printers) { if (printer.getName().equals(name)) return printer; } return null; } + + + // PrintJobWatcher class copied and modified from: + // http://www.rgagnon.com/javadetails/java-print-a-text-file-with-javax.print-api.html + class PrintJobWatcher { + PrintJobEvent event = null; // set to the final PrintJobAdapter + + PrintJobWatcher(DocPrintJob job) { + + job.addPrintJobListener(new PrintJobAdapter() { + public void printJobCanceled(PrintJobEvent pje) { + allDone(pje); + } + public void printJobCompleted(PrintJobEvent pje) { + allDone(pje); + } + public void printJobFailed(PrintJobEvent pje) { + allDone(pje); + } + public void printJobNoMoreEvents(PrintJobEvent pje) { + allDone(pje); + } + void allDone(PrintJobEvent pje) { + synchronized (PrintJobWatcher.this) { + event = pje; + PrintJobWatcher.this.notify(); + } + } + }); + } + + public PrintJobEvent getFinalEvent() { + return event; + } + + public synchronized void waitForDone() { + try { + while (event == null) { + wait(); + } + } catch (InterruptedException e) { + } + } + } } -- 2.11.0