misc improvements in text rendering in xlsf

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1203143 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Yegor Kozlov 2011-11-17 10:33:59 +00:00
parent fab3636895
commit 67c8cdac99
9 changed files with 273 additions and 55 deletions

View File

@ -431,6 +431,8 @@ class RenderableShape {
float lineWidth = (float) _shape.getLineWidth();
if(lineWidth == 0.0f) lineWidth = 0.25f; // Both PowerPoint and OOo draw zero-length lines as 0.25pt
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);
if(fontScale != null) lineWidth *= fontScale.floatValue();
LineDash lineDash = _shape.getLineDash();
float[] dash = null;

View File

@ -0,0 +1,88 @@
/*
* ====================================================================
* 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.
* ====================================================================
*/
package org.apache.poi.xslf.usermodel;
import java.awt.*;
import java.awt.font.TextLayout;
import java.text.AttributedCharacterIterator;
import java.text.AttributedString;
/**
* a renderable text fragment
*/
class TextFragment {
final TextLayout _layout;
final AttributedString _str;
TextFragment(TextLayout layout, AttributedString str){
_layout = layout;
_str = str;
}
void draw(Graphics2D graphics, double x, double y){
if(_str == null) {
return;
}
double yBaseline = y + _layout.getAscent();
Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE);
if(textMode != null && textMode == XSLFRenderingHint.TEXT_AS_SHAPES){
_layout.draw(graphics, (float)x, (float)yBaseline);
} else {
graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline );
}
}
/**
* @return full height of this text run which is sum of ascent, descent and leading
*/
public float getHeight(){
return _layout.getAscent() + _layout.getDescent() + _layout.getLeading();
}
/**
*
* @return width if this text run
*/
public float getWidth(){
return _layout.getAdvance();
}
/**
*
* @return the string to be painted
*/
public String getString(){
if(_str == null) return "";
AttributedCharacterIterator it = _str.getIterator();
StringBuffer buf = new StringBuffer();
for (char c = it.first(); c != it.DONE; c = it.next()) {
buf.append(c);
}
return buf.toString();
}
@Override
public String toString(){
return "[" + getClass().getSimpleName() + "] " + getString();
}
}

View File

@ -0,0 +1,39 @@
/*
* ====================================================================
* 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.
* ====================================================================
*/
package org.apache.poi.xslf.usermodel;
/**
* Manages fonts when rendering slides.
*
* Use this class to handle unknown / missing fonts or to substitute fonts
*/
public interface XSLFFontManager {
/**
* select a font to be used to paint text
*
* @param family the font family as defined in the .pptx file.
* This can be unknown or missing in the graphic environment.
*
* @return the font to be used to paint text
*/
String getRendererableFont(String typeface, int pitchFamily);
}

View File

@ -283,8 +283,8 @@ public class XSLFGroupShape extends XSLFShape {
double scaleY = exterior.getHeight() / interior.getHeight();
// group transform scales shapes but not fonts
Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE);
graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, Math.abs(1/scaleY));
Number prevFontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);
graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, Math.abs(1/scaleY));
graphics.scale(scaleX, scaleY);
graphics.translate(-interior.getX(), -interior.getY());
@ -302,7 +302,7 @@ public class XSLFGroupShape extends XSLFShape {
graphics.setRenderingHint(XSLFRenderingHint.GRESTORE, true);
}
graphics.setRenderingHint(XSLFRenderingHint.FONT_SCALE, prevFontScale);
graphics.setRenderingHint(XSLFRenderingHint.GROUP_SCALE, prevFontScale);
}

View File

@ -51,12 +51,12 @@ public class XSLFRenderingHint extends RenderingHints.Key {
/**
* how to render text:
*
* {@link #TEXT_MODE_CHARACTERS} (default) means to draw via
* {@link #TEXT_AS_CHARACTERS} (default) means to draw via
* {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)}.
* This mode draws text as characters. Use it if the target graphics writes the actual
* character codes instead of glyph outlines (PDFGraphics2D, SVGGraphics2D, etc.)
*
* {@link #TEXT_MODE_GLYPHS} means to render via
* {@link #TEXT_AS_SHAPES} means to render via
* {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)}.
* This mode draws glyphs as shapes and provides some advanced capabilities such as
* justification and font substitution. Use it if the target graphics is an image.
@ -67,13 +67,19 @@ public class XSLFRenderingHint extends RenderingHints.Key {
/**
* draw text via {@link java.awt.Graphics2D#drawString(java.text.AttributedCharacterIterator, float, float)}
*/
public static final int TEXT_MODE_CHARACTERS = 1;
public static final int TEXT_AS_CHARACTERS = 1;
/**
* draw text via {@link java.awt.font.TextLayout#draw(java.awt.Graphics2D, float, float)}
*/
public static final int TEXT_MODE_GLYPHS = 2;
public static final int TEXT_AS_SHAPES = 2;
@Internal
public static final XSLFRenderingHint FONT_SCALE = new XSLFRenderingHint(5);
}
static final XSLFRenderingHint GROUP_SCALE = new XSLFRenderingHint(5);
/**
* Use this object to resolve unknown / missing fonts when rendering slides
*/
public static final XSLFRenderingHint FONT_HANDLER = new XSLFRenderingHint(6);
}

