mirror of https://github.com/apache/poi.git
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:
parent
fab3636895
commit
67c8cdac99
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
@ -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(){
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue