123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593 |
- /* ====================================================================
- 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.sl.draw;
-
- import java.awt.Graphics2D;
- import java.awt.Paint;
- import java.awt.font.FontRenderContext;
- import java.awt.font.LineBreakMeasurer;
- import java.awt.font.TextAttribute;
- import java.awt.font.TextLayout;
- import java.awt.geom.Rectangle2D;
- import java.io.InvalidObjectException;
- import java.text.AttributedCharacterIterator;
- import java.text.AttributedCharacterIterator.Attribute;
- import java.text.AttributedString;
- import java.util.ArrayList;
- import java.util.List;
- import java.util.Map;
-
- import org.apache.poi.sl.usermodel.AutoNumberingScheme;
- import org.apache.poi.sl.usermodel.Hyperlink;
- import org.apache.poi.sl.usermodel.Insets2D;
- import org.apache.poi.sl.usermodel.PaintStyle;
- import org.apache.poi.sl.usermodel.PlaceableShape;
- import org.apache.poi.sl.usermodel.ShapeContainer;
- import org.apache.poi.sl.usermodel.TextParagraph;
- import org.apache.poi.sl.usermodel.TextParagraph.BulletStyle;
- import org.apache.poi.sl.usermodel.TextParagraph.TextAlign;
- import org.apache.poi.sl.usermodel.TextRun;
- import org.apache.poi.sl.usermodel.TextRun.TextCap;
- import org.apache.poi.sl.usermodel.TextShape;
- import org.apache.poi.util.StringUtil;
- import org.apache.poi.util.Units;
-
- public class DrawTextParagraph implements Drawable {
- /** Keys for passing hyperlinks to the graphics context */
- public static final XlinkAttribute HYPERLINK_HREF = new XlinkAttribute("href");
- public static final XlinkAttribute HYPERLINK_LABEL = new XlinkAttribute("label");
-
- protected TextParagraph<?,?,?> paragraph;
- double x, y;
- protected List<DrawTextFragment> lines = new ArrayList<DrawTextFragment>();
- protected String rawText;
- protected DrawTextFragment bullet;
- protected int autoNbrIdx = 0;
-
- /**
- * the highest line in this paragraph. Used for line spacing.
- */
- protected double maxLineHeight;
-
- /**
- * Defines an attribute used for storing the hyperlink associated with
- * some renderable text.
- */
- private static class XlinkAttribute extends Attribute {
-
- XlinkAttribute(String name) {
- super(name);
- }
-
- /**
- * Resolves instances being deserialized to the predefined constants.
- */
- protected Object readResolve() throws InvalidObjectException {
- if (HYPERLINK_HREF.getName().equals(getName())) {
- return HYPERLINK_HREF;
- }
- if (HYPERLINK_LABEL.getName().equals(getName())) {
- return HYPERLINK_LABEL;
- }
- throw new InvalidObjectException("unknown attribute name");
- }
- }
-
-
- public DrawTextParagraph(TextParagraph<?,?,?> paragraph) {
- this.paragraph = paragraph;
- }
-
- public void setPosition(double x, double y) {
- // TODO: replace it, by applyTransform????
- this.x = x;
- this.y = y;
- }
-
- public double getY() {
- return y;
- }
-
- /**
- * Sets the auto numbering index of the handled paragraph
- * @param index the auto numbering index
- */
- public void setAutoNumberingIdx(int index) {
- autoNbrIdx = index;
- }
-
- public void draw(Graphics2D graphics){
- if (lines.isEmpty()) return;
-
- double penY = y;
-
- boolean firstLine = true;
- int indentLevel = paragraph.getIndentLevel();
- Double leftMargin = paragraph.getLeftMargin();
- if (leftMargin == null) {
- // if the marL attribute is omitted, then a value of 347663 is implied
- leftMargin = Units.toPoints(347663*indentLevel);
- }
- Double indent = paragraph.getIndent();
- if (indent == null) {
- indent = Units.toPoints(347663*indentLevel);
- }
- if (isHSLF()) {
- // special handling for HSLF
- indent -= leftMargin;
- }
-
- // Double rightMargin = paragraph.getRightMargin();
- // if (rightMargin == null) {
- // rightMargin = 0d;
- // }
-
- //The vertical line spacing
- Double spacing = paragraph.getLineSpacing();
- if (spacing == null) spacing = 100d;
-
- for(DrawTextFragment line : lines){
- double penX;
-
- if(firstLine) {
- if (!isEmptyParagraph()) {
- // TODO: find out character style for empty, but bulleted/numbered lines
- bullet = getBullet(graphics, line.getAttributedString().getIterator());
- }
-
- if (bullet != null){
- bullet.setPosition(x+leftMargin+indent, penY);
- bullet.draw(graphics);
- // don't let text overlay the bullet and advance by the bullet width
- double bulletWidth = bullet.getLayout().getAdvance() + 1;
- penX = x + Math.max(leftMargin, leftMargin+indent+bulletWidth);
- } else {
- penX = x + leftMargin;
- }
- } else {
- penX = x + leftMargin;
- }
-
- Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape());
- // Insets are already applied on DrawTextShape.drawContent
- // but (outer) anchor need to be adjusted
- Insets2D insets = paragraph.getParentShape().getInsets();
- double leftInset = insets.left;
- double rightInset = insets.right;
-
- TextAlign ta = paragraph.getTextAlign();
- if (ta == null) ta = TextAlign.LEFT;
- switch (ta) {
- case CENTER:
- penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset - leftMargin) / 2;
- break;
- case RIGHT:
- penX += (anchor.getWidth() - line.getWidth() - leftInset - rightInset);
- break;
- default:
- break;
- }
-
- line.setPosition(penX, penY);
- line.draw(graphics);
-
- if(spacing > 0) {
- // If linespacing >= 0, then linespacing is a percentage of normal line height.
- penY += spacing*0.01* line.getHeight();
- } else {
- // negative value means absolute spacing in points
- penY += -spacing;
- }
-
- firstLine = false;
- }
-
- y = penY - y;
- }
-
- public float getFirstLineHeight() {
- return (lines.isEmpty()) ? 0 : lines.get(0).getHeight();
- }
-
- public float getLastLineHeight() {
- return (lines.isEmpty()) ? 0 : lines.get(lines.size()-1).getHeight();
- }
-
- public boolean isEmptyParagraph() {
- return (lines.isEmpty() || rawText.trim().isEmpty());
- }
-
- public void applyTransform(Graphics2D graphics) {
- }
-
- public void drawContent(Graphics2D graphics) {
- }
-
- /**
- * break text into lines, each representing a line of text that fits in the wrapping width
- *
- * @param graphics
- */
- protected void breakText(Graphics2D graphics){
- lines.clear();
-
- DrawFactory fact = DrawFactory.getInstance(graphics);
- StringBuilder text = new StringBuilder();
- AttributedString at = getAttributedString(graphics, text);
- boolean emptyParagraph = ("".equals(text.toString().trim()));
-
- AttributedCharacterIterator it = at.getIterator();
- LineBreakMeasurer measurer = new LineBreakMeasurer(it, graphics.getFontRenderContext());
- for (;;) {
- int startIndex = measurer.getPosition();
-
- double wrappingWidth = getWrappingWidth(lines.size() == 0, graphics) + 1; // add a pixel to compensate rounding errors
- // 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);
- if (nextBreak == -1) nextBreak = it.getEndIndex();
-
- TextLayout layout = measurer.nextLayout((float)wrappingWidth, nextBreak, true);
- if (layout == null) {
- // layout can be null if the entire word at the current position
- // does not fit within the wrapping width. Try with requireNextWord=false.
- layout = measurer.nextLayout((float)wrappingWidth, nextBreak, false);
- }
-
- if(layout == null) {
- // exit if can't break any more
- break;
- }
-
- int endIndex = measurer.getPosition();
- // 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);
- }
-
- TextAlign hAlign = paragraph.getTextAlign();
- if(hAlign == TextAlign.JUSTIFY || hAlign == TextAlign.JUSTIFY_LOW) {
- layout = layout.getJustifiedLayout((float)wrappingWidth);
- }
-
- AttributedString str = (emptyParagraph)
- ? null // we will not paint empty paragraphs
- : new AttributedString(it, startIndex, endIndex);
- DrawTextFragment line = fact.getTextFragment(layout, str);
- lines.add(line);
-
- maxLineHeight = Math.max(maxLineHeight, line.getHeight());
-
- if(endIndex == it.getEndIndex()) break;
- }
-
- rawText = text.toString();
- }
-
- protected DrawTextFragment getBullet(Graphics2D graphics, AttributedCharacterIterator firstLineAttr) {
- BulletStyle bulletStyle = paragraph.getBulletStyle();
- if (bulletStyle == null) return null;
-
- String buCharacter;
- AutoNumberingScheme ans = bulletStyle.getAutoNumberingScheme();
- if (ans != null) {
- buCharacter = ans.format(autoNbrIdx);
- } else {
- buCharacter = bulletStyle.getBulletCharacter();
- }
- if (buCharacter == null) return null;
-
- String buFont = bulletStyle.getBulletFont();
- if (buFont == null) buFont = paragraph.getDefaultFontFamily();
- assert(buFont != null);
-
- PlaceableShape<?,?> ps = getParagraphShape();
- PaintStyle fgPaintStyle = bulletStyle.getBulletFontColor();
- Paint fgPaint;
- if (fgPaintStyle == null) {
- fgPaint = (Paint)firstLineAttr.getAttribute(TextAttribute.FOREGROUND);
- } else {
- fgPaint = new DrawPaint(ps).getPaint(graphics, fgPaintStyle);
- }
-
- float fontSize = (Float)firstLineAttr.getAttribute(TextAttribute.SIZE);
- Double buSz = bulletStyle.getBulletFontSize();
- if (buSz == null) buSz = 100d;
- if (buSz > 0) fontSize *= buSz* 0.01;
- else fontSize = (float)-buSz;
-
-
- AttributedString str = new AttributedString(mapFontCharset(buCharacter,buFont));
- str.addAttribute(TextAttribute.FOREGROUND, fgPaint);
- str.addAttribute(TextAttribute.FAMILY, buFont);
- str.addAttribute(TextAttribute.SIZE, fontSize);
-
- TextLayout layout = new TextLayout(str.getIterator(), graphics.getFontRenderContext());
- DrawFactory fact = DrawFactory.getInstance(graphics);
- return fact.getTextFragment(layout, str);
- }
-
- protected String getRenderableText(TextRun tr) {
- StringBuilder buf = new StringBuilder();
- TextCap cap = tr.getTextCap();
- String tabs = null;
- for (char c : tr.getRawText().toCharArray()) {
- switch (c) {
- case '\t':
- if (tabs == null) {
- tabs = tab2space(tr);
- }
- buf.append(tabs);
- break;
- case '\u000b':
- buf.append('\n');
- break;
- default:
- switch (cap) {
- case ALL: c = Character.toUpperCase(c); break;
- case SMALL: c = Character.toLowerCase(c); break;
- case NONE: break;
- }
-
- buf.append(c);
- break;
- }
- }
-
- return buf.toString();
- }
-
- /**
- * Replace a tab with the effective number of white spaces.
- */
- private String tab2space(TextRun tr) {
- AttributedString string = new AttributedString(" ");
- String fontFamily = tr.getFontFamily();
- if (fontFamily == null) fontFamily = "Lucida Sans";
- string.addAttribute(TextAttribute.FAMILY, fontFamily);
-
- Double fs = tr.getFontSize();
- if (fs == null) fs = 12d;
- string.addAttribute(TextAttribute.SIZE, fs.floatValue());
-
- TextLayout l = new TextLayout(string.getIterator(), new FontRenderContext(null, true, true));
- double wspace = l.getAdvance();
-
- Double tabSz = paragraph.getDefaultTabSize();
- if (tabSz == null) tabSz = wspace*4;
-
- int numSpaces = (int)Math.ceil(tabSz / wspace);
- StringBuilder buf = new StringBuilder();
- for(int i = 0; i < numSpaces; i++) {
- buf.append(' ');
- }
- return buf.toString();
- }
-
-
- /**
- * Returns wrapping width to break lines in this paragraph
- *
- * @param firstLine whether the first line is breaking
- *
- * @return wrapping width in points
- */
- protected double getWrappingWidth(boolean firstLine, Graphics2D graphics){
- // internal margins for the text box
-
- Insets2D insets = paragraph.getParentShape().getInsets();
- double leftInset = insets.left;
- double rightInset = insets.right;
-
- Rectangle2D anchor = DrawShape.getAnchor(graphics, paragraph.getParentShape());
-
- int indentLevel = paragraph.getIndentLevel();
- if (indentLevel == -1) {
- // default to 0, if indentLevel is not set
- indentLevel = 0;
- }
- Double leftMargin = paragraph.getLeftMargin();
- if (leftMargin == null) {
- // if the marL attribute is omitted, then a value of 347663 is implied
- leftMargin = Units.toPoints(347663L*(indentLevel+1));
- }
- Double indent = paragraph.getIndent();
- if (indent == null) {
- indent = Units.toPoints(347663L*indentLevel);
- }
- Double rightMargin = paragraph.getRightMargin();
- if (rightMargin == null) {
- rightMargin = 0d;
- }
-
- double width;
- TextShape<?,?> ts = paragraph.getParentShape();
- if (!ts.getWordWrap()) {
- // if wordWrap == false then we return the advance to the right border of the sheet
- width = ts.getSheet().getSlideShow().getPageSize().getWidth() - anchor.getX();
- } else {
- width = anchor.getWidth() - leftInset - rightInset - leftMargin - rightMargin;
- if (firstLine && !isHSLF()) {
- if (bullet != null){
- if (indent > 0) width -= indent;
- } else {
- if (indent > 0) width -= indent; // first line indentation
- else if (indent < 0) { // hanging indentation: the first line start at the left margin
- width += leftMargin;
- }
- }
- }
- }
-
- return width;
- }
-
- private static class AttributedStringData {
- Attribute attribute;
- Object value;
- int beginIndex, endIndex;
- AttributedStringData(Attribute attribute, Object value, int beginIndex, int endIndex) {
- this.attribute = attribute;
- this.value = value;
- this.beginIndex = beginIndex;
- this.endIndex = endIndex;
- }
- }
-
- /**
- * Helper method for paint style relative to bounds, e.g. gradient paint
- */
- @SuppressWarnings("rawtypes")
- private PlaceableShape<?,?> getParagraphShape() {
- PlaceableShape<?,?> ps = new PlaceableShape(){
- public ShapeContainer<?,?> getParent() { return null; }
- public Rectangle2D getAnchor() { return paragraph.getParentShape().getAnchor(); }
- public void setAnchor(Rectangle2D anchor) {}
- public double getRotation() { return 0; }
- public void setRotation(double theta) {}
- public void setFlipHorizontal(boolean flip) {}
- public void setFlipVertical(boolean flip) {}
- public boolean getFlipHorizontal() { return false; }
- public boolean getFlipVertical() { return false; }
- };
- return ps;
- }
-
- protected AttributedString getAttributedString(Graphics2D graphics, StringBuilder text){
- List<AttributedStringData> attList = new ArrayList<AttributedStringData>();
- if (text == null) text = new StringBuilder();
-
- PlaceableShape<?,?> ps = getParagraphShape();
-
- DrawFontManager fontHandler = (DrawFontManager)graphics.getRenderingHint(Drawable.FONT_HANDLER);
-
- for (TextRun run : paragraph){
- String runText = getRenderableText(run);
- // skip empty runs
- if (runText.isEmpty()) continue;
-
- // user can pass an custom object to convert fonts
- String fontFamily = run.getFontFamily();
- @SuppressWarnings("unchecked")
- Map<String,String> fontMap = (Map<String,String>)graphics.getRenderingHint(Drawable.FONT_MAP);
- if (fontMap != null && fontMap.containsKey(fontFamily)) {
- fontFamily = fontMap.get(fontFamily);
- }
- if(fontHandler != null) {
- fontFamily = fontHandler.getRendererableFont(fontFamily, run.getPitchAndFamily());
- }
- if (fontFamily == null) {
- fontFamily = paragraph.getDefaultFontFamily();
- }
-
- int beginIndex = text.length();
- text.append(mapFontCharset(runText,fontFamily));
- int endIndex = text.length();
-
- attList.add(new AttributedStringData(TextAttribute.FAMILY, fontFamily, beginIndex, endIndex));
-
- PaintStyle fgPaintStyle = run.getFontColor();
- Paint fgPaint = new DrawPaint(ps).getPaint(graphics, fgPaintStyle);
- attList.add(new AttributedStringData(TextAttribute.FOREGROUND, fgPaint, beginIndex, endIndex));
-
- Double fontSz = run.getFontSize();
- if (fontSz == null) fontSz = paragraph.getDefaultFontSize();
- attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), beginIndex, endIndex));
-
- if(run.isBold()) {
- attList.add(new AttributedStringData(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD, beginIndex, endIndex));
- }
- if(run.isItalic()) {
- attList.add(new AttributedStringData(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE, beginIndex, endIndex));
- }
- if(run.isUnderlined()) {
- attList.add(new AttributedStringData(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, beginIndex, endIndex));
- attList.add(new AttributedStringData(TextAttribute.INPUT_METHOD_UNDERLINE, TextAttribute.UNDERLINE_LOW_TWO_PIXEL, beginIndex, endIndex));
- }
- if(run.isStrikethrough()) {
- attList.add(new AttributedStringData(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, beginIndex, endIndex));
- }
- if(run.isSubscript()) {
- attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUB, beginIndex, endIndex));
- }
- if(run.isSuperscript()) {
- attList.add(new AttributedStringData(TextAttribute.SUPERSCRIPT, TextAttribute.SUPERSCRIPT_SUPER, beginIndex, endIndex));
- }
-
- Hyperlink hl = run.getHyperlink();
- if (hl != null) {
- attList.add(new AttributedStringData(HYPERLINK_HREF, hl.getAddress(), beginIndex, endIndex));
- attList.add(new AttributedStringData(HYPERLINK_LABEL, hl.getLabel(), beginIndex, endIndex));
- }
- }
-
- // ensure that the paragraph contains at least one character
- // We need this trick to correctly measure text
- if (text.length() == 0) {
- Double fontSz = paragraph.getDefaultFontSize();
- text.append(" ");
- attList.add(new AttributedStringData(TextAttribute.SIZE, fontSz.floatValue(), 0, 1));
- }
-
- AttributedString string = new AttributedString(text.toString());
- for (AttributedStringData asd : attList) {
- string.addAttribute(asd.attribute, asd.value, asd.beginIndex, asd.endIndex);
- }
-
- return string;
- }
-
- protected boolean isHSLF() {
- return paragraph.getClass().getName().contains("HSLF");
- }
-
- /**
- * Map text charset depending on font family.
- * Currently this only maps for wingdings font (into unicode private use area)
- *
- * @param text the raw text
- * @param fontFamily the font family
- * @return AttributedString with mapped codepoints
- *
- * @see <a href="http://stackoverflow.com/questions/8692095">Drawing exotic fonts in a java applet</a>
- * @see StringUtil#mapMsCodepointString(String)
- */
- protected String mapFontCharset(String text, String fontFamily) {
- // TODO: find a real charset mapping solution instead of hard coding for Wingdings
- String attStr = text;
- if ("Wingdings".equalsIgnoreCase(fontFamily)) {
- // wingdings doesn't contain high-surrogates, so chars are ok
- boolean changed = false;
- char chrs[] = attStr.toCharArray();
- for (int i=0; i<chrs.length; i++) {
- // only change valid chars
- if ((0x20 <= chrs[i] && chrs[i] <= 0x7f) ||
- (0xa0 <= chrs[i] && chrs[i] <= 0xff)) {
- chrs[i] |= 0xf000;
- changed = true;
- }
- }
-
- if (changed) {
- attStr = new String(chrs);
- }
- }
- return attStr;
- }
- }
|