1 /*
   2  * Copyright (c) 2013, 2016, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.prism.j2d.print;
  27 
  28 import javafx.print.Collation;
  29 import javafx.print.JobSettings;
  30 import javafx.print.PageLayout;
  31 import javafx.print.PageOrientation;
  32 import javafx.print.PageRange;
  33 import javafx.print.Paper;
  34 import javafx.print.PaperSource;
  35 import javafx.print.PrintColor;
  36 import javafx.print.PrintResolution;
  37 import javafx.print.PrintSides;
  38 import javafx.print.Printer;
  39 import javafx.print.Printer.MarginType;
  40 import javafx.print.PrinterAttributes;
  41 import javafx.scene.Group;
  42 import javafx.scene.Node;
  43 import javafx.scene.Parent;
  44 import javafx.scene.Scene;
  45 import javafx.stage.Window;
  46 import javax.print.PrintService;
  47 import javax.print.attribute.HashPrintRequestAttributeSet;
  48 import javax.print.attribute.PrintRequestAttributeSet;
  49 import javax.print.attribute.ResolutionSyntax;
  50 import javax.print.attribute.Size2DSyntax;
  51 import javax.print.attribute.standard.Chromaticity;
  52 import javax.print.attribute.standard.Copies;
  53 import javax.print.attribute.standard.DialogTypeSelection;
  54 import javax.print.attribute.standard.Media;
  55 import javax.print.attribute.standard.MediaPrintableArea;
  56 import javax.print.attribute.standard.MediaSize;
  57 import javax.print.attribute.standard.MediaSizeName;
  58 import javax.print.attribute.standard.MediaTray;
  59 import javax.print.attribute.standard.OrientationRequested;
  60 import javax.print.attribute.standard.PageRanges;
  61 import javax.print.attribute.standard.PrintQuality;
  62 import javax.print.attribute.standard.PrinterResolution;
  63 import javax.print.attribute.standard.SheetCollate;
  64 import javax.print.attribute.standard.Sides;
  65 import java.awt.*;
  66 import java.awt.print.PageFormat;
  67 import java.awt.print.Pageable;
  68 import java.awt.print.Printable;
  69 import java.awt.print.PrinterException;
  70 import java.util.ArrayList;
  71 import java.util.Set;
  72 import com.sun.glass.ui.Application;
  73 import com.sun.javafx.PlatformUtil;
  74 import com.sun.javafx.print.PrintHelper;
  75 import com.sun.javafx.print.PrinterImpl;
  76 import com.sun.javafx.print.PrinterJobImpl;
  77 import com.sun.javafx.scene.NodeHelper;
  78 import com.sun.javafx.sg.prism.NGNode;
  79 import com.sun.javafx.tk.Toolkit;
  80 import com.sun.prism.j2d.PrismPrintGraphics;
  81 
  82 public class J2DPrinterJob implements PrinterJobImpl {
  83 
  84     javafx.print.PrinterJob fxPrinterJob;
  85     java.awt.print.PrinterJob pJob2D;
  86     javafx.print.Printer fxPrinter;
  87     J2DPrinter j2dPrinter;
  88 
  89     private JobSettings settings;
  90     private PrintRequestAttributeSet printReqAttrSet;
  91     private volatile Object elo = null;
  92 
  93     public J2DPrinterJob(javafx.print.PrinterJob fxJob) {
  94 
  95         fxPrinterJob = fxJob;
  96         fxPrinter = fxPrinterJob.getPrinter();
  97         j2dPrinter = getJ2DPrinter(fxPrinter);
  98         settings = fxPrinterJob.getJobSettings();
  99         pJob2D = java.awt.print.PrinterJob.getPrinterJob();
 100         try {
 101             pJob2D.setPrintService(j2dPrinter.getService());
 102         } catch (PrinterException pe) {
 103         }
 104         printReqAttrSet = new HashPrintRequestAttributeSet();
 105         printReqAttrSet.add(DialogTypeSelection.NATIVE);
 106 
 107         j2dPageable = new J2DPageable();
 108         pJob2D.setPageable(j2dPageable);
 109     }
 110 
 111     public boolean showPrintDialog(Window owner) {
 112 
 113         if (jobRunning || jobDone) {
 114             return false;
 115         }
 116 
 117         if (GraphicsEnvironment.isHeadless()) {
 118             return true;
 119         }
 120 
 121         boolean rv = false;
 122         syncSettingsToAttributes();
 123         if (!Toolkit.getToolkit().isFxUserThread()) {
 124             rv = pJob2D.printDialog(printReqAttrSet);
 125         } else {
 126             // If we are on the event thread, we need to check whether we are
 127             // allowed to call a nested event handler.
 128             if (!Toolkit.getToolkit().canStartNestedEventLoop()) {
 129                 throw new IllegalStateException("Printing is not allowed during animation or layout processing");
 130             }
 131             rv = showPrintDialogWithNestedLoop(owner);
 132         }
 133         if (rv) {
 134             updateSettingsFromDialog();
 135         }
 136         return rv;
 137     }
 138 
 139     private class PrintDialogRunnable implements Runnable {
 140 
 141         public void run() {
 142             boolean rv = false;
 143             try {
 144                 rv = pJob2D.printDialog(printReqAttrSet);
 145             } catch (Exception e) {
 146             } finally {
 147                 Application.invokeLater(new ExitLoopRunnable(this, rv));
 148             }
 149         }
 150     }
 151 
 152     private boolean showPrintDialogWithNestedLoop(Window owner) {
 153         PrintDialogRunnable dr = new PrintDialogRunnable();
 154         Thread prtThread = new Thread(dr, "FX Print Dialog Thread");
 155         prtThread.start();
 156         // the nested event loop will return after the runnable exits.
 157         Object rv = Toolkit.getToolkit().enterNestedEventLoop(dr);
 158 
 159         boolean rvbool = false;
 160         try {
 161             rvbool = ((Boolean)rv).booleanValue();
 162         } catch (Exception e) {
 163         }
 164         return rvbool;
 165     }
 166 
 167     public boolean showPageDialog(Window owner) {
 168         if (jobRunning || jobDone) {
 169             return false;
 170         }
 171         if (GraphicsEnvironment.isHeadless()) {
 172             return true;
 173         }
 174         boolean rv = false;
 175         syncSettingsToAttributes();
 176         if (!Toolkit.getToolkit().isFxUserThread()) {
 177             PageFormat pf = pJob2D.pageDialog(printReqAttrSet);
 178             rv = pf != null;
 179         } else {
 180             // If we are on the event thread, we need to check whether we are
 181             // allowed to call a nested event handler.
 182             if (!Toolkit.getToolkit().canStartNestedEventLoop()) {
 183                 throw new IllegalStateException("Printing is not allowed during animation or layout processing");
 184             }
 185             rv = showPageDialogFromNestedLoop(owner);
 186         }
 187         if (rv) {
 188             updateSettingsFromDialog();
 189         }
 190         return rv;
 191     }
 192 
 193     private class PageDialogRunnable implements Runnable {
 194 
 195         public void run() {
 196             PageFormat pf = null;
 197             try {
 198                 pf = pJob2D.pageDialog(printReqAttrSet);
 199             } catch (Exception e) {
 200             } finally {
 201                 Boolean rv = Boolean.valueOf(pf != null);
 202                 Application.invokeLater(new ExitLoopRunnable(this, rv));
 203             }
 204         }
 205     }
 206 
 207     private boolean showPageDialogFromNestedLoop(Window owner) {
 208 
 209         PageDialogRunnable dr = new PageDialogRunnable();
 210         Thread prtThread = new Thread(dr, "FX Page Setup Dialog Thread");
 211         prtThread.start();
 212         // the nested event loop will return after the runnable exits.
 213         Object rv = Toolkit.getToolkit().enterNestedEventLoop(dr);
 214         boolean rvbool = false;
 215         try {
 216             rvbool = ((Boolean)rv).booleanValue();
 217         } catch (Exception e) {
 218         }
 219         return rvbool;
 220     }
 221 
 222     /*
 223      * The update-Foo methods here are only used to update the
 224      * FX JobSettings as a result of changes by user interaction
 225      * with a print dialog. The new values are stored in the
 226      * PrintRequestAttributeSet and pulled from there in to the
 227      * equivalent FX public API JobSettings.
 228      */
 229     private void updateJobName() {
 230         String name =  pJob2D.getJobName();
 231         if (!name.equals(settings.getJobName())) {
 232             settings.setJobName(name);
 233         }
 234     }
 235     private void updateCopies() {
 236         int nCopies = pJob2D.getCopies();
 237         if (settings.getCopies() != nCopies) {
 238             settings.setCopies(nCopies);
 239         }
 240     }
 241 
 242     private void updatePageRanges() {
 243         PageRanges ranges = (PageRanges)printReqAttrSet.get(PageRanges.class);
 244         // JDK sets default to 1,Integer.MAX_VALUE
 245         // So in this case I think we can just check for non-null and
 246         // only set if its non-null.
 247         if (ranges != null) {
 248             int[][] members = ranges.getMembers();
 249             if (members.length == 1) {
 250                 PageRange range = new PageRange(members[0][0], members[0][1]);
 251                 settings.setPageRanges(range);
 252             } else if (members.length > 0) {
 253                 try {
 254                     ArrayList<PageRange> prList = new ArrayList<PageRange>();
 255                     int last = 0;
 256                     for (int i=0; i<members.length;i++) {
 257                         int s = members[i][0];
 258                         int e = members[i][1];
 259                         if (s <= last || e < s) {
 260                             return;
 261                         }
 262                         last = e;
 263                         prList.add(new PageRange(s, e));
 264                     }
 265                     settings.setPageRanges(prList.toArray(new PageRange[0]));
 266                 } catch (Exception e) {
 267                 }
 268             }
 269         }
 270     }
 271 
 272     private void updateSides() {
 273         Sides sides = (Sides)printReqAttrSet.get(Sides.class);
 274         if (sides == null) {
 275             sides = (Sides)j2dPrinter.getService().
 276                 getDefaultAttributeValue(Sides.class);
 277         }
 278         if (sides == Sides.ONE_SIDED) {
 279             settings.setPrintSides(PrintSides.ONE_SIDED);
 280         } else if (sides == Sides.DUPLEX) {
 281             settings.setPrintSides(PrintSides.DUPLEX);
 282         } else if (sides == Sides.TUMBLE) {
 283             settings.setPrintSides(PrintSides.TUMBLE);
 284         }
 285     }
 286 
 287     /* If the attribute set has an explicit setting for
 288      * collation, then its been set by the user at some point,
 289      * even if the current value is the printer default.
 290      * If there is no value for collation in the attribute set,
 291      * it means that we are u  sing the printer default.
 292      */
 293     private void updateCollation() {
 294         SheetCollate collate =
 295             (SheetCollate)printReqAttrSet.get(SheetCollate.class);
 296         if (collate == null) {
 297             collate = j2dPrinter.getDefaultSheetCollate();
 298         }
 299         if (collate == SheetCollate.UNCOLLATED) {
 300             settings.setCollation(Collation.UNCOLLATED);
 301         } else {
 302             settings.setCollation(Collation.COLLATED);
 303         }
 304     }
 305 
 306     private void updateColor() {
 307         Chromaticity color =
 308             (Chromaticity)printReqAttrSet.get(Chromaticity.class);
 309         if (color == null) {
 310             color = j2dPrinter.getDefaultChromaticity();
 311         }
 312         if (color == Chromaticity.COLOR) {
 313             settings.setPrintColor(PrintColor.COLOR);
 314         } else {
 315             settings.setPrintColor(PrintColor.MONOCHROME);
 316         }
 317     }
 318 
 319     private void updatePrintQuality() {
 320         PrintQuality quality =
 321             (PrintQuality)printReqAttrSet.get(PrintQuality.class);
 322         if (quality == null) {
 323             quality = j2dPrinter.getDefaultPrintQuality();
 324         }
 325 
 326         if (quality == PrintQuality.DRAFT) {
 327             settings.
 328                 setPrintQuality(javafx.print.PrintQuality.DRAFT);
 329         } else if (quality == PrintQuality.HIGH) {
 330             settings.
 331                 setPrintQuality(javafx.print.PrintQuality.HIGH);
 332         } else {
 333             settings.
 334                 setPrintQuality(javafx.print.PrintQuality.NORMAL);
 335         }
 336     }
 337 
 338     private void updatePrintResolution() {
 339         PrinterResolution res =
 340             (PrinterResolution)printReqAttrSet.get(PrinterResolution.class);
 341         if (res == null) {
 342             res = j2dPrinter.getDefaultPrinterResolution();
 343         }
 344         int cfr = res.getCrossFeedResolution(ResolutionSyntax.DPI);
 345         int fr = res.getFeedResolution(ResolutionSyntax.DPI);
 346         settings.setPrintResolution(PrintHelper.createPrintResolution(cfr, fr));
 347     }
 348 
 349     private void updatePageLayout() {
 350         Media media = (Media)printReqAttrSet.get(Media.class);
 351         Paper paper = j2dPrinter.getPaperForMedia(media);
 352         OrientationRequested o = (OrientationRequested)
 353             printReqAttrSet.get(OrientationRequested.class);
 354         PageOrientation orient = J2DPrinter.reverseMapOrientation(o);
 355         MediaPrintableArea mpa =
 356             (MediaPrintableArea)printReqAttrSet.get(MediaPrintableArea.class);
 357         PageLayout newLayout;
 358         if (mpa == null) {
 359             newLayout = fxPrinter.createPageLayout(paper, orient,
 360                                                    MarginType.DEFAULT);
 361         } else {
 362             double pWid = paper.getWidth();
 363             double pHgt = paper.getHeight();
 364             int INCH = MediaPrintableArea.INCH;
 365             double mpaX = mpa.getX(INCH) * 72;
 366             double mpaY = mpa.getY(INCH) * 72;
 367             double mpaW = mpa.getWidth(INCH) * 72;
 368             double mpaH = mpa.getHeight(INCH) * 72;
 369             double lm=0, rm=0, tm=0, bm=0;
 370             switch (orient) {
 371             case PORTRAIT:
 372                 lm = mpaX;
 373                 rm = pWid - mpaX - mpaW;
 374                 tm = mpaY;
 375                 bm = pHgt - mpaY - mpaH;
 376                 break;
 377             case REVERSE_PORTRAIT:
 378                 lm = pWid - mpaX - mpaW;
 379                 rm = mpaX;
 380                 tm = pHgt - mpaY - mpaH;
 381                 bm = mpaY;
 382                 break;
 383             case LANDSCAPE:
 384                 lm = mpaY;
 385                 rm = pHgt - mpaY - mpaH;
 386                 tm = pWid - mpaX - mpaW;
 387                 bm = mpaX;
 388                 break;
 389             case REVERSE_LANDSCAPE:
 390                 lm = pHgt - mpaY - mpaH;
 391                 tm = mpaX;
 392                 rm = mpaY;
 393                 bm = pWid - mpaX - mpaW;
 394                 break;
 395             }
 396             if (Math.abs(lm) < 0.01) lm = 0;
 397             if (Math.abs(rm) < 0.01) rm = 0;
 398             if (Math.abs(tm) < 0.01) tm = 0;
 399             if (Math.abs(bm) < 0.01) bm = 0;
 400             newLayout = fxPrinter.createPageLayout(paper, orient,
 401                                                    lm, rm, tm, bm);
 402         }
 403         settings.setPageLayout(newLayout);
 404     }
 405 
 406     private void updatePaperSource() {
 407         Media m = (Media)printReqAttrSet.get(Media.class);
 408         if (m instanceof MediaTray) {
 409             PaperSource s = j2dPrinter.getPaperSource((MediaTray)m);
 410             if (s != null) {
 411                 settings.setPaperSource(s);
 412             }
 413         }
 414     }
 415 
 416     private Printer getFXPrinterForService(PrintService service) {
 417         Set<Printer> printerSet = Printer.getAllPrinters();
 418         for (Printer p : printerSet) {
 419             J2DPrinter p2d = (J2DPrinter)PrintHelper.getPrinterImpl(p);
 420             PrintService s = p2d.getService();
 421             if (s.equals(service)) {
 422                 return p;
 423             }
 424         }
 425         return fxPrinter; // current printer.
 426     }
 427 
 428     public void setPrinterImpl(PrinterImpl impl) {
 429         j2dPrinter = (J2DPrinter)impl;
 430         fxPrinter = j2dPrinter.getPrinter();
 431         try {
 432             pJob2D.setPrintService(j2dPrinter.getService());
 433         } catch (PrinterException pe) {
 434         }
 435     }
 436 
 437     public PrinterImpl getPrinterImpl() {
 438         return j2dPrinter;
 439     }
 440 
 441     private J2DPrinter getJ2DPrinter(Printer printer) {
 442         return (J2DPrinter)PrintHelper.getPrinterImpl(printer);
 443     }
 444 
 445     public Printer getPrinter() {
 446         return fxPrinter;
 447     }
 448 
 449     public void setPrinter(Printer printer) {
 450         fxPrinter = printer;
 451         j2dPrinter = getJ2DPrinter(printer);
 452         try {
 453             pJob2D.setPrintService(j2dPrinter.getService());
 454         } catch (PrinterException pe) {
 455         }
 456     }
 457 
 458     private void updatePrinter() {
 459         PrintService currService = j2dPrinter.getService();
 460         PrintService jobService = pJob2D.getPrintService();
 461         if (currService.equals(jobService)) {
 462             return; // no change
 463         }
 464         Printer newFXPrinter = getFXPrinterForService(jobService);
 465         // The public setPrinter call also updates the job to be valid for
 466         // the new printer. Any old values not supported will be updated
 467         // to supported values. If we do that, then apply the new user
 468         // settings, any listener will see both sets of changes.
 469         // Its best to just see the single transition.
 470         fxPrinterJob.setPrinter(newFXPrinter);
 471     }
 472 
 473     private void updateSettingsFromDialog() {
 474         updatePrinter();
 475         updateJobName();
 476         updateCopies();
 477         updatePageRanges();
 478         updateSides();
 479         updateCollation();
 480         updatePageLayout();
 481         updatePaperSource();
 482         updateColor();
 483         updatePrintQuality();
 484         updatePrintResolution();
 485     }
 486 
 487     private void syncSettingsToAttributes() {
 488         syncJobName();
 489         syncCopies();
 490         syncPageRanges();
 491         syncSides();
 492         syncCollation();
 493         syncPageLayout();
 494         syncPaperSource();
 495         syncColor();
 496         syncPrintQuality();
 497         syncPrintResolution();
 498     }
 499 
 500     private void syncJobName() {
 501         pJob2D.setJobName(settings.getJobName());
 502     }
 503 
 504     private void syncCopies() {
 505         pJob2D.setCopies(settings.getCopies());
 506         printReqAttrSet.add(new Copies(settings.getCopies()));
 507     }
 508 
 509     private void syncPageRanges() {
 510         printReqAttrSet.remove(PageRanges.class);
 511         PageRange[] prArr = settings.getPageRanges();
 512         if (prArr != null && prArr.length>0) {
 513             int len = prArr.length;
 514             int[][] ranges = new int[len][2];
 515             for (int i=0;i<len;i++) {
 516                 ranges[i][0] = prArr[i].getStartPage();
 517                 ranges[i][1] = prArr[i].getEndPage();
 518             }
 519             printReqAttrSet.add(new PageRanges(ranges));
 520         }
 521     }
 522 
 523     private void syncSides() {
 524         Sides j2dSides = Sides.ONE_SIDED;
 525         PrintSides sides = settings.getPrintSides();
 526         if (sides == PrintSides.DUPLEX) {
 527             j2dSides = Sides.DUPLEX;
 528         } else if (sides == PrintSides.TUMBLE) {
 529             j2dSides = Sides.TUMBLE;
 530         }
 531         printReqAttrSet.add(j2dSides);
 532     }
 533 
 534     private void syncCollation() {
 535         if (settings.getCollation() == Collation.UNCOLLATED) {
 536             printReqAttrSet.add(SheetCollate.UNCOLLATED);
 537         } else {
 538             printReqAttrSet.add(SheetCollate.COLLATED);
 539         }
 540 
 541     }
 542 
 543     private void syncPageLayout() {
 544         PageLayout layout = settings.getPageLayout();
 545         PageOrientation orient = layout.getPageOrientation();
 546         printReqAttrSet.add(J2DPrinter.mapOrientation(orient));
 547         double pWid = layout.getPaper().getWidth();
 548         double pHgt = layout.getPaper().getHeight();
 549         float widthInInches = (float)(pWid/72.0);
 550         float heightInInches = (float)(pHgt/72.0);
 551         MediaSizeName media = MediaSize.findMedia(widthInInches,
 552                                                   heightInInches,
 553                                                   Size2DSyntax.INCH);
 554         if (media == null) {
 555             media = MediaSizeName.NA_LETTER;
 556         }
 557         printReqAttrSet.add(media);
 558         double ix=0, iy=0, iw=pWid, ih=pHgt;
 559         switch (orient) {
 560         case PORTRAIT:
 561             ix = layout.getLeftMargin();
 562             iy = layout.getTopMargin();
 563             iw = pWid - ix - layout.getRightMargin();
 564             ih = pHgt - iy - layout.getBottomMargin();
 565             break;
 566         case REVERSE_PORTRAIT:
 567             ix = layout.getRightMargin();
 568             iy = layout.getBottomMargin();
 569             iw = pWid - ix - layout.getLeftMargin();
 570             ih = pHgt - iy - layout.getTopMargin();
 571             break;
 572         case LANDSCAPE:
 573             ix = layout.getBottomMargin();
 574             iy = layout.getLeftMargin();
 575             iw = pWid - ix - layout.getTopMargin();
 576             ih = pHgt - iy - layout.getRightMargin();
 577             break;
 578         case REVERSE_LANDSCAPE:
 579             ix = layout.getTopMargin();
 580             iy = layout.getRightMargin();
 581             iw = pWid - ix - layout.getBottomMargin();
 582             ih = pHgt - iy - layout.getLeftMargin();
 583         }
 584         ix /= 72.0;
 585         iy /= 72.0;
 586         ih /= 72.0;
 587         iw /= 72.0;
 588         MediaPrintableArea mpa =
 589             new MediaPrintableArea((float)ix, (float)iy,
 590                                    (float)iw, (float)ih,
 591                                    MediaPrintableArea.INCH);
 592         printReqAttrSet.add(mpa);
 593     }
 594 
 595     private void syncPaperSource() {
 596         Media m = (Media)printReqAttrSet.get(Media.class);
 597         if (m != null && m instanceof MediaTray) {
 598             printReqAttrSet.remove(Media.class);
 599         }
 600         PaperSource source = settings.getPaperSource();
 601         if (!source.equals(j2dPrinter.defaultPaperSource())) {
 602             MediaTray tray = j2dPrinter.getTrayForPaperSource(source);
 603             if (tray != null) {
 604                 printReqAttrSet.add(tray);
 605             }
 606         }
 607     }
 608 
 609     private void syncColor() {
 610         if (settings.getPrintColor() == PrintColor.MONOCHROME) {
 611             printReqAttrSet.add(Chromaticity.MONOCHROME);
 612         } else {
 613             printReqAttrSet.add(Chromaticity.COLOR);
 614         }
 615     }
 616 
 617     private void syncPrintQuality() {
 618         javafx.print.PrintQuality
 619             quality = settings.getPrintQuality();
 620         PrintQuality j2DQuality;
 621         if (quality == javafx.print.PrintQuality.DRAFT) {
 622             j2DQuality = PrintQuality.DRAFT;
 623         } else if (quality == javafx.print.PrintQuality.HIGH) {
 624           j2DQuality = PrintQuality.HIGH;
 625         } else {
 626             j2DQuality = PrintQuality.NORMAL;
 627         }
 628         printReqAttrSet.add(j2DQuality);
 629     }
 630 
 631     private void syncPrintResolution() {
 632         /* An unsupported resolution results in incorrect scaling by J2D, so
 633          * remove any unsupported value, and only replace with a supported value.
 634          */
 635         PrintService ps = pJob2D.getPrintService();
 636         if (!ps.isAttributeCategorySupported(PrinterResolution.class)) {
 637             printReqAttrSet.remove(PrinterResolution.class);
 638             return;
 639         }
 640         PrinterResolution pres =
 641             (PrinterResolution)printReqAttrSet.get(PrinterResolution.class);
 642         if (pres != null && !ps.isAttributeValueSupported(pres, null, null)) {
 643             printReqAttrSet.remove(PrinterResolution.class);
 644         };
 645 
 646         // Any resolution is now at least known to be supported for this device.
 647         PrintResolution res = settings.getPrintResolution();
 648         if (res == null) {
 649             return;
 650         }
 651         int cfRes = res.getCrossFeedResolution();
 652         int fRes = res.getFeedResolution();
 653         pres = new PrinterResolution(cfRes, fRes, ResolutionSyntax.DPI);
 654         if (!ps.isAttributeValueSupported(pres, null, null)) {
 655             return;
 656         }
 657         // We have validated its a supported value, so add it.
 658         printReqAttrSet.add(pres);
 659     }
 660 
 661     public PageLayout validatePageLayout(PageLayout pageLayout) {
 662         boolean needsNewLayout = false;
 663         PrinterAttributes caps = fxPrinter.getPrinterAttributes();
 664         Paper p = pageLayout.getPaper();
 665         if (!caps.getSupportedPapers().contains(p)) {
 666             needsNewLayout = true;
 667             p = caps.getDefaultPaper();
 668         }
 669         PageOrientation o = pageLayout.getPageOrientation();
 670         if (!caps.getSupportedPageOrientations().contains(o)) {
 671             needsNewLayout = true;
 672             o = caps.getDefaultPageOrientation();
 673         }
 674         if (needsNewLayout) {
 675             pageLayout = fxPrinter.createPageLayout(p, o, MarginType.DEFAULT);
 676         }
 677         return pageLayout;
 678     }
 679 
 680     private boolean jobRunning = false;
 681     private boolean jobError = false;
 682     private boolean jobDone = false;
 683     private J2DPageable j2dPageable = null;
 684 
 685     /*
 686      * Permissions were already checked when creating the job,
 687      * and when setting output file, but this is a final check
 688      * to be made before we start the underlying native job.
 689      */
 690     private void checkPermissions() {
 691         SecurityManager security = System.getSecurityManager();
 692         if (security != null) {
 693             security.checkPrintJobAccess();
 694         }
 695     }
 696 
 697     /*
 698      * 2D uses a call back model. So the 2D PrinterJob needs to run
 699      * on a different thread than the one that the FX app uses.
 700      * This gets really interesting if the FX Node is attached to a
 701      * scene, as you are only supposed to update it on the FX thread
 702      * and the PG code can only access it during sync.
 703      */
 704     public boolean print(PageLayout pageLayout, Node node) {
 705         if (Toolkit.getToolkit().isFxUserThread()) {
 706             // If we are on the event thread, we need to check whether we are
 707             // allowed to call a nested event handler.
 708             if (!Toolkit.getToolkit().canStartNestedEventLoop()) {
 709                 throw new IllegalStateException("Printing is not allowed during animation or layout processing");
 710             }
 711         }
 712 
 713         if (jobError || jobDone) {
 714             return false;
 715         }
 716 
 717         if (!jobRunning) {
 718             checkPermissions();
 719             syncSettingsToAttributes();
 720             PrintJobRunnable runnable = new PrintJobRunnable();
 721             Thread prtThread = new Thread(runnable, "Print Job Thread");
 722             prtThread.start();
 723             jobRunning = true;
 724         }
 725         try {
 726             j2dPageable.implPrintPage(pageLayout, node);
 727         } catch (Throwable t) {
 728             if (com.sun.prism.impl.PrismSettings.debug) {
 729                 System.err.println("printPage caught exception.");
 730                 t.printStackTrace();
 731             }
 732             jobError = true;
 733             jobDone = true;
 734         }
 735         return !jobError;
 736     }
 737 
 738     private class PrintJobRunnable implements Runnable {
 739 
 740         public void run() {
 741 
 742             try {
 743                 pJob2D.print(printReqAttrSet);
 744                 jobDone = true;
 745             } catch (Throwable t) { /* subsumes declared PrinterException */
 746                 if (com.sun.prism.impl.PrismSettings.debug) {
 747                     System.err.println("print caught exception.");
 748                     t.printStackTrace();
 749                 }
 750                 jobError = true;
 751                 jobDone = true;
 752             }
 753             /*
 754              * If the job ends because its reached a page range limit
 755              * rather than calling getPage() we need to exit the nested loop.
 756              */
 757             if (elo != null) {
 758                 Application.invokeLater(new ExitLoopRunnable(elo, null));
 759             }
 760         }
 761     }
 762 
 763     static class LayoutRunnable implements Runnable {
 764         PageInfo pageInfo;
 765 
 766         LayoutRunnable(PageInfo info) {
 767             pageInfo = info;
 768         }
 769 
 770         public void run() {
 771             if (pageInfo.tempScene && pageInfo.root.getScene() == null) {
 772                 new Scene(pageInfo.root);
 773             }
 774             NodeHelper.layoutNodeForPrinting(pageInfo.root);
 775         }
 776     }
 777 
 778     static class ClearSceneRunnable implements Runnable {
 779         PageInfo pageInfo;
 780 
 781         ClearSceneRunnable(PageInfo info) {
 782             pageInfo = info;
 783         }
 784 
 785         public void run() {
 786             pageInfo.clearScene();
 787         }
 788     }
 789 
 790     private static class PageInfo {
 791 
 792         private PageLayout pageLayout;
 793         private Node node;
 794         private Parent root;
 795         private Node topNode;
 796         private Group group;
 797         private boolean tempGroup;
 798         private boolean tempScene;
 799         private boolean sceneInited;
 800 
 801         PageInfo(PageLayout pageLayout, Node node) {
 802             this.pageLayout = pageLayout;
 803             this.node = node;
 804         }
 805 
 806         Node getNode() {
 807             initScene();
 808             return node;
 809         }
 810 
 811         PageLayout getPageLayout() {
 812             return pageLayout;
 813         }
 814 
 815         /*
 816          * There are 4 scenarios here.
 817          * 1. We are passed the root node of a Scene.
 818          * 2. We are passed a child node of a Scene, but not the root
 819          * 3. We are passed a root node (no parent) but its not attached
 820          * to a Scene.
 821          * 4. We are passed a child node, but its not part of a Scene.
 822          * In addition we may be called on the FX thread, or not.
 823          * The code here is trying to make all of these work without
 824          * the application needing to do anything special, and hopefully
 825          * without affecting the application.
 826          * The application should not be surprised if we request layout for it,
 827          * since we can't display or print an unlaid out hiearchy.
 828          *
 829          * If this is the FX thread, then we can do everything directly.
 830          * If not, we must add the node to a scene (if needed) and
 831          * request layout on another thread.
 832          * I am assuming here that layout will be a quick no-op if
 833          * everything is already laid out.
 834          * Eventually all of this should be able to be performed on any
 835          * thread, and without attaching to a scene, so this is largely
 836          * workaround. One part I'm not so sure about is whether it
 837          * will ever be the case that being passed a node that is part
 838          * of a hierarchy, but not its root, will be able to be laid out
 839          * directly, or if you need to traverse to the root.
 840          */
 841         void initScene() {
 842             if (sceneInited) {
 843                return;
 844             }
 845             if (node.getScene() == null) {
 846                 tempScene = true;
 847                 Node topNode = node;
 848                 while (topNode.getParent() != null) {
 849                     topNode = topNode.getParent();
 850                 }
 851                 if (topNode instanceof Group) {
 852                     group = (Group)topNode;
 853                 } else {
 854                     tempGroup = true;
 855                     group = new Group();
 856                     group.getChildren().add(topNode);
 857                 }
 858                 root = group;
 859             } else {
 860                 root = node.getScene().getRoot();
 861             }
 862             if (Toolkit.getToolkit().isFxUserThread()) {
 863                 if (tempScene && root.getScene() == null) {
 864                     new Scene(root); // don't need to keep the scene variable
 865                 }
 866                 NodeHelper.layoutNodeForPrinting(root);
 867             } else {
 868                 Application.invokeAndWait(new LayoutRunnable(this));
 869             }
 870             sceneInited = true;
 871         }
 872 
 873         private void clearScene() {
 874             if (tempGroup) {
 875                 group.getChildren().removeAll(root);
 876             }
 877             tempGroup = false;
 878             tempScene = false;
 879             root = null;
 880             group = null;
 881             topNode = null;
 882             sceneInited = false;
 883         }
 884     }
 885 
 886     private Object monitor = new Object();
 887 
 888     static class ExitLoopRunnable implements Runnable {
 889         Object elo, rv;
 890 
 891         ExitLoopRunnable(Object elo, Object rv) {
 892             this.elo = elo;
 893             this.rv = rv;
 894         }
 895 
 896         public void run() {
 897             Toolkit.getToolkit().exitNestedEventLoop(elo, rv);
 898         }
 899     }
 900 
 901     private class J2DPageable implements Pageable, Printable {
 902 
 903         private volatile boolean pageDone;
 904 
 905         private int currPageIndex = -1;
 906 
 907         private volatile PageInfo newPageInfo = null;
 908         private PageInfo currPageInfo;
 909         private PageFormat currPageFormat;
 910 
 911 
 912         private boolean waitForNextPage(int pageIndex) {
 913 
 914             if (elo != null && currPageInfo != null) {
 915                 Application.invokeLater(new ExitLoopRunnable(elo, null));
 916             }
 917 
 918             if (currPageInfo != null) {
 919                 if (Toolkit.getToolkit().isFxUserThread()) {
 920                     currPageInfo.clearScene();
 921                 } else {
 922                     Application.
 923                         invokeAndWait(new ClearSceneRunnable(currPageInfo));
 924                 }
 925             }
 926             currPageInfo = null;
 927             pageDone = true;
 928             synchronized (monitor) {
 929                 if (newPageInfo == null) {
 930                     monitor.notify(); // page is printed and no new page to print
 931                 }
 932                 while (newPageInfo == null && !jobDone && !jobError) {
 933                     try {
 934                         monitor.wait(1000);
 935                     } catch (InterruptedException e) {
 936                     }
 937                 }
 938             }
 939             if (jobDone || jobError) {
 940                 return false;
 941             }
 942             currPageInfo = newPageInfo;
 943             newPageInfo = null;
 944             currPageIndex = pageIndex;
 945             currPageFormat = getPageFormatFromLayout(currPageInfo.getPageLayout());
 946             return true;
 947         }
 948 
 949         private PageFormat getPageFormatFromLayout(PageLayout layout) {
 950             java.awt.print.Paper paper = new java.awt.print.Paper();
 951             double pWid = layout.getPaper().getWidth();
 952             double pHgt = layout.getPaper().getHeight();
 953             double ix=0, iy=0, iw=pWid, ih=pHgt;
 954             PageOrientation orient = layout.getPageOrientation();
 955             switch (orient) {
 956             case PORTRAIT:
 957                 ix = layout.getLeftMargin();
 958                 iy = layout.getTopMargin();
 959                 iw = pWid - ix - layout.getRightMargin();
 960                 ih = pHgt - iy - layout.getBottomMargin();
 961                 break;
 962             case REVERSE_PORTRAIT:
 963                 ix = layout.getRightMargin();
 964                 iy = layout.getBottomMargin();
 965                 iw = pWid - ix - layout.getLeftMargin();
 966                 ih = pHgt - iy - layout.getTopMargin();
 967                 break;
 968             case LANDSCAPE:
 969                 ix = layout.getBottomMargin();
 970                 iy = layout.getLeftMargin();
 971                 iw = pWid - ix - layout.getTopMargin();
 972                 ih = pHgt - iy - layout.getRightMargin();
 973                 break;
 974             case REVERSE_LANDSCAPE:
 975                 ix = layout.getTopMargin();
 976                 iy = layout.getRightMargin();
 977                 iw = pWid - ix - layout.getBottomMargin();
 978                 ih = pHgt - iy - layout.getLeftMargin();
 979             }
 980             paper.setSize(pWid, pHgt);
 981             paper.setImageableArea(ix, iy, iw, ih);
 982             PageFormat format = new PageFormat();
 983             format.setOrientation(J2DPrinter.getOrientID(orient));
 984             format.setPaper(paper);
 985             return format;
 986         }
 987 
 988         private boolean getPage(int pageIndex) {
 989             if (pageIndex == currPageIndex) {
 990                 return true;
 991             }
 992             boolean nextPage = false;
 993             if (pageIndex > currPageIndex) {
 994                 nextPage = waitForNextPage(pageIndex);
 995             }
 996             return nextPage;
 997         }
 998 
 999         public int print(Graphics g, PageFormat pf, int pageIndex) {
1000             if (jobError || jobDone || !getPage(pageIndex)) {
1001                 return Printable.NO_SUCH_PAGE;
1002             }
1003             int x = (int)pf.getImageableX();
1004             int y = (int)pf.getImageableY();
1005             int w = (int)pf.getImageableWidth();
1006             int h = (int)pf.getImageableHeight();
1007             Node appNode = currPageInfo.getNode();
1008             g.translate(x, y);
1009             printNode(appNode, g, w, h);
1010             return Printable.PAGE_EXISTS;
1011         }
1012 
1013         private void printNode(Node node, Graphics g, int w, int h) {
1014             PrismPrintGraphics ppg =
1015                     new PrismPrintGraphics((Graphics2D) g, w, h);
1016             NGNode pgNode = NodeHelper.getPeer(node);
1017             boolean errored = false;
1018             try {
1019                 pgNode.render(ppg);
1020             } catch (Throwable t) {
1021                 if (com.sun.prism.impl.PrismSettings.debug) {
1022                     System.err.println("printNode caught exception.");
1023                     t.printStackTrace();
1024                 }
1025                 errored = true;
1026             }
1027             ppg.getResourceFactory()
1028                     .getTextureResourcePool()
1029                     .freeDisposalRequestedAndCheckResources(errored);
1030         }
1031 
1032         public Printable getPrintable(int pageIndex) {
1033             getPage(pageIndex);
1034             return this;
1035         }
1036 
1037         public PageFormat getPageFormat(int pageIndex) {
1038             getPage(pageIndex);
1039             return currPageFormat;
1040         }
1041 
1042         /*
1043          * Since we return unknown number of pages, then
1044          * the behaviour must be that we can only signal
1045          * end of the job by returning NO_SUCH_PAGE from
1046          * the print(..) method.
1047          */
1048         public int getNumberOfPages() {
1049             return Pageable.UNKNOWN_NUMBER_OF_PAGES;
1050         }
1051 
1052         /*
1053          * Executed on the application's thread.
1054          * Messages over to the printing thread.
1055          */
1056         private void implPrintPage(PageLayout pageLayout, Node node) {
1057 
1058             /* The public API printPage() is synchronized, so we know
1059              * that the app can't call it from 2 threads at the same
1060              * time, not that this is encouraged either.
1061              * Therefore when we are in this code, we know that any
1062              * previous page rendering has completed.
1063              * We also know that this means the app can't have 'queued up'
1064              * pages.
1065              * So, when we are in here, we know that the app is providing
1066              * the info for the next page.
1067              */
1068             pageDone = false;
1069             synchronized (monitor) {
1070                 newPageInfo = new PageInfo(pageLayout, node);
1071                 monitor.notify();
1072             }
1073             if (Toolkit.getToolkit().isFxUserThread()) {
1074                 elo = new Object();
1075                 Toolkit.getToolkit().enterNestedEventLoop(elo);
1076                 elo = null;
1077             } else {
1078                 while (!pageDone && !jobDone && !jobError) {
1079                     synchronized (monitor) {
1080                         try {
1081                             if (!pageDone) {
1082                                 monitor.wait(1000);
1083                             }
1084                         } catch (InterruptedException e) {
1085                         }
1086                     }
1087                 }
1088             }
1089         }
1090 
1091     } /* END J2DPageable class */
1092 
1093 
1094     public boolean endJob() {
1095         if (jobRunning && !jobDone && !jobError) {
1096             jobDone = true;
1097             try {
1098                 synchronized (monitor) {
1099                     monitor.notify();
1100                     return jobDone;
1101                 }
1102             } catch (IllegalStateException e) {
1103                 if (com.sun.prism.impl.PrismSettings.debug) {
1104                     System.err.println("Internal Error " + e);
1105                 }
1106             }
1107         } else {
1108             return jobDone && !jobError;
1109         }
1110         return jobDone;
1111     }
1112 
1113     public void cancelJob() {
1114         if (!pJob2D.isCancelled()) {
1115             pJob2D.cancel();
1116         }
1117         jobDone = true;
1118         if (jobRunning) {
1119             jobRunning = false;
1120             try {
1121                 synchronized (monitor) {
1122                     monitor.notify();
1123                 }
1124             } catch (IllegalStateException e) {
1125                 if (com.sun.prism.impl.PrismSettings.debug) {
1126                     System.err.println("Internal Error " + e);
1127                 }
1128             }
1129         }
1130     }
1131 }