From 4e27a1c9dd7246dff9d9380755171f179e3fcb8a Mon Sep 17 00:00:00 2001 From: Jeremias Maerki Date: Thu, 5 Feb 2009 16:27:08 +0000 Subject: [PATCH] Performance improvements and file-size reductions by introducing letter-spacing and word-spacing attributes in new IF (as mentioned on fop-dev). Allow to control whether kerning information is loaded from fonts. Started support for AFP font embedding (incomplete and currently disabled) git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/branches/Temp_AreaTreeNewDesign@741165 13f79535-47bb-0310-9956-ffa450edef68 --- .../fop-intermediate-format-ng-content.xsd | 2 + .../apache/fop/afp/AFPResourceManager.java | 127 +++++++++++---- .../org/apache/fop/afp/fonts/AFPFont.java | 8 + .../fop/afp/modca/IncludedResourceObject.java | 63 ++++++++ .../afp/util/DefaultFOPResourceAccessor.java | 60 +++++++ .../apache/fop/afp/util/ResourceAccessor.java | 40 +++++ .../fop/afp/util/SimpleResourceAccessor.java | 58 +++++++ src/java/org/apache/fop/fonts/FontLoader.java | 26 +++- src/java/org/apache/fop/fonts/LazyFont.java | 2 +- .../fop/fonts/autodetect/FontInfoFinder.java | 2 +- .../fop/fonts/truetype/TTFFontLoader.java | 12 +- .../fop/fonts/type1/Type1FontLoader.java | 15 +- .../org/apache/fop/pdf/PDFPaintingState.java | 75 ++++++--- .../org/apache/fop/render/afp/AFPPainter.java | 146 ++++++++++++++---- .../render/afp/AFPRendererConfigurator.java | 3 +- .../intermediate/AbstractIFPainter.java | 1 - .../fop/render/intermediate/IFPainter.java | 56 ++++++- .../fop/render/intermediate/IFParser.java | 7 +- .../fop/render/intermediate/IFRenderer.java | 20 ++- .../fop/render/intermediate/IFSerializer.java | 12 +- .../java2d/ConfiguredFontCollection.java | 3 +- .../fop/render/java2d/Java2DPainter.java | 10 +- .../org/apache/fop/render/pcl/PCLPainter.java | 34 ++-- .../fop/render/pdf/PDFContentGenerator.java | 10 ++ .../org/apache/fop/render/pdf/PDFPainter.java | 12 +- .../org/apache/fop/render/ps/FOPProcSet.java | 81 ++++++++++ .../fop/render/ps/PSDocumentHandler.java | 1 + .../org/apache/fop/render/ps/PSPainter.java | 119 ++++++++++---- .../org/apache/fop/render/svg/SVGPainter.java | 12 +- .../block_letter-spacing.xml | 16 +- .../standard-testcases/kerning_1_on.xml | 28 +++- 31 files changed, 879 insertions(+), 182 deletions(-) create mode 100644 src/java/org/apache/fop/afp/modca/IncludedResourceObject.java create mode 100644 src/java/org/apache/fop/afp/util/DefaultFOPResourceAccessor.java create mode 100644 src/java/org/apache/fop/afp/util/ResourceAccessor.java create mode 100644 src/java/org/apache/fop/afp/util/SimpleResourceAccessor.java create mode 100644 src/java/org/apache/fop/render/ps/FOPProcSet.java diff --git a/src/documentation/intermediate-format-ng/fop-intermediate-format-ng-content.xsd b/src/documentation/intermediate-format-ng/fop-intermediate-format-ng-content.xsd index daedf9272..c0bdd281b 100644 --- a/src/documentation/intermediate-format-ng/fop-intermediate-format-ng-content.xsd +++ b/src/documentation/intermediate-format-ng/fop-intermediate-format-ng-content.xsd @@ -61,6 +61,8 @@ + + diff --git a/src/java/org/apache/fop/afp/AFPResourceManager.java b/src/java/org/apache/fop/afp/AFPResourceManager.java index 9218ea8e4..78c06bedf 100644 --- a/src/java/org/apache/fop/afp/AFPResourceManager.java +++ b/src/java/org/apache/fop/afp/AFPResourceManager.java @@ -21,19 +21,32 @@ package org.apache.fop.afp; import java.io.IOException; import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.util.Map; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.apache.fop.afp.modca.AbstractNamedAFPObject; import org.apache.fop.afp.modca.AbstractPageObject; import org.apache.fop.afp.modca.IncludeObject; +import org.apache.fop.afp.modca.IncludedResourceObject; import org.apache.fop.afp.modca.PageSegment; import org.apache.fop.afp.modca.Registry; import org.apache.fop.afp.modca.ResourceGroup; +import org.apache.fop.afp.modca.ResourceObject; +import org.apache.fop.afp.util.ResourceAccessor; +import org.apache.fop.afp.util.SimpleResourceAccessor; /** * Manages the creation and storage of document resources */ public class AFPResourceManager { + + /** logging instance */ + private static Log log = LogFactory.getLog(AFPResourceManager.class); + /** The AFP datastream (document tree) */ private DataStream dataStream; @@ -47,8 +60,8 @@ public class AFPResourceManager { /** Maintain a reference count of instream objects for referencing purposes */ private int instreamObjectCount = 0; - /** a mapping of resourceInfo --> names of includable objects */ - private final Map/**/ includableObjectsMap + /** a mapping of resourceInfo --> include name */ + private final Map/**/ includeNameMap = new java.util.HashMap()/**/; private Map pageSegmentMap = new java.util.HashMap(); @@ -120,7 +133,7 @@ public class AFPResourceManager { AFPResourceInfo resourceInfo = dataObjectInfo.getResourceInfo(); updateResourceInfoUri(resourceInfo); - String objectName = (String)includableObjectsMap.get(resourceInfo); + String objectName = (String)includeNameMap.get(resourceInfo); if (objectName != null) { // an existing data resource so reference it by adding an include to the current page includeObject(dataObjectInfo, objectName); @@ -156,35 +169,35 @@ public class AFPResourceManager { useInclude &= resourceGroup != null; if (useInclude) { - boolean usePageSegment = dataObjectInfo.isCreatePageSegment(); - - // if it is to reside within a resource group at print-file or external level - if (resourceLevel.isPrintFile() || resourceLevel.isExternal()) { - if (usePageSegment) { - String pageSegmentName = "S10" + namedObj.getName().substring(3); - namedObj.setName(pageSegmentName); - PageSegment seg = new PageSegment(pageSegmentName); - seg.addObject(namedObj); - namedObj = seg; - } + boolean usePageSegment = dataObjectInfo.isCreatePageSegment(); - // wrap newly created data object in a resource object - namedObj = dataObjectFactory.createResource(namedObj, resourceInfo, objectType); + // if it is to reside within a resource group at print-file or external level + if (resourceLevel.isPrintFile() || resourceLevel.isExternal()) { + if (usePageSegment) { + String pageSegmentName = "S10" + namedObj.getName().substring(3); + namedObj.setName(pageSegmentName); + PageSegment seg = new PageSegment(pageSegmentName); + seg.addObject(namedObj); + namedObj = seg; } - // add data object into its resource group destination - resourceGroup.addObject(namedObj); + // wrap newly created data object in a resource object + namedObj = dataObjectFactory.createResource(namedObj, resourceInfo, objectType); + } - // create the include object - objectName = namedObj.getName(); - if (usePageSegment) { - includePageSegment(dataObjectInfo, objectName); - pageSegmentMap.put(resourceInfo, objectName); - } else { - includeObject(dataObjectInfo, objectName); - // record mapping of resource info to data object resource name - includableObjectsMap.put(resourceInfo, objectName); - } + // add data object into its resource group destination + resourceGroup.addObject(namedObj); + + // create the include object + objectName = namedObj.getName(); + if (usePageSegment) { + includePageSegment(dataObjectInfo, objectName); + pageSegmentMap.put(resourceInfo, objectName); + } else { + includeObject(dataObjectInfo, objectName); + // record mapping of resource info to data object resource name + includeNameMap.put(resourceInfo, objectName); + } } else { // not to be included so inline data object directly into the current page @@ -206,10 +219,10 @@ public class AFPResourceManager { private void includeObject(AFPDataObjectInfo dataObjectInfo, String objectName) { - IncludeObject includeObject - = dataObjectFactory.createInclude(objectName, dataObjectInfo); - dataStream.getCurrentPage().addObject(includeObject); - } + IncludeObject includeObject + = dataObjectFactory.createInclude(objectName, dataObjectInfo); + dataStream.getCurrentPage().addObject(includeObject); + } private void includePageSegment(AFPDataObjectInfo dataObjectInfo, String pageSegmentName) { @@ -220,6 +233,53 @@ public class AFPResourceManager { currentPage.createIncludePageSegment(pageSegmentName, x, y, createHardPageSegments); } + /** + * Creates an included resource object by loading the contained object from a file. + * @param resourceName the name of the resource + * @param basePath the base path in which to look for the resource files + * @param resourceObjectType the resource object type ({@link ResourceObject}.*) + * @throws IOException if an I/O error occurs while loading the resource + */ + public void createIncludedResource(String resourceName, String basePath, + byte resourceObjectType) throws IOException { + AFPResourceLevel resourceLevel = new AFPResourceLevel(AFPResourceLevel.PRINT_FILE); + URI uri; + try { + uri = new URI(resourceName.trim()); + } catch (URISyntaxException e) { + throw new IOException("Could not create URI from resource name: " + resourceName + + " (" + e.getMessage() + ")"); + } + + AFPResourceInfo resourceInfo = new AFPResourceInfo(); + resourceInfo.setLevel(resourceLevel); + resourceInfo.setName(resourceName); + resourceInfo.setUri(uri.toASCIIString()); + + String objectName = (String)includeNameMap.get(resourceInfo); + if (objectName == null) { + if (log.isDebugEnabled()) { + log.debug("Adding included resource: " + resourceName); + } + //TODO This works with local filenames only. In the long term, this + //should work through FOP's URI resolver. + ResourceAccessor accessor = new SimpleResourceAccessor(basePath); + IncludedResourceObject resourceContent = new IncludedResourceObject( + resourceName, accessor, uri); + + ResourceObject resourceObject = factory.createResource(resourceName); + resourceObject.setDataObject(resourceContent); + resourceObject.setType(resourceObjectType); + + ResourceGroup resourceGroup = streamer.getResourceGroup(resourceLevel); + resourceGroup.addObject(resourceObject); + // record mapping of resource info to data object resource name + includeNameMap.put(resourceInfo, resourceName); + } else { + //skip, already created + } + } + /** * Sets resource level defaults. The existing defaults over merged with the ones passed in * as parameter. @@ -236,4 +296,5 @@ public class AFPResourceManager { public AFPResourceLevelDefaults getResourceLevelDefaults() { return this.resourceLevelDefaults; } -} \ No newline at end of file + +} diff --git a/src/java/org/apache/fop/afp/fonts/AFPFont.java b/src/java/org/apache/fop/afp/fonts/AFPFont.java index 4801737b3..f56611087 100644 --- a/src/java/org/apache/fop/afp/fonts/AFPFont.java +++ b/src/java/org/apache/fop/afp/fonts/AFPFont.java @@ -97,6 +97,14 @@ public abstract class AFPFont extends Typeface { */ public abstract CharacterSet getCharacterSet(int size); + /** + * Indicates if this font may be embedded. + * @return True, if embedding is possible/permitted + */ + public boolean isEmbeddable() { + return false; //TODO Complete AFP font embedding + } + /** {@inheritDoc} */ public String toString() { return "name=" + name; diff --git a/src/java/org/apache/fop/afp/modca/IncludedResourceObject.java b/src/java/org/apache/fop/afp/modca/IncludedResourceObject.java new file mode 100644 index 000000000..296ab2d9d --- /dev/null +++ b/src/java/org/apache/fop/afp/modca/IncludedResourceObject.java @@ -0,0 +1,63 @@ +/* + * 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.afp.modca; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; + +import org.apache.commons.io.IOUtils; + +import org.apache.fop.afp.util.ResourceAccessor; + + +/** + * Encapsulates an included resource object that is loaded from an external file. + */ +public class IncludedResourceObject extends AbstractNamedAFPObject { + + private ResourceAccessor resourceAccessor; + private URI uri; + + /** + * Main constructor. + * @param name the name of the included resource + * @param resourceAccessor the resource accessor to load the external file with + * @param uri the URI of the external file + */ + public IncludedResourceObject(String name, + ResourceAccessor resourceAccessor, URI uri) { + super(name); + this.resourceAccessor = resourceAccessor; + this.uri = uri; + } + + /** {@inheritDoc} */ + public void writeToStream(OutputStream os) throws IOException { + InputStream in = resourceAccessor.createInputStream(this.uri); + try { + IOUtils.copy(in, os); + } finally { + IOUtils.closeQuietly(in); + } + } + +} diff --git a/src/java/org/apache/fop/afp/util/DefaultFOPResourceAccessor.java b/src/java/org/apache/fop/afp/util/DefaultFOPResourceAccessor.java new file mode 100644 index 000000000..55c8eab7d --- /dev/null +++ b/src/java/org/apache/fop/afp/util/DefaultFOPResourceAccessor.java @@ -0,0 +1,60 @@ +/* + * 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.afp.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; + +import org.apache.fop.apps.FOUserAgent; + +/** + * Default implementation of the {@link ResourceAccessor} interface for use inside FOP. + */ +public class DefaultFOPResourceAccessor implements ResourceAccessor { + + private FOUserAgent userAgent; + + /** + * Main constructor. + * @param userAgent the FO user agent + */ + public DefaultFOPResourceAccessor(FOUserAgent userAgent) { + this.userAgent = userAgent; + } + + /** {@inheritDoc} */ + public InputStream createInputStream(URI uri) throws IOException { + Source src = userAgent.resolveURI(uri.toASCIIString()); + if (src == null) { + return null; + } else if (src instanceof StreamSource) { + StreamSource ss = (StreamSource)src; + InputStream in = ss.getInputStream(); + return in; + } else { + return null; + } + } + +} diff --git a/src/java/org/apache/fop/afp/util/ResourceAccessor.java b/src/java/org/apache/fop/afp/util/ResourceAccessor.java new file mode 100644 index 000000000..6b9995c44 --- /dev/null +++ b/src/java/org/apache/fop/afp/util/ResourceAccessor.java @@ -0,0 +1,40 @@ +/* + * 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.afp.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; + +/** + * Defines an interface through which external resource objects can be accessed. + */ +public interface ResourceAccessor { + + /** + * Creates a new {@link InputStream} for the given URI that allows read access to an external + * resource. + * @param uri the URI of an external resource. + * @return the new input stream + * @throws IOException if an I/O error occurs while opening the resource + */ + InputStream createInputStream(URI uri) throws IOException; + +} diff --git a/src/java/org/apache/fop/afp/util/SimpleResourceAccessor.java b/src/java/org/apache/fop/afp/util/SimpleResourceAccessor.java new file mode 100644 index 000000000..51772c253 --- /dev/null +++ b/src/java/org/apache/fop/afp/util/SimpleResourceAccessor.java @@ -0,0 +1,58 @@ +/* + * 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.afp.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; + +/** + * Simple implementation of the {@link ResourceAccessor} interface for access via files. + */ +public class SimpleResourceAccessor implements ResourceAccessor { + + private URI baseURI; + + /** + * Creates a new simple resource accessor. + * @param basePath the base path to resolve relative URIs to + */ + public SimpleResourceAccessor(File basePath) { + this.baseURI = basePath.toURI(); + } + + /** + * Creates a new simple resource accessor. + * @param basePath the base path to resolve relative URIs to + */ + public SimpleResourceAccessor(String basePath) { + this(new File(basePath)); + } + + /** {@inheritDoc} */ + public InputStream createInputStream(URI uri) throws IOException { + URI resolved = this.baseURI.resolve(uri); + URL url = resolved.toURL(); + return url.openStream(); + } + +} diff --git a/src/java/org/apache/fop/fonts/FontLoader.java b/src/java/org/apache/fop/fonts/FontLoader.java index b3d32e38d..aaca5b3fd 100644 --- a/src/java/org/apache/fop/fonts/FontLoader.java +++ b/src/java/org/apache/fop/fonts/FontLoader.java @@ -53,16 +53,21 @@ public abstract class FontLoader { protected boolean loaded = false; /** true if the font will be embedded, false if it will be referenced only. */ protected boolean embedded = true; + /** true if kerning information shall be loaded if available. */ + protected boolean useKerning = true; /** * Default constructor. * @param fontFileURI the URI to the PFB file of a Type 1 font * @param embedded indicates whether the font is embedded or referenced + * @param useKerning indicates whether kerning information shall be loaded if available * @param resolver the font resolver used to resolve URIs */ - public FontLoader(String fontFileURI, boolean embedded, FontResolver resolver) { + public FontLoader(String fontFileURI, boolean embedded, boolean useKerning, + FontResolver resolver) { this.fontFileURI = fontFileURI; this.embedded = embedded; + this.useKerning = useKerning; this.resolver = resolver; } @@ -82,7 +87,8 @@ public abstract class FontLoader { */ public static CustomFont loadFont(File fontFile, String subFontName, boolean embedded, EncodingMode encodingMode, FontResolver resolver) throws IOException { - return loadFont(fontFile.getAbsolutePath(), subFontName, embedded, encodingMode, resolver); + return loadFont(fontFile.getAbsolutePath(), subFontName, + embedded, encodingMode, true, resolver); } /** @@ -96,8 +102,11 @@ public abstract class FontLoader { * @throws IOException In case of an I/O error */ public static CustomFont loadFont(URL fontUrl, String subFontName, - boolean embedded, EncodingMode encodingMode, FontResolver resolver) throws IOException { - return loadFont(fontUrl.toExternalForm(), subFontName, embedded, encodingMode, resolver); + boolean embedded, EncodingMode encodingMode, + FontResolver resolver) throws IOException { + return loadFont(fontUrl.toExternalForm(), subFontName, + embedded, encodingMode, true, + resolver); } /** @@ -106,12 +115,14 @@ public abstract class FontLoader { * @param subFontName the sub-fontname of a font (for TrueType Collections, null otherwise) * @param embedded indicates whether the font is embedded or referenced * @param encodingMode the requested encoding mode + * @param useKerning indicates whether kerning information should be loaded if available * @param resolver the font resolver to use when resolving URIs * @return the newly loaded font * @throws IOException In case of an I/O error */ public static CustomFont loadFont(String fontFileURI, String subFontName, - boolean embedded, EncodingMode encodingMode, FontResolver resolver) throws IOException { + boolean embedded, EncodingMode encodingMode, boolean useKerning, + FontResolver resolver) throws IOException { fontFileURI = fontFileURI.trim(); boolean type1 = isType1(fontFileURI); FontLoader loader; @@ -120,9 +131,10 @@ public abstract class FontLoader { throw new IllegalArgumentException( "CID encoding mode not supported for Type 1 fonts"); } - loader = new Type1FontLoader(fontFileURI, embedded, resolver); + loader = new Type1FontLoader(fontFileURI, embedded, useKerning, resolver); } else { - loader = new TTFFontLoader(fontFileURI, subFontName, embedded, encodingMode, resolver); + loader = new TTFFontLoader(fontFileURI, subFontName, + embedded, encodingMode, useKerning, resolver); } return loader.getFont(); } diff --git a/src/java/org/apache/fop/fonts/LazyFont.java b/src/java/org/apache/fop/fonts/LazyFont.java index c18ed6965..e5d111d38 100644 --- a/src/java/org/apache/fop/fonts/LazyFont.java +++ b/src/java/org/apache/fop/fonts/LazyFont.java @@ -132,7 +132,7 @@ public class LazyFont extends Typeface implements FontDescriptor { throw new RuntimeException("Cannot load font. No font URIs available."); } realFont = FontLoader.loadFont(fontEmbedPath, this.subFontName, - this.embedded, this.encodingMode, resolver); + this.embedded, this.encodingMode, useKerning, resolver); } if (realFont instanceof FontDescriptor) { realFontDescriptor = (FontDescriptor) realFont; diff --git a/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java b/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java index 951ebdcff..03a3e1018 100644 --- a/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java +++ b/src/java/org/apache/fop/fonts/autodetect/FontInfoFinder.java @@ -225,7 +225,7 @@ public class FontInfoFinder { } try { TTFFontLoader ttfLoader = new TTFFontLoader( - fontFileURI, fontName, true, EncodingMode.AUTO, resolver); + fontFileURI, fontName, true, EncodingMode.AUTO, true, resolver); customFont = ttfLoader.getFont(); if (this.eventListener != null) { customFont.setEventListener(this.eventListener); diff --git a/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java b/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java index cbf9c9d17..405a25f9e 100644 --- a/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java +++ b/src/java/org/apache/fop/fonts/truetype/TTFFontLoader.java @@ -55,7 +55,7 @@ public class TTFFontLoader extends FontLoader { * @param resolver the FontResolver for font URI resolution */ public TTFFontLoader(String fontFileURI, FontResolver resolver) { - this(fontFileURI, null, true, EncodingMode.AUTO, resolver); + this(fontFileURI, null, true, EncodingMode.AUTO, true, resolver); } /** @@ -65,11 +65,13 @@ public class TTFFontLoader extends FontLoader { * TrueType fonts) * @param embedded indicates whether the font is embedded or referenced * @param encodingMode the requested encoding mode + * @param useKerning true to enable loading kerning info if available, false to disable * @param resolver the FontResolver for font URI resolution */ public TTFFontLoader(String fontFileURI, String subFontName, - boolean embedded, EncodingMode encodingMode, FontResolver resolver) { - super(fontFileURI, embedded, resolver); + boolean embedded, EncodingMode encodingMode, boolean useKerning, + FontResolver resolver) { + super(fontFileURI, embedded, true, resolver); this.subFontName = subFontName; this.encodingMode = encodingMode; if (this.encodingMode == EncodingMode.AUTO) { @@ -164,7 +166,9 @@ public class TTFFontLoader extends FontLoader { copyWidthsSingleByte(ttf); } - copyKerning(ttf, isCid); + if (useKerning) { + copyKerning(ttf, isCid); + } if (this.embedded && ttf.isEmbeddable()) { returnFont.setEmbedFileName(this.fontFileURI); } diff --git a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java index 981f3ad69..1d0c75605 100644 --- a/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java +++ b/src/java/org/apache/fop/fonts/type1/Type1FontLoader.java @@ -45,12 +45,13 @@ public class Type1FontLoader extends FontLoader { * Constructs a new Type 1 font loader. * @param fontFileURI the URI to the PFB file of a Type 1 font * @param embedded indicates whether the font is embedded or referenced + * @param useKerning indicates whether to load kerning information if available * @param resolver the font resolver used to resolve URIs * @throws IOException In case of an I/O error */ - public Type1FontLoader(String fontFileURI, boolean embedded, FontResolver resolver) - throws IOException { - super(fontFileURI, embedded, resolver); + public Type1FontLoader(String fontFileURI, boolean embedded, boolean useKerning, + FontResolver resolver) throws IOException { + super(fontFileURI, embedded, useKerning, resolver); } private String getPFMURI(String pfbURI) { @@ -321,7 +322,9 @@ public class Type1FontLoader extends FontLoader { singleFont.setWidth(chm.getCharCode(), (int)Math.round(chm.getWidthX())); } } - returnFont.replaceKerningMap(afm.createXKerningMapEncoded()); + if (useKerning) { + returnFont.replaceKerningMap(afm.createXKerningMapEncoded()); + } } else { returnFont.setFlags(pfm.getFlags()); returnFont.setFirstChar(pfm.getFirstChar()); @@ -329,7 +332,9 @@ public class Type1FontLoader extends FontLoader { for (short i = pfm.getFirstChar(); i <= pfm.getLastChar(); i++) { singleFont.setWidth(i, pfm.getCharWidth(i)); } - returnFont.replaceKerningMap(pfm.getKerning()); + if (useKerning) { + returnFont.replaceKerningMap(pfm.getKerning()); + } } } diff --git a/src/java/org/apache/fop/pdf/PDFPaintingState.java b/src/java/org/apache/fop/pdf/PDFPaintingState.java index 11dfc635a..ebe2b383b 100644 --- a/src/java/org/apache/fop/pdf/PDFPaintingState.java +++ b/src/java/org/apache/fop/pdf/PDFPaintingState.java @@ -62,14 +62,15 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState * @return true if the new paint changes the current paint */ public boolean setPaint(Paint p) { - Paint paint = ((PDFData)getData()).paint; + PDFData data = getPDFData(); + Paint paint = data.paint; if (paint == null) { if (p != null) { - ((PDFData)getData()).paint = p; + data.paint = p; return true; } } else if (!paint.equals(p)) { - ((PDFData)getData()).paint = p; + data.paint = p; return true; } return false; @@ -88,7 +89,7 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState * @return true if the clip will change the current clip. */ public boolean checkClip(Shape cl) { - Shape clip = ((PDFData)getData()).clip; + Shape clip = getPDFData().clip; if (clip == null) { if (cl != null) { return true; @@ -108,16 +109,39 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState * @param cl the new clip in the current state */ public void setClip(Shape cl) { - Shape clip = ((PDFData)getData()).clip; + PDFData data = getPDFData(); + Shape clip = data.clip; if (clip != null) { Area newClip = new Area(clip); newClip.intersect(new Area(cl)); - ((PDFData)getData()).clip = new GeneralPath(newClip); + data.clip = new GeneralPath(newClip); } else { - ((PDFData)getData()).clip = cl; + data.clip = cl; } } + /** + * Sets the character spacing (Tc). + * @param value the new value + * @return true if the value was changed with respect to the previous value + */ + public boolean setCharacterSpacing(float value) { + PDFData data = getPDFData(); + if (value != data.characterSpacing) { + data.characterSpacing = value; + return true; + } + return false; + } + + /** + * Returns the current character spacing (Tc) value. + * @return the Tc value + */ + public float getCharacterSpacing() { + return getPDFData().characterSpacing; + } + /** * Get the current stack level. * @@ -149,8 +173,8 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState newState.addValues(state); } } - if (((PDFData)getData()).gstate != null) { - newState.addValues(((PDFData)getData()).gstate); + if (getPDFData().gstate != null) { + newState.addValues(getPDFData().gstate); } return newState; } @@ -177,32 +201,38 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState getStateStack().add(copy); } + private PDFData getPDFData() { + return (PDFData)getData(); + } + private class PDFData extends org.apache.fop.util.AbstractPaintingState.AbstractData { private static final long serialVersionUID = 3527950647293177764L; private Paint paint = null; private Paint backPaint = null; - private int lineCap = 0; - private int lineJoin = 0; - private float miterLimit = 0; - private boolean text = false; - private int dashOffset = 0; + //private int lineCap = 0; //Disabled the ones that are not used, yet + //private int lineJoin = 0; + //private float miterLimit = 0; + //private int dashOffset = 0; private Shape clip = null; private PDFGState gstate = null; + //text state + private float characterSpacing = 0f; + /** {@inheritDoc} */ public Object clone() { PDFData obj = (PDFData)super.clone(); obj.paint = this.paint; obj.backPaint = this.paint; - obj.lineCap = this.lineCap; - obj.lineJoin = this.lineJoin; - obj.miterLimit = this.miterLimit; - obj.text = this.text; - obj.dashOffset = this.dashOffset; + //obj.lineCap = this.lineCap; + //obj.lineJoin = this.lineJoin; + //obj.miterLimit = this.miterLimit; + //obj.dashOffset = this.dashOffset; obj.clip = this.clip; obj.gstate = this.gstate; + obj.characterSpacing = this.characterSpacing; return obj; } @@ -211,10 +241,9 @@ public class PDFPaintingState extends org.apache.fop.util.AbstractPaintingState return super.toString() + ", paint=" + paint + ", backPaint=" + backPaint - + ", lineCap=" + lineCap - + ", miterLimit=" + miterLimit - + ", text=" + text - + ", dashOffset=" + dashOffset + //+ ", lineCap=" + lineCap + //+ ", miterLimit=" + miterLimit + //+ ", dashOffset=" + dashOffset + ", clip=" + clip + ", gstate=" + gstate; } diff --git a/src/java/org/apache/fop/render/afp/AFPPainter.java b/src/java/org/apache/fop/render/afp/AFPPainter.java index d22ea8326..0c2501b87 100644 --- a/src/java/org/apache/fop/render/afp/AFPPainter.java +++ b/src/java/org/apache/fop/render/afp/AFPPainter.java @@ -30,9 +30,6 @@ import java.util.Map; import org.w3c.dom.Document; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - import org.apache.xmlgraphics.image.loader.ImageProcessingHints; import org.apache.xmlgraphics.image.loader.ImageSessionContext; @@ -49,6 +46,7 @@ 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.modca.ResourceObject; import org.apache.fop.afp.ptoca.PtocaBuilder; import org.apache.fop.afp.ptoca.PtocaProducer; import org.apache.fop.fonts.Font; @@ -69,8 +67,8 @@ import org.apache.fop.util.CharUtilities; */ public class AFPPainter extends AbstractIFPainter { - /** logging instance */ - private static Log log = LogFactory.getLog(AFPPainter.class); + //** logging instance */ + //private static Log log = LogFactory.getLog(AFPPainter.class); private static final int X = 0; private static final int Y = 1; @@ -241,7 +239,7 @@ public class AFPPainter extends AbstractIFPainter { //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 class AFPBorderPainterAdapter extends BorderPainter { + private static class AFPBorderPainterAdapter extends BorderPainter { private AFPBorderPainter delegate; @@ -312,8 +310,9 @@ public class AFPPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawText(int x, int y, final int[] dx, int[] dy, final String text) - throws IFException { + public void drawText(int x, int y, + final int letterSpacing, final int wordSpacing, final int[] dx, + final String text) throws IFException { final int fontSize = this.state.getFontSize(); getPaintingState().setFontSize(fontSize); @@ -322,8 +321,8 @@ public class AFPPainter extends AbstractIFPainter { //TODO Ignored: state.getFontVariant() String fontKey = getFontInfo().getInternalFontKey(triplet); if (fontKey == null) { - fontKey = getFontInfo().getInternalFontKey( - new FontTriplet("any", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL)); + triplet = new FontTriplet("any", Font.STYLE_NORMAL, Font.WEIGHT_NORMAL); + fontKey = getFontInfo().getInternalFontKey(triplet); } // register font as necessary @@ -339,6 +338,23 @@ public class AFPPainter extends AbstractIFPainter { final CharacterSet charSet = afpFont.getCharacterSet(fontSize); + if (afpFont.isEmbeddable()) { + try { + //Embed fonts (char sets and code pages) + //TODO This should be moved to a place where it has less performance impact + if (charSet.getPath() != null) { + documentHandler.getResourceManager().createIncludedResource( + charSet.getName(), charSet.getPath(), + ResourceObject.TYPE_FONT_CHARACTER_SET); + documentHandler.getResourceManager().createIncludedResource( + charSet.getCodePage(), charSet.getPath(), + ResourceObject.TYPE_CODE_PAGE); + } + } catch (IOException ioe) { + throw new IFException("Error while embedding font resources", ioe); + } + } + AbstractPageObject page = getDataStream().getCurrentPage(); PresentationTextObject pto = page.getPresentationTextObject(); try { @@ -350,8 +366,6 @@ public class AFPPainter extends AbstractIFPainter { builder.absoluteMoveBaseline(p.y); builder.absoluteMoveInline(p.x); - builder.setVariableSpaceCharacterIncrement(0); - builder.setInterCharacterAdjustment(0); builder.setExtendedTextColor(state.getTextColor()); builder.setCodedFont((byte)fontReference); @@ -363,33 +377,105 @@ public class AFPPainter extends AbstractIFPainter { int dxu = Math.round(unitConv.mpt2units(dx[0])); builder.relativeMoveInline(-dxu); } - 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); + + //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 (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 (glyphAdjust != 0) { - if (sb.length() > 0) { - builder.addTransparentData(charSet.encodeChars(sb)); - sb.setLength(0); + 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); } - 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); } } diff --git a/src/java/org/apache/fop/render/afp/AFPRendererConfigurator.java b/src/java/org/apache/fop/render/afp/AFPRendererConfigurator.java index fffba85a1..bd6df6136 100644 --- a/src/java/org/apache/fop/render/afp/AFPRendererConfigurator.java +++ b/src/java/org/apache/fop/render/afp/AFPRendererConfigurator.java @@ -345,7 +345,8 @@ public class AFPRendererConfigurator extends PrintRendererConfigurator LogUtil.handleException(log, e, userAgent.getFactory().validateUserConfigStrictly()); } - + } else { + fontCollections.add(new AFPFontCollection(userAgent.getEventBroadcaster(), null)); } fontManager.setup(fontInfo, diff --git a/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java b/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java index 82ea18d00..7f6cce05a 100644 --- a/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java +++ b/src/java/org/apache/fop/render/intermediate/AbstractIFPainter.java @@ -411,5 +411,4 @@ public abstract class AbstractIFPainter implements IFPainter { return new AffineTransform(matrix); } - } diff --git a/src/java/org/apache/fop/render/intermediate/IFPainter.java b/src/java/org/apache/fop/render/intermediate/IFPainter.java index d60cd24d2..6369b0251 100644 --- a/src/java/org/apache/fop/render/intermediate/IFPainter.java +++ b/src/java/org/apache/fop/render/intermediate/IFPainter.java @@ -81,13 +81,53 @@ import org.apache.fop.traits.RuleStyle; */ public interface IFPainter { - void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) throws IFException; - void startViewport(AffineTransform[] transforms, Dimension size, Rectangle clipRect) throws IFException; + /** + * Starts a new viewport, establishing a new coordinate system. A viewport has a size and + * can optionally be clipped. Corresponds to SVG's svg element. + * @param transform the transformation matrix establishing the new coordinate system + * @param size the size of the viewport + * @param clipRect the clipping rectangle (may be null) + * @throws IFException if an error occurs while handling this element + */ + void startViewport(AffineTransform transform, Dimension size, Rectangle clipRect) + throws IFException; + + /** + * Starts a new viewport, establishing a new coordinate system. A viewport has a size and + * can optionally be clipped. Corresponds to SVG's svg element. + * @param transforms a series of transformation matrices establishing the new coordinate system + * @param size the size of the viewport + * @param clipRect the clipping rectangle (may be null) + * @throws IFException if an error occurs while handling this element + */ + void startViewport(AffineTransform[] transforms, Dimension size, Rectangle clipRect) + throws IFException; //For transform, Batik's org.apache.batik.parser.TransformListHandler/Parser can be used + + /** + * Ends the current viewport and restores the previous coordinate system. + * @throws IFException if an error occurs while handling this element + */ void endViewport() throws IFException; + /** + * Starts a new group of graphical elements. Corresponds to SVG's g element. + * @param transforms a series of transformation matrices establishing the new coordinate system + * @throws IFException if an error occurs while handling this element + */ void startGroup(AffineTransform[] transforms) throws IFException; + + /** + * Starts a new group of graphical elements. Corresponds to SVG's g element. + * @param transform the transformation matrix establishing the new coordinate system + * @throws IFException if an error occurs while handling this element + */ void startGroup(AffineTransform transform) throws IFException; + + /** + * Ends the current group and restores the previous coordinate system. + * @throws IFException if an error occurs while handling this element + */ void endGroup() throws IFException; /** @@ -105,16 +145,18 @@ public interface IFPainter { /** * Draws text. The initial coordinates (x and y) point to the starting point at the normal - * baseline of the font. The arrays (dx and dy) are optional and can be used to achieve - * effects like kerning. + * baseline of the font. The parameters letterSpacing, wordSpacing and the array dx are + * optional and can be used to influence character positioning (for example, for kerning). * @param x X-coordinate of the starting point of the text * @param y Y-coordinate of the starting point of the text - * @param dx an array of adjustment values for each character in X-direction - * @param dy an array of adjustment values for each character in Y-direction + * @param letterSpacing additional spacing between characters (may be 0) + * @param wordSpacing additional spacing between words (may be 0) + * @param dx an array of adjustment values for each character in X-direction (may be null) * @param text the text * @throws IFException if an error occurs while handling this event */ - void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException; + void drawText(int x, int y, int letterSpacing, int wordSpacing, + int[] dx, String text) throws IFException; /** * Restricts the current clipping region with the given rectangle. diff --git a/src/java/org/apache/fop/render/intermediate/IFParser.java b/src/java/org/apache/fop/render/intermediate/IFParser.java index c5c88ca78..61d1838bc 100644 --- a/src/java/org/apache/fop/render/intermediate/IFParser.java +++ b/src/java/org/apache/fop/render/intermediate/IFParser.java @@ -462,9 +462,12 @@ public class IFParser implements IFConstants { public void endElement() throws IFException { int x = Integer.parseInt(lastAttributes.getValue("x")); int y = Integer.parseInt(lastAttributes.getValue("y")); + String s = lastAttributes.getValue("letter-spacing"); + int letterSpacing = (s != null ? Integer.parseInt(s) : 0); + s = lastAttributes.getValue("word-spacing"); + int wordSpacing = (s != null ? Integer.parseInt(s) : 0); int[] dx = XMLUtil.getAttributeAsIntArray(lastAttributes, "dx"); - int[] dy = XMLUtil.getAttributeAsIntArray(lastAttributes, "dy"); - painter.drawText(x, y, dx, dy, content.toString()); + painter.drawText(x, y, letterSpacing, wordSpacing, dx, content.toString()); } public boolean ignoreCharacters() { diff --git a/src/java/org/apache/fop/render/intermediate/IFRenderer.java b/src/java/org/apache/fop/render/intermediate/IFRenderer.java index abef34e22..03ec6a4c0 100644 --- a/src/java/org/apache/fop/render/intermediate/IFRenderer.java +++ b/src/java/org/apache/fop/render/intermediate/IFRenderer.java @@ -984,6 +984,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { int bl = currentBPPosition + text.getOffset() + text.getBaselineOffset(); textUtil.flush(); textUtil.setStartPosition(rx, bl); + textUtil.setSpacing(text.getTextLetterSpaceAdjust(), text.getTextWordSpaceAdjust()); super.renderText(text); textUtil.flush(); @@ -1009,9 +1010,9 @@ public class IFRenderer extends AbstractPathOrientedRenderer { AbstractTextArea textArea = (AbstractTextArea)space.getParentArea(); renderText(s, null, font, textArea); - if (space.isAdjustable()) { + if (textUtil.combined && space.isAdjustable()) { //Used for justified text, for example - int tws = ((TextArea) space.getParentArea()).getTextWordSpaceAdjust() + int tws = textArea.getTextWordSpaceAdjust() + 2 * textArea.getTextLetterSpaceAdjust(); if (tws != 0) { textUtil.adjust(tws); @@ -1042,7 +1043,7 @@ public class IFRenderer extends AbstractPathOrientedRenderer { char ch = s.charAt(i); textUtil.addChar(ch); int glyphAdjust = 0; - if (font.hasChar(ch)) { + if (textUtil.combined && font.hasChar(ch)) { int tls = (i < l - 1 ? parentArea.getTextLetterSpaceAdjust() : 0); glyphAdjust += tls; } @@ -1060,6 +1061,8 @@ public class IFRenderer extends AbstractPathOrientedRenderer { private int lastDXPos = 0; private StringBuffer text = new StringBuffer(); private int startx, starty; + private int tls, tws; + private boolean combined = false; void addChar(char ch) { text.append(ch); @@ -1092,6 +1095,11 @@ public class IFRenderer extends AbstractPathOrientedRenderer { this.starty = y; } + void setSpacing(int tls, int tws) { + this.tls = tls; + this.tws = tws; + } + void flush() { if (text.length() > 0) { try { @@ -1101,7 +1109,11 @@ public class IFRenderer extends AbstractPathOrientedRenderer { effDX = new int[size]; System.arraycopy(dx, 0, effDX, 0, size); } - painter.drawText(startx, starty, effDX, null, text.toString()); + if (combined) { + painter.drawText(startx, starty, 0, 0, effDX, text.toString()); + } else { + painter.drawText(startx, starty, tls, tws, effDX, text.toString()); + } } catch (IFException e) { handleIFException(e); } diff --git a/src/java/org/apache/fop/render/intermediate/IFSerializer.java b/src/java/org/apache/fop/render/intermediate/IFSerializer.java index 50bc5f7de..e4aa86e9e 100644 --- a/src/java/org/apache/fop/render/intermediate/IFSerializer.java +++ b/src/java/org/apache/fop/render/intermediate/IFSerializer.java @@ -508,18 +508,22 @@ public class IFSerializer extends AbstractXMLWritingIFDocumentHandler } /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { + public void drawText(int x, int y, int letterSpacing, int wordSpacing, + int[] dx, String text) throws IFException { try { AttributesImpl atts = new AttributesImpl(); XMLUtil.addAttribute(atts, XMLConstants.XML_SPACE, "preserve"); addAttribute(atts, "x", Integer.toString(x)); addAttribute(atts, "y", Integer.toString(y)); + if (letterSpacing != 0) { + addAttribute(atts, "letter-spacing", Integer.toString(letterSpacing)); + } + if (wordSpacing != 0) { + addAttribute(atts, "word-spacing", Integer.toString(wordSpacing)); + } if (dx != null) { addAttribute(atts, "dx", IFUtil.toString(dx)); } - if (dy != null) { - addAttribute(atts, "dy", IFUtil.toString(dy)); - } handler.startElement(EL_TEXT, atts); char[] chars = text.toCharArray(); handler.characters(chars, 0, chars.length); diff --git a/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java b/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java index 9f96087b9..29570e69f 100644 --- a/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java +++ b/src/java/org/apache/fop/render/java2d/ConfiguredFontCollection.java @@ -88,7 +88,8 @@ public class ConfiguredFontCollection implements FontCollection { font = new CustomFontMetricsMapper(fontMetrics, fontSource); } else { CustomFont fontMetrics = FontLoader.loadFont( - fontFile, null, true, EncodingMode.AUTO, fontResolver); + fontFile, null, true, EncodingMode.AUTO, + configFontInfo.getKerning(), fontResolver); font = new CustomFontMetricsMapper(fontMetrics); } diff --git a/src/java/org/apache/fop/render/java2d/Java2DPainter.java b/src/java/org/apache/fop/render/java2d/Java2DPainter.java index c32df4f38..55c5b8015 100644 --- a/src/java/org/apache/fop/render/java2d/Java2DPainter.java +++ b/src/java/org/apache/fop/render/java2d/Java2DPainter.java @@ -46,6 +46,7 @@ import org.apache.fop.render.intermediate.IFException; import org.apache.fop.render.intermediate.IFState; import org.apache.fop.traits.BorderProps; import org.apache.fop.traits.RuleStyle; +import org.apache.fop.util.CharUtilities; /** * {@code IFPainter} implementation that paints on a Graphics2D instance. @@ -83,6 +84,7 @@ public class Java2DPainter extends AbstractIFPainter { * @param g2d the target Graphics2D instance * @param context the IF context * @param fontInfo the font information + * @param state the IF state object */ public Java2DPainter(Graphics2D g2d, IFContext context, FontInfo fontInfo, IFState state) { super(); @@ -206,8 +208,8 @@ public class Java2DPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { - //Note: dy is currently ignored + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + throws IFException { g2dState.updateColor(state.getTextColor()); FontTriplet triplet = new FontTriplet( state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); @@ -234,6 +236,10 @@ public class Java2DPainter extends AbstractIFPainter { float glyphAdjust = 0; int cw = font.getCharWidth(orgChar); + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust += wordSpacing; + } + glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } diff --git a/src/java/org/apache/fop/render/pcl/PCLPainter.java b/src/java/org/apache/fop/render/pcl/PCLPainter.java index 03e40d549..da4f6a656 100644 --- a/src/java/org/apache/fop/render/pcl/PCLPainter.java +++ b/src/java/org/apache/fop/render/pcl/PCLPainter.java @@ -312,8 +312,8 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { - //Note: dy is currently ignored + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + throws IFException { try { FontTriplet triplet = new FontTriplet( state.getFontFamily(), state.getFontStyle(), state.getFontWeight()); @@ -324,13 +324,13 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { ? false : HardcodedFonts.setFont(gen, fontKey, state.getFontSize(), text); if (pclFont) { - drawTextNative(x, y, dx, text, triplet); + drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); } else { - drawTextAsBitmap(x, y, dx, dy, text, triplet); + drawTextAsBitmap(x, y, letterSpacing, wordSpacing, dx, text, triplet); if (DEBUG) { state.setTextColor(Color.GRAY); HardcodedFonts.setFont(gen, "F1", state.getFontSize(), text); - drawTextNative(x, y, dx, text, triplet); + drawTextNative(x, y, letterSpacing, wordSpacing, dx, text, triplet); } } } catch (IOException ioe) { @@ -338,8 +338,8 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } } - private void drawTextNative(int x, int y, int[] dx, String text, FontTriplet triplet) - throws IOException { + private void drawTextNative(int x, int y, int letterSpacing, int wordSpacing, int[] dx, + String text, FontTriplet triplet) throws IOException { Color textColor = state.getTextColor(); if (textColor != null) { gen.setTransparencyMode(true, false); @@ -376,6 +376,10 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { } sb.append(ch); + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust += wordSpacing; + } + glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } @@ -391,7 +395,9 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { private static final double SAFETY_MARGIN_FACTOR = 0.05; - private Rectangle getTextBoundingBox(int x, int y, int[] dx, int[] dy, String text, + private Rectangle getTextBoundingBox(int x, int y, + int letterSpacing, int wordSpacing, int[] dx, + String text, Font font, FontMetricsMapper metrics) { int maxAscent = metrics.getMaxAscent(font.getFontSize()) / 1000; int descent = metrics.getDescender(font.getFontSize()) / 1000; //is negative @@ -412,6 +418,10 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { float glyphAdjust = 0; int cw = font.getCharWidth(orgChar); + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust += wordSpacing; + } + glyphAdjust += letterSpacing; if (dx != null && i < dxl - 1) { glyphAdjust += dx[i + 1]; } @@ -425,7 +435,8 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { return boundingRect; } - private void drawTextAsBitmap(final int x, final int y, final int[] dx, final int[] dy, + private void drawTextAsBitmap(final int x, final int y, + final int letterSpacing, final int wordSpacing, final int[] dx, final String text, FontTriplet triplet) throws IFException { //Use Java2D to paint different fonts via bitmap final Font font = parent.getFontInfo().getFontInstance(triplet, state.getFontSize()); @@ -439,7 +450,8 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { int safetyMargin = (int)(SAFETY_MARGIN_FACTOR * font.getFontSize()); final int baselineOffset = maxAscent + safetyMargin; - final Rectangle boundingBox = getTextBoundingBox(x, y, dx, dy, text, font, mapper); + final Rectangle boundingBox = getTextBoundingBox(x, y, + letterSpacing, wordSpacing, dx, text, font, mapper); final Dimension dim = boundingBox.getSize(); Graphics2DImagePainter painter = new Graphics2DImagePainter() { @@ -462,7 +474,7 @@ public class PCLPainter extends AbstractIFPainter implements PCLConstants { Java2DPainter painter = new Java2DPainter(g2d, getContext(), parent.getFontInfo(), state); try { - painter.drawText(x, y, dx, dy, text); + painter.drawText(x, y, letterSpacing, wordSpacing, dx, text); } catch (IFException e) { //This should never happen with the Java2DPainter throw new RuntimeException("Unexpected error while painting text", e); diff --git a/src/java/org/apache/fop/render/pdf/PDFContentGenerator.java b/src/java/org/apache/fop/render/pdf/PDFContentGenerator.java index 567ef0507..45fb7ec51 100644 --- a/src/java/org/apache/fop/render/pdf/PDFContentGenerator.java +++ b/src/java/org/apache/fop/render/pdf/PDFContentGenerator.java @@ -237,6 +237,16 @@ public class PDFContentGenerator { } } + /** + * Sets the current character spacing (Tc) value. + * @param value the Tc value (in unscaled text units) + */ + public void updateCharacterSpacing(float value) { + if (getState().setCharacterSpacing(value)) { + currentStream.add(format(value) + " Tc\n"); + } + } + /** * Establishes a new foreground or fill color. * @param col the color to apply diff --git a/src/java/org/apache/fop/render/pdf/PDFPainter.java b/src/java/org/apache/fop/render/pdf/PDFPainter.java index 6386b8df6..fa00fd7b4 100644 --- a/src/java/org/apache/fop/render/pdf/PDFPainter.java +++ b/src/java/org/apache/fop/render/pdf/PDFPainter.java @@ -253,8 +253,8 @@ public class PDFPainter extends AbstractIFPainter { } /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { - //Note: dy is currently ignored + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + throws IFException { generator.updateColor(state.getTextColor(), true, null); generator.beginTextObject(); FontTriplet triplet = new FontTriplet( @@ -277,6 +277,8 @@ public class PDFPainter extends AbstractIFPainter { PDFTextUtil textutil = generator.getTextUtil(); textutil.updateTf(fontKey, fontSize, tf.isMultiByte()); + generator.updateCharacterSpacing((float)letterSpacing / 1000f); + textutil.writeTextMatrix(new AffineTransform(1, 0, 0, -1, x / 1000f, y / 1000f)); int l = text.length(); int dxl = (dx != null ? dx.length : 0); @@ -300,6 +302,9 @@ public class PDFPainter extends AbstractIFPainter { ch = (char)(ch % 256); } } + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust += wordSpacing; + } } else { if (CharUtilities.isFixedWidthSpace(orgChar)) { //Fixed width space are rendered as spaces so copy/paste works in a reader @@ -308,6 +313,9 @@ public class PDFPainter extends AbstractIFPainter { glyphAdjust = -spaceDiff; } else { ch = font.mapChar(orgChar); + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust += wordSpacing; + } } } textutil.writeTJMappedChar(ch); diff --git a/src/java/org/apache/fop/render/ps/FOPProcSet.java b/src/java/org/apache/fop/render/ps/FOPProcSet.java new file mode 100644 index 000000000..99b62a64d --- /dev/null +++ b/src/java/org/apache/fop/render/ps/FOPProcSet.java @@ -0,0 +1,81 @@ +/* + * 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.ps; + +import java.io.IOException; + +import org.apache.xmlgraphics.ps.DSCConstants; +import org.apache.xmlgraphics.ps.PSGenerator; +import org.apache.xmlgraphics.ps.PSProcSet; + +/** + * Proc Set with FOP-specific procs. + */ +public final class FOPProcSet extends PSProcSet { + + /** Singleton instance of the FOP procset */ + public static final FOPProcSet INSTANCE = new FOPProcSet(); + + private FOPProcSet() { + super("Apache FOP Std ProcSet", 1.0f, 0); + } + + /** + * Writes the procset to the PostScript file. + * @param gen the PS generator + * @throws IOException if an I/O error occurs + */ + public void writeTo(PSGenerator gen) throws IOException { + gen.writeDSCComment(DSCConstants.BEGIN_RESOURCE, + new Object[] {TYPE_PROCSET, getName(), + Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.VERSION, + new Object[] {Float.toString(getVersion()), Integer.toString(getRevision())}); + gen.writeDSCComment(DSCConstants.COPYRIGHT, "Copyright 2009 " + + "The Apache Software Foundation. " + + "License terms: http://www.apache.org/licenses/LICENSE-2.0"); + gen.writeDSCComment(DSCConstants.TITLE, + "Basic set of procedures used by Apache FOP"); + + + gen.writeln("/TJ { % Similar but not equal to PDF's TJ operator"); + gen.writeln(" {"); + gen.writeln(" dup type /stringtype eq"); + gen.writeln(" { show }"); //normal text show + gen.writeln(" { neg 1000 div 0 rmoveto }"); //negative X movement + gen.writeln(" ifelse"); + gen.writeln(" } forall"); + gen.writeln("} bd"); + + gen.writeln("/ATJ { % As TJ but adds letter-spacing"); + gen.writeln(" /ATJls exch def"); + gen.writeln(" {"); + gen.writeln(" dup type /stringtype eq"); + gen.writeln(" { ATJls 0 3 2 roll ashow }"); //normal text show + gen.writeln(" { neg 1000 div 0 rmoveto }"); //negative X movement + gen.writeln(" ifelse"); + gen.writeln(" } forall"); + gen.writeln("} bd"); + + gen.writeDSCComment(DSCConstants.END_RESOURCE); + gen.getResourceTracker().registerSuppliedResource(this); + } + +} diff --git a/src/java/org/apache/fop/render/ps/PSDocumentHandler.java b/src/java/org/apache/fop/render/ps/PSDocumentHandler.java index 7f86017c1..c3aac8cd8 100644 --- a/src/java/org/apache/fop/render/ps/PSDocumentHandler.java +++ b/src/java/org/apache/fop/render/ps/PSDocumentHandler.java @@ -199,6 +199,7 @@ public class PSDocumentHandler extends AbstractBinaryWritingIFDocumentHandler { gen.writeDSCComment(DSCConstants.BEGIN_PROLOG); PSProcSets.writeStdProcSet(gen); PSProcSets.writeEPSProcSet(gen); + FOPProcSet.INSTANCE.writeTo(gen); gen.writeDSCComment(DSCConstants.END_PROLOG); //Setup diff --git a/src/java/org/apache/fop/render/ps/PSPainter.java b/src/java/org/apache/fop/render/ps/PSPainter.java index 02fb7292c..cb88f4670 100644 --- a/src/java/org/apache/fop/render/ps/PSPainter.java +++ b/src/java/org/apache/fop/render/ps/PSPainter.java @@ -312,8 +312,33 @@ public class PSPainter extends AbstractIFPainter { } } + private String formatMptAsPt(PSGenerator gen, int value) { + return gen.formatDouble(value / 1000.0); + } + + /* Disabled: performance experiment (incomplete) + + private static final String ZEROS = "0.00"; + + private String formatMptAsPt1(int value) { + String s = Integer.toString(value); + int len = s.length(); + StringBuffer sb = new StringBuffer(); + if (len < 4) { + sb.append(ZEROS.substring(0, 5 - len)); + sb.append(s); + } else { + int dec = len - 3; + sb.append(s.substring(0, dec)); + sb.append('.'); + sb.append(s.substring(dec)); + } + return sb.toString(); + }*/ + /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { + public void drawText(int x, int y, int letterSpacing, int wordSpacing, + int[] dx, String text) throws IFException { try { //Note: dy is currently ignored PSGenerator generator = getGenerator(); @@ -334,16 +359,13 @@ public class PSPainter extends AbstractIFPainter { singleByteFont = (SingleByteFont)tf; } Font font = getFontInfo().getFontInstance(triplet, sizeMillipoints); - //String fontName = font.getFontName(); PSResource res = this.documentHandler.getPSResourceForFontKey(fontKey); generator.useFont("/" + res.getName(), fontSize); generator.getResourceTracker().notifyResourceUsageOnPage(res); - //textutil.updateTf(fontKey, fontSize, tf.isMultiByte()); - generator.writeln("1 0 0 -1 " + generator.formatDouble(x / 1000.0) - + " " + generator.formatDouble(y / 1000.0) + " Tm"); - //textutil.writeTextMatrix(new AffineTransform(1, 0, 0, -1, x / 1000f, y / 1000f)); + generator.writeln("1 0 0 -1 " + formatMptAsPt(generator, x) + + " " + formatMptAsPt(generator, y) + " Tm"); int textLen = text.length(); if (singleByteFont != null && singleByteFont.hasAdditionalEncodings()) { @@ -356,7 +378,8 @@ public class PSPainter extends AbstractIFPainter { int encoding = mapped / 256; if (currentEncoding != encoding) { if (i > 0) { - writeText(text, start, i - start, dx, dy, font, tf); + writeText(text, start, i - start, + letterSpacing, wordSpacing, dx, font, tf); } if (encoding == 0) { useFont(fontKey, sizeMillipoints); @@ -367,62 +390,98 @@ public class PSPainter extends AbstractIFPainter { start = i; } } - writeText(text, start, textLen - start, dx, dy, font, tf); + writeText(text, start, textLen - start, + letterSpacing, wordSpacing, dx, font, tf); } else { //Simple single-font painting useFont(fontKey, sizeMillipoints); - writeText(text, 0, textLen, dx, dy, font, tf); + writeText(text, 0, textLen, + letterSpacing, wordSpacing, dx, font, tf); } } catch (IOException ioe) { throw new IFException("I/O error in drawText()", ioe); } } - private void writeText(String text, int start, int len, int[] dx, int[] dy, + private void writeText(String text, int start, int len, + int letterSpacing, int wordSpacing, int[] dx, Font font, Typeface tf) throws IOException { PSGenerator generator = getGenerator(); int end = start + len; int initialSize = len; initialSize += initialSize / 2; + + boolean hasLetterSpacing = (letterSpacing != 0); + boolean needTJ = false; + + int lineStart = 0; + StringBuffer accText = new StringBuffer(initialSize); StringBuffer sb = new StringBuffer(initialSize); - sb.append("("); - int[] offsets = new int[len]; int dxl = (dx != null ? dx.length : 0); for (int i = start; i < end; i++) { char orgChar = text.charAt(i); char ch; int cw; + int glyphAdjust = 0; if (CharUtilities.isFixedWidthSpace(orgChar)) { //Fixed width space are rendered as spaces so copy/paste works in a reader ch = font.mapChar(CharUtilities.SPACE); - //int spaceDiff = font.getCharWidth(ch) - font.getCharWidth(orgChar); - //glyphAdjust = -(spaceDiff); + cw = font.getCharWidth(orgChar); + glyphAdjust = font.getCharWidth(ch) - cw; } else { + if ((wordSpacing != 0) && CharUtilities.isAdjustableSpace(orgChar)) { + glyphAdjust -= wordSpacing; + } ch = font.mapChar(orgChar); - //cw = tf.getWidth(ch, font.getFontSize()) / 1000; + cw = font.getCharWidth(orgChar); } - cw = font.getCharWidth(orgChar); - int glyphAdjust = 0; if (dx != null && i < dxl - 1) { - glyphAdjust += dx[i + 1]; + glyphAdjust -= dx[i + 1]; } - offsets[i - start] = cw + glyphAdjust; char codepoint = (char)(ch % 256); - PSGenerator.escapeChar(codepoint, sb); - } - sb.append(")" + PSGenerator.LF + "["); - for (int i = 0; i < len; i++) { - if (i > 0) { - if (i % 8 == 0) { - sb.append(PSGenerator.LF); - } else { - sb.append(" "); + PSGenerator.escapeChar(codepoint, accText); //add character to accumulated text + if (glyphAdjust != 0) { + needTJ = true; + if (sb.length() == 0) { + sb.append('['); //Need to start TJ + } + if (accText.length() > 0) { + if ((sb.length() - lineStart + accText.length()) > 200) { + sb.append(PSGenerator.LF); + lineStart = sb.length(); + } + sb.append('('); + sb.append(accText); + sb.append(") "); + accText.setLength(0); //reset accumulated text } + sb.append(Integer.toString(glyphAdjust)).append(' '); + } + } + if (needTJ) { + if (accText.length() > 0) { + sb.append('('); + sb.append(accText); + sb.append(')'); + } + if (hasLetterSpacing) { + sb.append("] " + formatMptAsPt(generator, letterSpacing) + " ATJ"); + } else { + sb.append("] TJ"); + } + } else { + sb.append('(').append(accText).append(")"); + if (hasLetterSpacing) { + StringBuffer spb = new StringBuffer(); + spb.append(formatMptAsPt(generator, letterSpacing)) + .append(" 0 "); + sb.insert(0, spb.toString()); + sb.append(" ashow"); + } else { + sb.append(" show"); } - sb.append(generator.formatDouble(offsets[i] / 1000f)); } - sb.append("]" + PSGenerator.LF + "xshow"); generator.writeln(sb.toString()); } diff --git a/src/sandbox/org/apache/fop/render/svg/SVGPainter.java b/src/sandbox/org/apache/fop/render/svg/SVGPainter.java index d427096be..0ee13c021 100644 --- a/src/sandbox/org/apache/fop/render/svg/SVGPainter.java +++ b/src/sandbox/org/apache/fop/render/svg/SVGPainter.java @@ -325,19 +325,23 @@ public class SVGPainter extends AbstractIFPainter implements SVGConstants { } /** {@inheritDoc} */ - public void drawText(int x, int y, int[] dx, int[] dy, String text) throws IFException { + public void drawText(int x, int y, int letterSpacing, int wordSpacing, int[] dx, String text) + throws IFException { try { establish(MODE_TEXT); AttributesImpl atts = new AttributesImpl(); XMLUtil.addAttribute(atts, XMLConstants.XML_SPACE, "preserve"); XMLUtil.addAttribute(atts, "x", Integer.toString(x)); XMLUtil.addAttribute(atts, "y", Integer.toString(y)); + if (letterSpacing != 0) { + XMLUtil.addAttribute(atts, "letter-spacing", Integer.toString(letterSpacing)); + } + if (wordSpacing != 0) { + XMLUtil.addAttribute(atts, "word-spacing", Integer.toString(wordSpacing)); + } if (dx != null) { XMLUtil.addAttribute(atts, "dx", IFUtil.toString(dx)); } - if (dy != null) { - XMLUtil.addAttribute(atts, "dy", IFUtil.toString(dy)); - } handler.startElement("text", atts); char[] chars = text.toCharArray(); handler.characters(chars, 0, chars.length); diff --git a/test/layoutengine/standard-testcases/block_letter-spacing.xml b/test/layoutengine/standard-testcases/block_letter-spacing.xml index 73f9b8e3a..092cac755 100644 --- a/test/layoutengine/standard-testcases/block_letter-spacing.xml +++ b/test/layoutengine/standard-testcases/block_letter-spacing.xml @@ -71,11 +71,23 @@ - + + + - + + + + + + + + + + + diff --git a/test/layoutengine/standard-testcases/kerning_1_on.xml b/test/layoutengine/standard-testcases/kerning_1_on.xml index df74f09c5..b7410bab1 100644 --- a/test/layoutengine/standard-testcases/kerning_1_on.xml +++ b/test/layoutengine/standard-testcases/kerning_1_on.xml @@ -69,37 +69,51 @@ + + - + + + - - + + + + + + - - + + + + - + + + - + + + -- 2.39.5