[OpenJDK 2D-Dev] [PATCH] JDK-4627340 : RFE: A way to improve text printing performance for postscript devices
Alex Geller
ag at 4js.com
Thu Jan 9 16:34:02 UTC 2014
Hi,
this is not a production code patch but rather a proof of concept.
The patch improves the Postscript produced in calls to
Graphics2D.drawString().
The the current implementation first tries to print strings using one of
the standard Postscript fonts (PSPrinterJob.textOut()) and if that fails
it falls back to drawing glyph vectors.
The patch adds a third method which is to convert the string into glyphs
by means of Font.createGlyphVector() and embed those glyphs in form of a
Postscript "Type 3" font.
The font is updated incrementally which is explicitly allowed by the
Postscript specification.
The incremental update makes the patch also usable for Asian
environments where eager embedding is not a good option because the
fonts are huge.
The file is reduced compared to glyph vector drawing. As an example
consider a "terms an conditions" page that has 17,000 characters using 3
different fonts.
Using the current method the Postscript file is about 8 MB. Using the
new method the file is 164 KB.
However, the motivation for submitting this patch is not the file size
but the printing time. The original file takes 4:45 minutes to print
while the version with the embedded font prints in less than 10 seconds
on the same printer.
I suspect the slowness in the fact that the glyph vectors are not cached
while the Type 3 fonts are. I posted the results of some related
experiments with the Postscript "ucache" command on the OTN forum (see
https://community.oracle.com/thread/2617145).
Based on that I can post a patch for speeding up
Graphics2D.drawGlyphVector() substantially too if there is interest.
Coming back to the main topic my question is if there are chances that
this gets included. If yes, then I would do (perhaps with some help)
what is necessary to take it from a POC to production code.
I would also be happy if it could be included and activated only by a
system property or a rendering hint.
The issue is quite important to us and time doesn't seem to heal this.
Even printers in the 10,000$ class can take minutes to print a few pages
using the current method.
Attachments:
- openjdk.patch: A patch based on "openjdk_7_b147_jun_11"
- PSTest.java: A test program demonstrating the feature
- out.ps.zip: A zipped Postscript file produced by the test program
"PSTest.java"
Thanks,
Alex
-------------- next part --------------
A non-text attachment was scrubbed...
Name: out.ps.zip
Type: application/zip
Size: 48654 bytes
Desc: not available
URL: <http://mail.openjdk.java.net/pipermail/2d-dev/attachments/20140109/0205d64f/out.ps.zip>
-------------- next part --------------
//Derived from http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4483236
/*
* Copyright 2001 Sun Microsystems, Inc. All Rights Reserved.
*
* This software is the proprietary information of Sun Microsystems, Inc.
* Use is subject to license terms.
*
*/
import java.io.*;
import java.awt.*;
import java.awt.print.*;
import javax.print.*;
import javax.print.attribute.*;
import javax.print.attribute.standard.*;
/*
* Use the Java(TM) Print Service API to locate a service which can export
* 2D graphics to a stream as Postscript. This may be spooled to a
* Postscript printer, or used in a postscript viewer.
*/
public class PSTest implements Printable{
final static String FONT_NAME="Arial";
public PSTest() {
/* Use the pre-defined flavor for a Printable from an
InputStream */
DocFlavor flavor = DocFlavor.SERVICE_FORMATTED.PRINTABLE;
/* Specify the type of the output stream */
String psMimeType = DocFlavor.BYTE_ARRAY.POSTSCRIPT.getMimeType
();
/* Locate factory which can export a GIF image stream as
Postscript */
StreamPrintServiceFactory[] factories =
StreamPrintServiceFactory.lookupStreamPrintServiceFactories(
flavor, psMimeType);
if (factories.length == 0) {
System.err.println("No suitable factories");
System.exit(0);
}
try {
/* Create a file for the exported postscript */
FileOutputStream fos = new FileOutputStream("out.ps");
/* Create a Stream printer for Postscript */
StreamPrintService sps = factories[0].getPrintService
(fos);
/* Create and call a Print Job */
DocPrintJob pj = sps.createPrintJob();
PrintRequestAttributeSet aset = new
HashPrintRequestAttributeSet();
Doc doc = new SimpleDoc(this, flavor, null);
pj.print(doc, aset);
fos.close();
} catch (PrintException pe) {
System.err.println(pe);
} catch (IOException ie) {
System.err.println(ie);
}
}
public int print(Graphics g,PageFormat pf,int pageIndex) {
if (pageIndex <= 1) {
Graphics2D g2d = (Graphics2D)g;
g2d.translate(pf.getImageableX(), pf.getImageableY());
int y=0;
g2d.setFont(new Font(FONT_NAME,Font.PLAIN,12));
g2d.drawString("Page "+(pageIndex+1)+":", 0, y+=14);
g2d.setFont(new Font(FONT_NAME,Font.PLAIN,8));
if(pageIndex==0)
{
g2d.drawString("Note the incremental font update. All new characters on this line but the ", 0, y+=10);
g2d.drawString("characters 'P', 'a', 'g', 'e', ' ' and '1' are added to the font only now.", 0, y+=10);
g2d.drawString("Also note which strings are displayed using \"show\"", 0, y+=10);
g2d.drawString("and which are displayed character by character using \"glyphshow\".", 0, y+=10);
}
else
{
g2d.drawString("Note that the fonts defintions are valid thru", 0, y+=10);
g2d.drawString("the entire document and not only on a single page.", 0, y+=10);
}
g2d.setFont(new Font(FONT_NAME,Font.PLAIN,12));
g2d.drawString("A non ASCII character from ISO-8859-1 (A dieresis): \u00c4", 0, y+=14);
g2d.drawString("A non ASCII character not in ISO-8859-1 (C with hacek): \u010c", 0, y+=14);
g2d.drawString("A unicode character with a value larger than 0xff (Hebrew aleph): \u05d0", 0, y+=14);
g2d.drawString("A ligature \"fi\"", 0, y+=14);
g2d.setFont(new Font(FONT_NAME,Font.ITALIC,12));
g2d.drawString("This time in italic style", 0, y+=14);
g2d.drawString("A ASCII string", 0, y+=14);
g2d.drawString("Another ASCII string with more character so demonstrate the incremental font update", 0, y+=14);
g2d.drawString("A non ASCII character from ISO-8859-1 (A dieresis): \u00c4", 0, y+=14);
g2d.drawString("A non ASCII character not in ISO-8859-1 (C with hacek): \u010c", 0, y+=14);
g2d.drawString("A unicode character with a value larger than 0xff (Hebrew aleph): \u05d0", 0, y+=14);
g2d.drawString("A ligature \"fi\"", 0, y+=14);
g2d.setFont(new Font(FONT_NAME,Font.BOLD,12));
g2d.drawString("This time in bold style (There is a bug in this. The text is too wide)", 0, y+=14);
g2d.drawString("A ASCII string", 0, y+=14);
g2d.drawString("Another ASCII string with more character so demonstrate the incremental font update", 0, y+=14);
g2d.drawString("A non ASCII character from ISO-8859-1 (A dieresis): \u00c4", 0, y+=14);
g2d.drawString("A non ASCII character not in ISO-8859-1 (C with hacek): \u010c", 0, y+=14);
g2d.drawString("A unicode character with a value larger than 0xff (Hebrew aleph): \u05d0", 0, y+=14);
g2d.drawString("A ligature \"fi\"", 0, y+=14);
return Printable.PAGE_EXISTS;
} else {
return Printable.NO_SUCH_PAGE;
}
}
public static void main(String args[]) {
PSTest sp = new PSTest();
}
}
-------------- next part --------------
diff -rupN /tmp/openjdk/jdk/src/share/classes/sun/print/PSPathGraphics.java /home/alex/openjdk_7_b147_jun_11/openjdk/jdk/src/share/classes/sun/print/PSPathGraphics.java
--- /tmp/openjdk/jdk/src/share/classes/sun/print/PSPathGraphics.java 2011-06-27 19:38:47.000000000 +0200
+++ /home/alex/openjdk_7_b147_jun_11/openjdk/jdk/src/share/classes/sun/print/PSPathGraphics.java 2014-01-09 15:25:04.000000000 +0100
@@ -228,6 +228,8 @@ class PSPathGraphics extends PathGraphic
drawnWithPS = psPrinterJob.textOut(this, str,
x+translateX, y+translateY,
font, frc, w);
+ if(!drawnWithPS)
+ drawnWithPS=psPrinterJob.textOutType3(str,x+translateX,y+translateY,font,frc);
}
}
diff -rupN /tmp/openjdk/jdk/src/share/classes/sun/print/PSPrinterJob.java /home/alex/openjdk_7_b147_jun_11/openjdk/jdk/src/share/classes/sun/print/PSPrinterJob.java
--- /tmp/openjdk/jdk/src/share/classes/sun/print/PSPrinterJob.java 2011-06-27 19:38:48.000000000 +0200
+++ /home/alex/openjdk_7_b147_jun_11/openjdk/jdk/src/share/classes/sun/print/PSPrinterJob.java 2014-01-09 15:32:05.000000000 +0100
@@ -94,6 +94,11 @@ import java.nio.charset.*;
import java.nio.CharBuffer;
import java.nio.ByteBuffer;
+import java.awt.font.GlyphVector;
+import java.awt.geom.AffineTransform;
+import java.util.HashMap;
+import java.util.BitSet;
+
//REMIND: Remove use of this class when IPPPrintService is moved to share directory.
import java.lang.reflect.Method;
@@ -332,6 +337,15 @@ public class PSPrinterJob extends Raster
*/
private static Properties mFontProps = null;
+ static final float FONT_SCALE_FACTOR=1f;
+ static final AffineTransform ident=new AffineTransform();
+ static final boolean mDefineFontsIncrementally=true;
+
+
+ CharsetEncoder mIsoEncoder=Charset.forName("ISO-8859-15").newEncoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
+ HashMap<String,FontInfo> mSeenFonts=new HashMap<String,FontInfo>();
+ int mFontIdCounter=0;
+
/* Class static initialiser block */
static {
//enable priviledges so initProps can access system properties,
@@ -2018,6 +2032,260 @@ public class PSPrinterJob extends Raster
}
+ public void setISO8859TextEncoding(int n)
+ {
+ assert n>=1&&n<=16;
+ if(n<1||n>16)
+ throw new IllegalArgumentException("n must be >=1 and <=16");
+ mIsoEncoder=Charset.forName("ISO-8859-"+n).newEncoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
+ }
+
+//TODO: Make sure that long strings are broken down into chunks of maximum 64 K (MAX_PSSTR) size
+ public boolean textOutType3(String s,float x,float y,Font font,FontRenderContext frc) {
+//TODO: Can there be two different fonts that have the same Font.getFamily()/Font.getStyle() combination? If yes, then this needs to be changed and some other
+// method needs to be found that identifies different fonts.
+ Font font1000=font.deriveFont(1000f);
+ String key=font.getFamily()+font.getStyle();
+ FontInfo fi=mSeenFonts.get(key);
+ if(fi==null) { // seen font for the first time -> creat initial definition
+ fi=new FontInfo();
+ mSeenFonts.put(key,fi);
+
+ int missingGlyphCode[] = new int[1];
+ missingGlyphCode[0]=font1000.getMissingGlyphCode();
+ GlyphVector missingGlyphGv=font1000.createGlyphVector(frc, missingGlyphCode);
+ Shape missingGlyphShape=missingGlyphGv.getGlyphOutline(0);
+
+ float[] bbox=new float[] { Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE};
+ adjustBBox(missingGlyphShape,bbox);
+ byte[] bytes=new byte[256];
+ for(int i=0;i<256;i++) bytes[i]=(byte)i;
+ CharBuffer cb=mIsoEncoder.charset().decode(ByteBuffer.wrap(bytes));
+ assert cb.limit()==256;
+ for(int i=0;i<256;i++) {
+ char code=cb.get(i);
+ if(font1000.canDisplay(code)) {
+ String c=""+code;
+ GlyphVector gv=font1000.createGlyphVector(frc,c);
+ Shape sh=gv.getGlyphOutline(0);
+ if(sh==null) continue;
+ adjustBBox(sh,bbox);
+ }
+ }
+
+ mPSStream.println("true setglobal");
+ mPSStream.println("globaldict begin");
+
+ mPSStream.println("8 dict begin");
+ mPSStream.println("/FontType 3 def");
+ mPSStream.println("/FontMatrix [.001 0 0 .001 0 0] def");
+ mPSStream.println("/FontBBox ["+trunc(bbox[0])+" "+trunc(bbox[1])+" "+trunc(bbox[2])+" "+trunc(bbox[3])+"] def");
+ mPSStream.println("/Encoding 256 array def");
+
+//first loop for printing the encoding table
+ for(int i=0;i<256;i++) {
+ char code=(char)i;
+ if(font1000.canDisplay(code)) {
+ mPSStream.println("Encoding "+i+" /c"+Integer.toHexString((int)code)+" put");
+ }
+ else {
+ mPSStream.println("Encoding "+i+" /.notdef put");
+ }
+ }
+//second loop for printing the character definitions
+ mPSStream.println("/CharProcs 3 dict def");
+ mPSStream.println("CharProcs begin");
+ printGlyphDefinition(".notdef",missingGlyphGv);
+
+ //int len=mDefineFontsIncrementally?0xff:0xffff;
+ int len=mDefineFontsIncrementally?0:0xffff;
+ for(int i=0;i<len;i++) {
+ char code=cb.get(i);
+ if(font1000.canDisplay(code)) {
+ String c=""+code;
+ GlyphVector gv=font1000.createGlyphVector(frc,c);
+ if(!mDefineFontsIncrementally) {
+ printGlyphDefinition("c"+Integer.toHexString((int)code),gv);
+ }
+ else
+ if(!fi.isSeen(code)) {
+ fi.markAsSeen(code);
+ printGlyphDefinition("c"+Integer.toHexString((int)code),gv);
+ }
+ }
+ }
+
+ mPSStream.println("end");
+ mPSStream.println("/BuildGlyph % Stack contains: font charname");
+ mPSStream.println("{");
+ mPSStream.println("exch /CharProcs get exch % Get CharProcs dictionary");
+ mPSStream.println("2 copy known not % See if charname is known");
+ mPSStream.println("{ pop /.notdef }");
+ mPSStream.println("if");
+ mPSStream.println("get exec % Execute BuildGlyph procedure");
+ mPSStream.println("} bind def");
+ mPSStream.println("/BuildChar % LanguageLevel 1 compatibility");
+ mPSStream.println("{ 1 index /Encoding get exch get");
+ mPSStream.println("1 index /BuildGlyph get exec");
+ mPSStream.println("} bind def");
+ mPSStream.println("currentdict");
+ mPSStream.println("end ");
+
+ mPSStream.println("/"+fi.getName()+" exch definefont pop");
+
+ mPSStream.println("end");
+ mPSStream.println("false setglobal");
+ } // end of initial font definition
+
+ if(mDefineFontsIncrementally) {
+ int len=s.length();
+ boolean isFirst=true;
+ for(int i=0;i<len;i++) {
+ char code=s.charAt(i);
+ if(!fi.isSeen(code)) {
+
+ fi.markAsSeen(code);
+ if(isFirst) {
+ isFirst=false;
+ mPSStream.println("true setglobal");
+ mPSStream.println("globaldict begin");
+ mPSStream.println("/"+fi.getName()+" findfont");
+ mPSStream.println("/CharProcs get");
+ mPSStream.println("begin");
+ }
+//TODO: grow bbox either here (if allowed) or do it on the initial definition by walking over all characters and not only the first 256 as it is currently the case
+ GlyphVector gv=font1000.createGlyphVector(frc,""+code);
+ printGlyphDefinition("c"+Integer.toHexString((int)code),gv);
+ }
+ }
+ if(!isFirst) {
+ mPSStream.println("end");
+ mPSStream.println("end");
+ mPSStream.println("false setglobal");
+ }
+ }
+ mPSStream.println("/"+fi.getName()+" findfont "+(font.getSize2D()*FONT_SCALE_FACTOR)+" scalefont setfont");
+ mPSStream.println(""+x + " " + y + MOVETO_STR);
+ try {
+ ByteBuffer b=mIsoEncoder.encode(CharBuffer.wrap(s));
+ mPSStream.print("<");
+ int cnt=b.limit();
+ for(int i=0;i<cnt;i++)
+ mPSStream.print(Integer.toHexString(b.get()&0xff));
+ mPSStream.println("> show");
+ }
+//It isn't great to make exceptions part of regular flow but what are the alternatives? On CharsetEncoder.canEncode() the documentation is not very inviting: "The default implementation of this method is not very efficient"
+ catch(Exception e) {
+ int len=s.length();
+ for(int i=0;i<len;i++)
+ mPSStream.println("/c"+Integer.toHexString((int)s.charAt(i))+" glyphshow");
+ }
+ return true;
+ }
+ private static void adjustBBox(Shape s,float[] bbox) {
+ if(!(s instanceof java.awt.geom.GeneralPath)) s=new java.awt.geom.GeneralPath(s);
+ java.awt.geom.GeneralPath gp=(java.awt.geom.GeneralPath)s;
+ java.awt.geom.PathIterator pi=gp.getPathIterator(ident);
+ float coords[]=new float[6];
+ while(!pi.isDone()) {
+ switch(pi.currentSegment(coords)) {
+ case java.awt.geom.PathIterator.SEG_CUBICTO: {
+ if(coords[4]<bbox[0]) bbox[0]=coords[4];
+ if(coords[4]>bbox[2]) bbox[2]=coords[4];
+ if(coords[5]<bbox[1]) bbox[1]=coords[5];
+ if(coords[5]>bbox[3]) bbox[3]=coords[5];
+ }
+ case java.awt.geom.PathIterator.SEG_QUADTO: {
+ if(coords[2]<bbox[0]) bbox[0]=coords[2];
+ if(coords[2]>bbox[2]) bbox[2]=coords[2];
+ if(coords[3]<bbox[1]) bbox[1]=coords[3];
+ if(coords[3]>bbox[3]) bbox[3]=coords[3];
+ }
+ case java.awt.geom.PathIterator.SEG_MOVETO:
+ case java.awt.geom.PathIterator.SEG_LINETO: {
+ if(coords[0]<bbox[0]) bbox[0]=coords[0];
+ if(coords[0]>bbox[2]) bbox[2]=coords[0];
+ if(coords[1]<bbox[1]) bbox[1]=coords[1];
+ if(coords[1]>bbox[3]) bbox[3]=coords[1];
+ break;
+ }
+ }
+ pi.next();
+ }
+ }
+ private void printGlyphDefinition(String name,GlyphVector gv) {
+ Shape s=gv.getGlyphOutline(0);
+ assert s!=null;
+ if(s==null) return;
+ mPSStream.println("/"+name);
+ mPSStream.println("{");
+ float[] bbox=new float[] { Float.MAX_VALUE, Float.MAX_VALUE, -Float.MAX_VALUE, -Float.MAX_VALUE};
+ adjustBBox(s,bbox);
+ mPSStream.println(trunc(gv.getGlyphMetrics(0).getAdvanceX())+" "+trunc(gv.getGlyphMetrics(0).getAdvanceY()));
+ if(bbox[0]>bbox[2]||bbox[1]>bbox[3])
+ mPSStream.println("0 0 0 0");
+ else
+ mPSStream.println(trunc(bbox[0])+" "+trunc(bbox[1])+" "+trunc(bbox[2])+" "+trunc(bbox[3]));
+ mPSStream.println("setcachedevice");
+ if(!(s instanceof java.awt.geom.GeneralPath)) s=new java.awt.geom.GeneralPath(s);
+ java.awt.geom.GeneralPath gp=(java.awt.geom.GeneralPath)s;
+ java.awt.geom.PathIterator pi=gp.getPathIterator(ident);
+ float coords[]=new float[6];
+ float lastX=0,lastY=0;
+ while(!pi.isDone()) {
+ switch(pi.currentSegment(coords)) {
+ case java.awt.geom.PathIterator.SEG_CLOSE: {
+ mPSStream.println(CLOSEPATH_STR);
+ break;
+ }
+ case java.awt.geom.PathIterator.SEG_LINETO: {
+ mPSStream.println(trunc(coords[0])+" "+trunc(coords[1])+LINETO_STR);
+ lastX=coords[0];
+ lastY=coords[1];
+ break;
+ }
+ case java.awt.geom.PathIterator.SEG_MOVETO: {
+ mPSStream.println(trunc(coords[0])+" "+trunc(coords[1])+MOVETO_STR);
+ lastX=coords[0];
+ lastY=coords[1];
+ break;
+ }
+ case java.awt.geom.PathIterator.SEG_QUADTO: {
+ float c1x = lastX + (coords[0] - lastX) * 2 / 3;
+ float c1y = lastY + (coords[1] - lastY) * 2 / 3;
+ float c2x = coords[2] - (coords[2] - coords[0]) * 2/ 3;
+ float c2y = coords[3] - (coords[3] - coords[1]) * 2/ 3;
+ mPSStream.println(trunc(c1x)+" "+trunc(c1y)+" "+trunc(c2x)+" "+trunc(c2y)+" "+trunc(coords[2])+" "+trunc(coords[3])+CURVETO_STR);
+ lastX=coords[2];
+ lastY=coords[3];
+ break;
+ }
+ case java.awt.geom.PathIterator.SEG_CUBICTO: {
+ mPSStream.println(trunc(coords[0])+" "+trunc(coords[1])+" "+trunc(coords[2])+" "+trunc(coords[3])+" "+trunc(coords[4])+" "+trunc(coords[5])+CURVETO_STR);
+ lastX=coords[4];
+ lastY=coords[5];
+ break;
+ }
+ }
+ pi.next();
+ }
+ mPSStream.println("fill");
+ mPSStream.println("} bind def");
+ }
+ private class FontInfo {
+ int mId=mFontIdCounter++;
+ BitSet mSeenCharacters=new BitSet();
+ public String getName() {
+ return "f"+Integer.toHexString(mId);
+ }
+ void markAsSeen(char c) {
+ mSeenCharacters.set((int)c);
+ }
+ public boolean isSeen(char c) {
+ return mSeenCharacters.get((int)c);
+ }
+ }
+
/**
* PluginPrinter generates EPSF wrapped with a header and trailer
* comment. This conforms to the new requirements of Mozilla 1.7
More information about the 2d-dev
mailing list