/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* $Id$ */ package org.apache.fop.render.afp; import java.awt.Color; import java.awt.Dimension; import java.awt.Paint; import java.awt.Point; import java.awt.Rectangle; import java.awt.geom.AffineTransform; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.Map; import org.apache.fop.afp.AFPBorderPainter; import org.apache.fop.afp.AFPPaintingState; import org.apache.fop.afp.AFPUnitConverter; import org.apache.fop.afp.AbstractAFPPainter; import org.apache.fop.afp.BorderPaintingInfo; import org.apache.fop.afp.DataStream; import org.apache.fop.afp.RectanglePaintingInfo; import org.apache.fop.afp.fonts.AFPFont; import org.apache.fop.afp.fonts.AFPFontAttributes; import org.apache.fop.afp.fonts.AFPPageFonts; import org.apache.fop.afp.fonts.CharacterSet; import org.apache.fop.afp.modca.AbstractPageObject; import org.apache.fop.afp.modca.PresentationTextObject; import org.apache.fop.afp.ptoca.PtocaBuilder; import org.apache.fop.afp.ptoca.PtocaProducer; import org.apache.fop.afp.util.AFPResourceAccessor; import org.apache.fop.fonts.Font; import org.apache.fop.fonts.FontInfo; import org.apache.fop.fonts.FontTriplet; import org.apache.fop.fonts.Typeface; import org.apache.fop.render.RenderingContext; import org.apache.fop.render.intermediate.AbstractIFPainter; import org.apache.fop.render.intermediate.BorderPainter; import org.apache.fop.render.intermediate.IFContext; import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; import org.apache.fop.render.intermediate.IFUtil; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; import org.apache.fop.util.CharUtilities; import org.apache.xmlgraphics.image.loader.ImageProcessingHints; import org.apache.xmlgraphics.image.loader.ImageSessionContext; import org.w3c.dom.Document; /** * IFPainter implementation that produces AFP (MO:DCA). */ public class AFPPainter extends AbstractIFPainter { //** logging instance */ //private static Log log = LogFactory.getLog(AFPPainter.class); private static final int X = 0; private static final int Y = 1; private final AFPDocumentHandler documentHandler; /** the border painter */ private final AFPBorderPainterAdapter borderPainter; /** the rectangle painter */ private final AbstractAFPPainter rectanglePainter; /** unit converter */ private final AFPUnitConverter unitConv; /** * Default constructor. * @param documentHandler the parent document handler */ public AFPPainter(AFPDocumentHandler documentHandler) { super(); this.documentHandler = documentHandler; this.state = IFState.create(); this.borderPainter = new AFPBorderPainterAdapter( new AFPBorderPainter(getPaintingState(), getDataStream())); this.rectanglePainter = documentHandler.createRectanglePainter(); this.unitConv = getPaintingState().getUnitConverter(); } /** {@inheritDoc} */ @Override protected IFContext getContext() { return this.documentHandler.getContext(); } FontInfo getFontInfo() { return this.documentHandler.getFontInfo(); } AFPPaintingState getPaintingState() { return this.documentHandler.getPaintingState(); } DataStream getDataStream() { return this.documentHandler.getDataStream(); } /** {@inheritDoc} */ public void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) throws IFException { //AFP doesn't support clipping, so we treat viewport like a group //this is the same code as for startGroup() try { saveGraphicsState(); concatenateTransformationMatrix(transform); } catch (IOException ioe) { throw new IFException("I/O error in startViewport()", ioe); } } /** {@inheritDoc} */ public void endViewport() throws IFException { try { restoreGraphicsState(); } catch (IOException ioe) { throw new IFException("I/O error in endViewport()", ioe); } } private void concatenateTransformationMatrix(AffineTransform at) { if (!at.isIdentity()) { getPaintingState().concatenate(at); } } /** {@inheritDoc} */ public void startGroup(AffineTransform transform) throws IFException { try { saveGraphicsState(); concatenateTransformationMatrix(transform); } catch (IOException ioe) { throw new IFException("I/O error in startGroup()", ioe); } } /** {@inheritDoc} */ public void endGroup() throws IFException { try { restoreGraphicsState(); } catch (IOException ioe) { throw new IFException("I/O error in endGroup()", ioe); } } /** {@inheritDoc} */ @Override protected Map createDefaultImageProcessingHints(ImageSessionContext sessionContext) { Map hints = super.createDefaultImageProcessingHints(sessionContext); //AFP doesn't support alpha channels hints.put(ImageProcessingHints.TRANSPARENCY_INTENT, ImageProcessingHints.TRANSPARENCY_INTENT_IGNORE); return hints; } /** {@inheritDoc} */ @Override protected RenderingContext createRenderingContext() { AFPRenderingContext psContext = new AFPRenderingContext( getUserAgent(), documentHandler.getResourceManager(), getPaintingState(), getFontInfo(), getContext().getForeignAttributes()); return psContext; } /** {@inheritDoc} */ public void drawImage(String uri, Rectangle rect) throws IFException { PageSegmentDescriptor pageSegment = documentHandler.getPageSegmentNameFor(uri); if (pageSegment != null) { float[] srcPts = {rect.x, rect.y}; int[] coords = unitConv.mpts2units(srcPts); int width = Math.round(unitConv.mpt2units(rect.width)); int height = Math.round(unitConv.mpt2units(rect.height)); getDataStream().createIncludePageSegment(pageSegment.getName(), coords[X], coords[Y], width, height); //Do we need to embed an external page segment? if (pageSegment.getURI() != null) { AFPResourceAccessor accessor = new AFPResourceAccessor( documentHandler.getUserAgent().getResourceResolver()); try { URI resourceUri = new URI(pageSegment.getURI()); documentHandler.getResourceManager().createIncludedResourceFromExternal( pageSegment.getName(), resourceUri, accessor); } catch (URISyntaxException urie) { throw new IFException("Could not handle resource url" + pageSegment.getURI(), urie); } catch (IOException ioe) { throw new IFException("Could not handle resource" + pageSegment.getURI(), ioe); } } } else { drawImageUsingURI(uri, rect); } } /** {@inheritDoc} */ public void drawImage(Document doc, Rectangle rect) throws IFException { drawImageUsingDocument(doc, rect); } /** {@inheritDoc} */ public void clipRect(Rectangle rect) throws IFException { //Not supported! } private float toPoint(int mpt) { return mpt / 1000f; } /** {@inheritDoc} */ public void fillRect(Rectangle rect, Paint fill) throws IFException { if (fill == null) { return; } if (rect.width != 0 && rect.height != 0) { if (fill instanceof Color) { getPaintingState().setColor((Color)fill); } else { throw new UnsupportedOperationException("Non-Color paints NYI"); } RectanglePaintingInfo rectanglePaintInfo = new RectanglePaintingInfo( toPoint(rect.x), toPoint(rect.y), toPoint(rect.width), toPoint(rect.height)); try { rectanglePainter.paint(rectanglePaintInfo); } catch (IOException ioe) { throw new IFException("IO error while painting rectangle", ioe); } } } /** {@inheritDoc} */ @Override public void drawBorderRect(Rectangle rect, BorderProps top, BorderProps bottom, BorderProps left, BorderProps right) throws IFException { if (top != null || bottom != null || left != null || right != null) { try { this.borderPainter.drawBorders(rect, top, bottom, left, right); } catch (IOException ife) { throw new IFException("IO error while painting borders", ife); } } } //TODO Try to resolve the name-clash between the AFPBorderPainter in the afp package //and this one. Not done for now to avoid a lot of re-implementation and code duplication. private static class AFPBorderPainterAdapter extends BorderPainter { private final AFPBorderPainter delegate; public AFPBorderPainterAdapter(AFPBorderPainter borderPainter) { this.delegate = borderPainter; } @Override protected void clip() throws IOException { //not supported by AFP } @Override protected void closePath() throws IOException { //used for clipping only, so not implemented } @Override protected void moveTo(int x, int y) throws IOException { //used for clipping only, so not implemented } @Override protected void lineTo(int x, int y) throws IOException { //used for clipping only, so not implemented } @Override protected void saveGraphicsState() throws IOException { //used for clipping only, so not implemented } @Override protected void restoreGraphicsState() throws IOException { //used for clipping only, so not implemented } private float toPoints(int mpt) { return mpt / 1000f; } @Override protected void drawBorderLine( // CSOK: ParameterNumber int x1, int y1, int x2, int y2, boolean horz, boolean startOrBefore, int style, Color color) throws IOException { BorderPaintingInfo borderPaintInfo = new BorderPaintingInfo( toPoints(x1), toPoints(y1), toPoints(x2), toPoints(y2), horz, style, color); delegate.paint(borderPaintInfo); } @Override public void drawLine(Point start, Point end, int width, Color color, RuleStyle style) throws IOException { if (start.y != end.y) { //TODO Support arbitrary lines if necessary throw new UnsupportedOperationException( "Can only deal with horizontal lines right now"); } //Simply delegates to drawBorderLine() as AFP line painting is not very sophisticated. int halfWidth = width / 2; drawBorderLine(start.x, start.y - halfWidth, end.x, start.y + halfWidth, true, true, style.getEnumValue(), color); } } /** {@inheritDoc} */ @Override public void drawLine(Point start, Point end, int width, Color color, RuleStyle style) throws IFException { try { this.borderPainter.drawLine(start, end, width, color, style); } catch (IOException ioe) { throw new IFException("I/O error in drawLine()", ioe); } } /** {@inheritDoc} */ public void drawText( // CSOK: MethodLength int x, int y, final int letterSpacing, final int wordSpacing, final int[][] dp, final String text) throws IFException { final int fontSize = this.state.getFontSize(); getPaintingState().setFontSize(fontSize); FontTriplet triplet = new FontTriplet( state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); //TODO Ignored: state.getFontVariant() String fontKey = getFontInfo().getInternalFontKey(triplet); if (fontKey == null) { triplet = new FontTriplet("any", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL); fontKey = getFontInfo().getInternalFontKey(triplet); } // register font as necessary Map fontMetricMap = documentHandler.getFontInfo().getFonts(); final AFPFont afpFont = (AFPFont)fontMetricMap.get(fontKey); final Font font = getFontInfo().getFontInstance(triplet, fontSize); AFPPageFonts pageFonts = getPaintingState().getPageFonts(); AFPFontAttributes fontAttributes = pageFonts.registerFont(fontKey, afpFont, fontSize); final int fontReference = fontAttributes.getFontReference(); final int[] coords = unitConv.mpts2units(new float[] {x, y} ); final CharacterSet charSet = afpFont.getCharacterSet(fontSize); if (afpFont.isEmbeddable()) { try { documentHandler.getResourceManager().embedFont(afpFont, charSet); } catch (IOException ioe) { throw new IFException("Error while embedding font resources", ioe); } } AbstractPageObject page = getDataStream().getCurrentPage(); PresentationTextObject pto = page.getPresentationTextObject(); try { pto.createControlSequences(new PtocaProducer() { public void produce(PtocaBuilder builder) throws IOException { Point p = getPaintingState().getPoint(coords[X], coords[Y]); builder.setTextOrientation(getPaintingState().getRotation()); builder.absoluteMoveBaseline(p.y); builder.absoluteMoveInline(p.x); builder.setExtendedTextColor(state.getTextColor()); builder.setCodedFont((byte)fontReference); int l = text.length(); int[] dx = IFUtil.convertDPToDX ( dp ); int dxl = (dx != null ? dx.length : 0); StringBuffer sb = new StringBuffer(); if (dxl > 0 && dx[0] != 0) { int dxu = Math.round(unitConv.mpt2units(dx[0])); builder.relativeMoveInline(-dxu); } //Following are two variants for glyph placement. //SVI does not seem to be implemented in the same way everywhere, so //a fallback alternative is preserved here. final boolean usePTOCAWordSpacing = true; if (usePTOCAWordSpacing) { int interCharacterAdjustment = 0; if (letterSpacing != 0) { interCharacterAdjustment = Math.round(unitConv.mpt2units( letterSpacing)); } builder.setInterCharacterAdjustment(interCharacterAdjustment); int spaceWidth = font.getCharWidth(CharUtilities.SPACE); int fixedSpaceCharacterIncrement = Math.round(unitConv.mpt2units( spaceWidth + letterSpacing)); int varSpaceCharacterIncrement = fixedSpaceCharacterIncrement; if (wordSpacing != 0) { varSpaceCharacterIncrement = Math.round(unitConv.mpt2units( spaceWidth + wordSpacing + letterSpacing)); } builder.setVariableSpaceCharacterIncrement(varSpaceCharacterIncrement); boolean fixedSpaceMode = false; for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); float glyphAdjust = 0; if (CharUtilities.isFixedWidthSpace(orgChar)) { flushText(builder, sb, charSet); builder.setVariableSpaceCharacterIncrement( fixedSpaceCharacterIncrement); fixedSpaceMode = true; sb.append(CharUtilities.SPACE); int charWidth = font.getCharWidth(orgChar); glyphAdjust += (charWidth - spaceWidth); } else { if (fixedSpaceMode) { flushText(builder, sb, charSet); builder.setVariableSpaceCharacterIncrement( varSpaceCharacterIncrement); fixedSpaceMode = false; } char ch; if (orgChar == CharUtilities.NBSPACE) { ch = ' '; //converted to normal space to allow word spacing } else { ch = orgChar; } sb.append(ch); } if (i < dxl - 1) { glyphAdjust += dx[i + 1]; } if (glyphAdjust != 0) { flushText(builder, sb, charSet); int increment = Math.round(unitConv.mpt2units(glyphAdjust)); builder.relativeMoveInline(increment); } } } else { for (int i = 0; i < l; i++) { char orgChar = text.charAt(i); float glyphAdjust = 0; if (CharUtilities.isFixedWidthSpace(orgChar)) { sb.append(CharUtilities.SPACE); int spaceWidth = font.getCharWidth(CharUtilities.SPACE); int charWidth = font.getCharWidth(orgChar); glyphAdjust += (charWidth - spaceWidth); } else { sb.append(orgChar); } if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { glyphAdjust += wordSpacing; } glyphAdjust += letterSpacing; if (i < dxl - 1) { glyphAdjust += dx[i + 1]; } if (glyphAdjust != 0) { flushText(builder, sb, charSet); int increment = Math.round(unitConv.mpt2units(glyphAdjust)); builder.relativeMoveInline(increment); } } } flushText(builder, sb, charSet); } private void flushText(PtocaBuilder builder, StringBuffer sb, final CharacterSet charSet) throws IOException { if (sb.length() > 0) { builder.addTransparentData(charSet.encodeChars(sb)); sb.setLength(0); } } }); } catch (IOException ioe) { throw new IFException("I/O error in drawText()", ioe); } } /** * Saves the graphics state of the rendering engine. * @throws IOException if an I/O error occurs */ protected void saveGraphicsState() throws IOException { getPaintingState().save(); } /** * Restores the last graphics state of the rendering engine. * @throws IOException if an I/O error occurs */ protected void restoreGraphicsState() throws IOException { getPaintingState().restore(); } }