View File

@ -719,7 +719,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
if(spacing > 0) {
// If linespacing >= 0, then linespacing is a percentage of normal line height.
penY += spacing*0.01* _maxLineHeight;
penY += spacing*0.01* line.getHeight();
} else {
// positive value means absolute spacing in points
penY += -spacing;
@ -731,41 +731,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
return penY - y;
}
static class TextFragment {
private TextLayout _layout;
private AttributedString _str;
TextFragment(TextLayout layout, AttributedString str){
_layout = layout;
_str = str;
}
void draw(Graphics2D graphics, double x, double y){
double yBaseline = y + _layout.getAscent();
Integer textMode = (Integer)graphics.getRenderingHint(XSLFRenderingHint.TEXT_RENDERING_MODE);
if(textMode != null && textMode == XSLFRenderingHint.TEXT_MODE_GLYPHS){
_layout.draw(graphics, (float)x, (float)yBaseline);
} else {
graphics.drawString(_str.getIterator(), (float)x, (float)yBaseline );
}
}
public float getHeight(){
return _layout.getAscent() + _layout.getDescent() + _layout.getLeading();
}
public float getWidth(){
return _layout.getAdvance();
}
}
AttributedString getAttributedString(Graphics2D graphics){
String text = getRenderableText();
AttributedString string = new AttributedString(text);
XSLFFontManager fontHandler = (XSLFFontManager)graphics.getRenderingHint(XSLFRenderingHint.FONT_HANDLER);
int startIndex = 0;
for (XSLFTextRun run : _runs){
int length = run.getRenderableText().length();
@ -777,11 +750,15 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
string.addAttribute(TextAttribute.FOREGROUND, run.getFontColor(), startIndex, endIndex);
// user can pass an object to convert fonts via a rendering hint
string.addAttribute(TextAttribute.FAMILY, run.getFontFamily(), startIndex, endIndex);
// user can pass an custom object to convert fonts
String fontFamily = run.getFontFamily();
if(fontHandler != null) {
fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily());
}
string.addAttribute(TextAttribute.FAMILY, fontFamily, startIndex, endIndex);
float fontSz = (float)run.getFontSize();
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.FONT_SCALE);
Number fontScale = (Number)graphics.getRenderingHint(XSLFRenderingHint.GROUP_SCALE);
if(fontScale != null) fontSz *= fontScale.floatValue();
string.addAttribute(TextAttribute.SIZE, fontSz , startIndex, endIndex);
@ -813,7 +790,8 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
}
/**
* ensure that the paragraph contains at least one character
* ensure that the paragraph contains at least one character.
* We need this trick to correctly measure text
*/
private void ensureNotEmpty(){
XSLFTextRun r = addNewTextRun();
@ -824,7 +802,14 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
}
}
void breakText(Graphics2D graphics){
/**
* break text into lines
*
* @param graphics
* @return array of text fragments,
* each representing a line of text that fits in the wrapping width
*/
List<TextFragment> breakText(Graphics2D graphics){
_lines = new ArrayList<TextFragment>();
// does this paragraph contain text?
@ -834,15 +819,16 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
if(_runs.size() == 0) ensureNotEmpty();
String text = getRenderableText();
if(text.length() == 0) return;
if(text.length() == 0) return _lines;
AttributedString at = getAttributedString(graphics);
AttributedCharacterIterator it = at.getIterator();
LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());
for (;;) {
int startIndex = measurer.getPosition();
double wrappingWidth = getWrappingWidth(_lines.size() == 0) + 1; // add a pixel to compensate rounding errors
// shape width can be smaller that the sum of insets (proved by a test file)
// shape width can be smaller that the sum of insets (this was proved by a test file)
if(wrappingWidth < 0) wrappingWidth = 1;
int nextBreak = text.indexOf('\n', startIndex + 1);
@ -861,14 +847,22 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) {
layout = layout.getJustifiedLayout((float)wrappingWidth);
}
// skip over new line breaks (we paint 'clear' text runs not starting or ending with \n)
if(endIndex < it.getEndIndex() && text.charAt(endIndex) == '\n'){
measurer.setPosition(endIndex + 1);
}
AttributedString str = new AttributedString(it, startIndex, endIndex);
TextFragment line = new TextFragment(layout, str);
TextFragment line = new TextFragment(
layout, // we will not paint empty paragraphs
emptyParagraph ? null : str);
_lines.add(line);
_maxLineHeight = Math.max(_maxLineHeight, line.getHeight());
if(endIndex == it.getEndIndex()) break;
}
if(isBullet() && !emptyParagraph) {
@ -897,7 +891,7 @@ public class XSLFTextParagraph implements Iterable<XSLFTextRun>{
_bullet = new TextFragment(layout, str);
}
}
return _lines;
}
CTTextParagraphProperties getDefaultStyle(){

View File

@ -63,15 +63,15 @@ public class XSLFTextRun {
String getRenderableText(){
String txt = _r.getT();
TextCap cap = getTextCap();
StringBuffer buf = new StringBuffer();
for(int i = 0; i < txt.length(); i++) {
char c = txt.charAt(i);
if(c == '\t') {
// replace tab with the effective number of white spaces
// TODO: finish support for tabs
buf.append(" ");
} else {
switch (getTextCap()){
switch (cap){
case ALL:
buf.append(Character.toUpperCase(c));
break;
@ -268,6 +268,24 @@ public class XSLFTextRun {
return visitor.getValue();
}
public byte getPitchAndFamily(){
final XSLFTheme theme = _p.getParentShape().getSheet().getTheme();
CharacterPropertyFetcher<Byte> visitor = new CharacterPropertyFetcher<Byte>(_p.getLevel()){
public boolean fetch(CTTextCharacterProperties props){
CTTextFont font = props.getLatin();
if(font != null){
setValue(font.getPitchFamily());
return true;
}
return false;
}
};
fetchCharacterProperty(visitor);
return visitor.getValue() == null ? 0 : visitor.getValue();
}
/**
* Specifies whether a run of text will be formatted as strikethrough text.
*

View File

@ -493,7 +493,7 @@ public abstract class XSLFTextShape extends XSLFSimpleShape implements Iterable<
double y0 = y;
for(int i = 0; i < _paragraphs.size(); i++){
XSLFTextParagraph p = _paragraphs.get(i);
List<XSLFTextParagraph.TextFragment> lines = p.getTextLines();
List<TextFragment> lines = p.getTextLines();
if(i > 0 && lines.size() > 0) {
// the amount of vertical white space before the paragraph

View File

@ -2,9 +2,10 @@ package org.apache.poi.xslf.usermodel;
import junit.framework.TestCase;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.List;
/**
* Created by IntelliJ IDEA.
@ -98,4 +99,74 @@ public class TestXSLFTextParagraph extends TestCase {
assertEquals(244.0, expectedWidth); // 300 - 10 - 10 - 36
assertEquals(expectedWidth, p.getWrappingWidth(false));
}
public void testBreakLines(){
XMLSlideShow ppt = new XMLSlideShow();
XSLFSlide slide = ppt.createSlide();
XSLFTextShape sh = slide.createAutoShape();
XSLFTextParagraph p = sh.addNewTextParagraph();
XSLFTextRun r = p.addNewTextRun();
r.setFontFamily("serif"); // this should always be available
r.setFontSize(12);
r.setText(
"Paragraph formatting allows for more granular control " +
"of text within a shape. Properties here apply to all text " +
"residing within the corresponding paragraph.");
sh.setAnchor(new Rectangle(50, 50, 300, 200));
BufferedImage img = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
Graphics2D graphics = img.createGraphics();
List<TextFragment> lines;
lines = p.breakText(graphics);
assertEquals(3, lines.size());
// descrease the shape width from 300 pt to 100 pt
sh.setAnchor(new Rectangle(50, 50, 100, 200));
lines = p.breakText(graphics);
assertEquals(10, lines.size());
// descrease the shape width from 300 pt to 100 pt
sh.setAnchor(new Rectangle(50, 50, 600, 200));
lines = p.breakText(graphics);
assertEquals(2, lines.size());
// set left and right margins to 200pt. This leaves 200pt for wrapping text
sh.setLeftInset(200);
sh.setRightInset(200);
lines = p.breakText(graphics);
assertEquals(4, lines.size());
r.setText("Apache POI");
lines = p.breakText(graphics);
assertEquals(1, lines.size());
assertEquals("Apache POI", lines.get(0).getString());
r.setText("Apache\nPOI");
lines = p.breakText(graphics);
assertEquals(2, lines.size());
assertEquals("Apache", lines.get(0).getString());
assertEquals("POI", lines.get(1).getString());
XSLFAutoShape sh2 = slide.createAutoShape();
sh2.setAnchor(new Rectangle(50, 50, 300, 200));
XSLFTextParagraph p2 = sh2.addNewTextParagraph();
XSLFTextRun r2 = p2.addNewTextRun();
r2.setFontFamily("serif"); // this should always be available
r2.setFontSize(30);
r2.setText("Apache\n");
XSLFTextRun r3 = p2.addNewTextRun();
r3.setFontFamily("serif"); // this should always be available
r3.setFontSize(10);
r3.setText("POI");
lines = p2.breakText(graphics);
assertEquals(2, lines.size());
assertEquals("Apache", lines.get(0).getString());
assertEquals("POI", lines.get(1).getString());
// the first line is at least two times higher than the second
assertTrue(lines.get(0).getHeight() > lines.get(1).getHeight()*2);
}
}