123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895 |
- /* ====================================================================
- 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.hwmf.draw;
-
- import java.awt.AlphaComposite;
- import java.awt.BasicStroke;
- import java.awt.Color;
- import java.awt.Composite;
- import java.awt.Font;
- import java.awt.Graphics2D;
- import java.awt.GraphicsConfiguration;
- import java.awt.Insets;
- import java.awt.Paint;
- import java.awt.Rectangle;
- import java.awt.Shape;
- import java.awt.TexturePaint;
- import java.awt.font.FontRenderContext;
- import java.awt.font.GlyphVector;
- import java.awt.font.LineBreakMeasurer;
- import java.awt.font.TextAttribute;
- import java.awt.font.TextLayout;
- import java.awt.geom.AffineTransform;
- import java.awt.geom.Dimension2D;
- import java.awt.geom.NoninvertibleTransformException;
- import java.awt.geom.Point2D;
- import java.awt.geom.Rectangle2D;
- import java.awt.image.BufferedImage;
- import java.awt.image.DataBuffer;
- import java.awt.image.IndexColorModel;
- import java.nio.charset.Charset;
- import java.nio.charset.StandardCharsets;
- import java.text.AttributedString;
- import java.util.ArrayList;
- import java.util.BitSet;
- import java.util.HashMap;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import java.util.NoSuchElementException;
- import java.util.Objects;
- import java.util.TreeMap;
- import java.util.function.BiConsumer;
- import java.util.function.Supplier;
-
- import org.apache.poi.common.usermodel.fonts.FontCharset;
- import org.apache.poi.common.usermodel.fonts.FontInfo;
- import org.apache.poi.hwmf.record.HwmfBrushStyle;
- import org.apache.poi.hwmf.record.HwmfFont;
- import org.apache.poi.hwmf.record.HwmfMapMode;
- import org.apache.poi.hwmf.record.HwmfMisc.WmfSetBkMode.HwmfBkMode;
- import org.apache.poi.hwmf.record.HwmfObjectTableEntry;
- import org.apache.poi.hwmf.record.HwmfPenStyle;
- import org.apache.poi.hwmf.record.HwmfPenStyle.HwmfLineDash;
- import org.apache.poi.hwmf.record.HwmfRegionMode;
- import org.apache.poi.hwmf.record.HwmfText.WmfExtTextOutOptions;
- import org.apache.poi.hwmf.usermodel.HwmfCharsetAware;
- import org.apache.poi.sl.draw.BitmapImageRenderer;
- import org.apache.poi.sl.draw.DrawFactory;
- import org.apache.poi.sl.draw.DrawFontManager;
- import org.apache.poi.sl.draw.DrawPictureShape;
- import org.apache.poi.sl.draw.ImageRenderer;
- import org.apache.poi.util.Internal;
- import org.apache.poi.util.LocaleUtil;
-
- public class HwmfGraphics implements HwmfCharsetAware {
-
- public enum FillDrawStyle {
- NONE(FillDrawStyle::fillNone),
- FILL(HwmfGraphics::fill),
- DRAW(HwmfGraphics::draw),
- FILL_DRAW(FillDrawStyle::fillDraw);
-
- public final BiConsumer<HwmfGraphics,Shape> handler;
-
- FillDrawStyle(BiConsumer<HwmfGraphics,Shape> handler) {
- this.handler = handler;
- }
-
- private static void fillNone(HwmfGraphics g, Shape s) {
- }
-
- private static void fillDraw(HwmfGraphics g, Shape s) {
- g.fill(s);
- g.draw(s);
- }
- }
-
- private static final Float[] WEIGHT_MAP = {
- 900f, TextAttribute.WEIGHT_ULTRABOLD,
- 800f, TextAttribute.WEIGHT_EXTRABOLD,
- 750f, TextAttribute.WEIGHT_HEAVY,
- 700f, TextAttribute.WEIGHT_BOLD,
- 600f, TextAttribute.WEIGHT_DEMIBOLD,
- 500f, TextAttribute.WEIGHT_MEDIUM,
- 450f, TextAttribute.WEIGHT_SEMIBOLD,
- 400f, TextAttribute.WEIGHT_REGULAR,
- 300f, TextAttribute.WEIGHT_DEMILIGHT,
- 200f, TextAttribute.WEIGHT_LIGHT,
- 1f, TextAttribute.WEIGHT_EXTRA_LIGHT
- };
-
- private static class DxLayout {
- double dx;
- // Spacing at default tracking value of 0
- double pos0;
- // Spacing at second tracking value
- double pos1;
- int beginIndex;
- int endIndex;
- }
-
-
- private final List<HwmfDrawProperties> propStack = new LinkedList<>();
- protected HwmfDrawProperties prop;
- protected final Graphics2D graphicsCtx;
- protected final BitSet objectIndexes = new BitSet();
- protected final TreeMap<Integer,HwmfObjectTableEntry> objectTable = new TreeMap<>();
- private final AffineTransform initialAT = new AffineTransform();
-
-
- /** Bounding box from the placeable header */
- private final Rectangle2D bbox;
- private Supplier<Charset> charsetProvider = () -> LocaleUtil.CHARSET_1252;
-
- /**
- * Initialize a graphics context for wmf rendering
- *
- * @param graphicsCtx the graphics context to delegate drawing calls
- * @param bbox the bounding box of the wmf (taken from the placeable header)
- */
- public HwmfGraphics(Graphics2D graphicsCtx, Rectangle2D bbox) {
- this.graphicsCtx = graphicsCtx;
- this.bbox = (Rectangle2D)bbox.clone();
- this.initialAT.setTransform(graphicsCtx.getTransform());
- }
-
- public HwmfDrawProperties getProperties() {
- if (prop == null) {
- prop = newProperties(null);
- }
- return prop;
- }
-
- protected HwmfDrawProperties newProperties(HwmfDrawProperties oldProps) {
- return (oldProps == null) ? new HwmfDrawProperties() : new HwmfDrawProperties(oldProps);
- }
-
- public void draw(Shape shape) {
- HwmfPenStyle ps = getProperties().getPenStyle();
- if (ps == null) {
- return;
- }
- HwmfLineDash lineDash = ps.getLineDash();
- if (lineDash == HwmfLineDash.NULL) {
- // line is not drawn
- return;
- }
-
- BasicStroke stroke = getStroke();
-
- // first draw a solid background line (depending on bkmode)
- // only makes sense if the line is not solid
- if (getProperties().getBkMode() == HwmfBkMode.OPAQUE && (lineDash != HwmfLineDash.SOLID && lineDash != HwmfLineDash.INSIDEFRAME)) {
- graphicsCtx.setStroke(new BasicStroke(stroke.getLineWidth()));
- graphicsCtx.setColor(getProperties().getBackgroundColor().getColor());
- graphicsCtx.draw(shape);
- }
-
- // then draw the (dashed) line
- graphicsCtx.setStroke(stroke);
- graphicsCtx.setColor(getProperties().getPenColor().getColor());
- graphicsCtx.draw(shape);
- }
-
- public void fill(Shape shape) {
- HwmfDrawProperties prop = getProperties();
-
- if (prop.getBrushStyle() != HwmfBrushStyle.BS_NULL) {
- Composite old = graphicsCtx.getComposite();
- graphicsCtx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER));
- // if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
- // graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
- // graphicsCtx.fill(shape);
- // }
-
- graphicsCtx.setPaint(getFill());
- graphicsCtx.fill(shape);
- graphicsCtx.setComposite(old);
- }
-
- draw(shape);
- }
-
- @SuppressWarnings("MagicConstant")
- protected BasicStroke getStroke() {
- HwmfDrawProperties prop = getProperties();
- HwmfPenStyle ps = prop.getPenStyle();
- // TODO: fix line width calculation
- float width = (float)prop.getPenWidth();
- if (width == 0) {
- width = 1;
- }
- int cap = ps.getLineCap().awtFlag;
- int join = ps.getLineJoin().awtFlag;
- float miterLimit = (float)prop.getPenMiterLimit();
- float[] dashes = ps.getLineDashes();
- boolean dashAlt = ps.isAlternateDash();
- // This value is not an integer index into the dash pattern array.
- // Instead, it is a floating-point value that specifies a linear distance.
- float dashStart = (dashAlt && dashes != null && dashes.length > 1) ? dashes[0] : 0;
-
- return new BasicStroke(width, cap, join, Math.max(1,miterLimit), dashes, dashStart);
- }
-
- protected Paint getFill() {
- switch (getProperties().getBrushStyle()) {
- default:
- case BS_INDEXED:
- case BS_PATTERN8X8:
- case BS_DIBPATTERN8X8:
- case BS_MONOPATTERN:
- case BS_NULL: return null;
- case BS_PATTERN:
- case BS_DIBPATTERN:
- case BS_DIBPATTERNPT: return getPatternPaint();
- case BS_SOLID: return getSolidFill();
- case BS_HATCHED: return getHatchedFill();
- case BS_LINEAR_GRADIENT: return getLinearGradient();
- }
- }
-
- protected Paint getLinearGradient() {
- return null;
- }
-
- protected Paint getSolidFill() {
- return getProperties().getBrushColor().getColor();
- }
-
- protected Paint getHatchedFill() {
- HwmfDrawProperties prop = getProperties();
- BufferedImage pattern = getPatternFromLong(
- prop.getBrushHatch().getPattern(),
- prop.getBackgroundColor().getColor(),
- prop.getBrushColor().getColor(),
- prop.getBkMode() == HwmfBkMode.TRANSPARENT
- );
- return new TexturePaint(pattern, new Rectangle(0,0,8,8));
- }
-
- public static BufferedImage getPatternFromLong(long patternLng, Color background, Color foreground, boolean hasAlpha) {
- final int[] cmap = {background.getRGB(), foreground.getRGB()};
- final IndexColorModel icm = new IndexColorModel(1, 2, cmap, 0, hasAlpha, hasAlpha ? 0 : -1, DataBuffer.TYPE_BYTE);
- final BufferedImage pattern = new BufferedImage(8, 8, BufferedImage.TYPE_BYTE_INDEXED, icm);
-
- byte[] pt = new byte[64];
- for (int i=0; i<pt.length; i++) {
- pt[i] = (byte)((patternLng >>> i) & 1);
- }
- pattern.getRaster().setDataElements(0, 0, 8, 8, pt);
- return pattern;
- }
-
- protected Paint getPatternPaint() {
- HwmfDrawProperties prop = getProperties();
- ImageRenderer bb = prop.getBrushBitmap();
- if (bb == null) {
- return null;
- }
-
- Dimension2D dim = bb.getDimension();
- Rectangle2D rect = new Rectangle2D.Double(0, 0, dim.getWidth(), dim.getHeight());
- rect = prop.getBrushTransform().createTransformedShape(rect).getBounds2D();
-
- return new TexturePaint(bb.getImage(), rect);
- }
-
- /**
- * Adds an record of type {@link HwmfObjectTableEntry} to the object table.
- *
- * Every object is assigned the lowest available index-that is, the smallest
- * numerical value-in the WMF Object Table. This binding happens at object creation,
- * not when the object is used.
- * Moreover, each object table index uniquely refers to an object.
- * Indexes in the WMF Object Table always start at 0.
- *
- * @param entry the object table entry
- */
- public void addObjectTableEntry(HwmfObjectTableEntry entry) {
- int objIdx = objectIndexes.nextClearBit(0);
- objectIndexes.set(objIdx);
- objectTable.put(objIdx, entry);
- }
-
- /**
- * Applies the object table entry
- *
- * @param index the index of the object table entry (0-based)
- *
- * @throws IndexOutOfBoundsException if the index is out of range
- * @throws NoSuchElementException if the entry was deleted before
- */
- public void applyObjectTableEntry(int index) {
- HwmfObjectTableEntry ote = objectTable.get(index);
- if (ote == null) {
- throw new NoSuchElementException("WMF reference exception - object table entry on index "+index+" was deleted before.");
- }
- ote.applyObject(this);
- }
-
- /**
- * Unsets (deletes) the object table entry for further usage
- *
- * When a META_DELETEOBJECT record (section 2.3.4.7) is received that specifies this
- * object's particular index, the object's resources are released, the binding to its
- * WMF Object Table index is ended, and the index value is returned to the pool of
- * available indexes. The index will be reused, if needed, by a subsequent object
- * created by another Object Record Type record.
- *
- * @param index the index (0-based)
- *
- * @throws IndexOutOfBoundsException if the index is out of range
- */
- public void unsetObjectTableEntry(int index) {
- if (index < 0) {
- throw new IndexOutOfBoundsException("Invalid index: "+index);
- }
- // sometime emfs remove object table entries before they set them
- // so ignore requests, if the table entry doesn't exist
- objectTable.remove(index);
- objectIndexes.clear(index);
- }
-
- /**
- * Saves the current properties to the stack
- */
- public void saveProperties() {
- final HwmfDrawProperties p = getProperties();
- assert(p != null);
- p.setTransform(graphicsCtx.getTransform());
- p.setClip(graphicsCtx.getClip());
- propStack.add(p);
- prop = newProperties(p);
- }
-
- /**
- * Restores the properties from the stack
- *
- * @param index if the index is positive, the n-th element from the start is activated.
- * If the index is negative, the n-th previous element relative to the current properties element is activated.
- */
- public void restoreProperties(int index) {
- if (index == 0) {
- return;
- }
- int stackIndex = index;
- if (stackIndex < 0) {
- int curIdx = propStack.indexOf(getProperties());
- if (curIdx == -1) {
- // the current element is not pushed to the stacked, i.e. it's the last
- curIdx = propStack.size();
- }
- stackIndex = curIdx + index;
- }
- if (stackIndex == -1) {
- // roll to last when curIdx == 0
- stackIndex = propStack.size()-1;
- }
-
- // The playback device context is restored by popping state information off a stack that was created by
- // prior SAVEDC records
- // ... so because being a stack, we will remove all entries having a greater index than the stackIndex
- for (int i=propStack.size()-1; i>=stackIndex; i--) {
- prop = propStack.remove(i);
- }
-
- graphicsCtx.setTransform(prop.getTransform());
- graphicsCtx.setClip(prop.getClip());
- }
-
- /**
- * After setting various window and viewport related properties,
- * the underlying graphics context needs to be adapted.
- * This methods gathers and sets the corresponding graphics transformations.
- */
- public void updateWindowMapMode() {
- Rectangle2D win = getProperties().getWindow();
- Rectangle2D view = getProperties().getViewport();
- HwmfMapMode mapMode = getProperties().getMapMode();
- graphicsCtx.setTransform(getInitTransform());
-
- switch (mapMode) {
- default:
- case MM_ANISOTROPIC:
- // scale window bounds to output bounds
- if (view != null) {
- graphicsCtx.translate(view.getCenterX(), view.getCenterY());
- graphicsCtx.scale(view.getWidth() / win.getWidth(), view.getHeight() / win.getHeight());
- graphicsCtx.translate(-win.getCenterX(), -win.getCenterY());
- }
- break;
- case MM_ISOTROPIC:
- // TODO: to be validated ...
- // like anisotropic, but use x-axis as reference
- graphicsCtx.translate(bbox.getCenterX(), bbox.getCenterY());
- graphicsCtx.scale(bbox.getWidth()/win.getWidth(), bbox.getWidth()/win.getWidth());
- graphicsCtx.translate(-win.getCenterX(), -win.getCenterY());
- break;
- case MM_LOMETRIC:
- case MM_HIMETRIC:
- case MM_LOENGLISH:
- case MM_HIENGLISH:
- case MM_TWIPS: {
- // TODO: to be validated ...
- GraphicsConfiguration gc = graphicsCtx.getDeviceConfiguration();
- graphicsCtx.transform(gc.getNormalizingTransform());
- graphicsCtx.scale(1./mapMode.scale, -1./mapMode.scale);
- graphicsCtx.translate(-win.getX(), -win.getY());
- break;
- }
- case MM_TEXT:
- // TODO: to be validated ...
- break;
- }
- }
-
- public void drawString(byte[] text, int length, Point2D reference) {
- drawString(text, length, reference, null, null, null, null, false);
- }
-
- public void drawString(byte[] text, int length, Point2D reference, Dimension2D scale, Rectangle2D clip, WmfExtTextOutOptions opts, List<Integer> dx, boolean isUnicode) {
- final HwmfDrawProperties prop = getProperties();
-
- final AffineTransform at = graphicsCtx.getTransform();
- try {
- at.createInverse();
- } catch (NoninvertibleTransformException e) {
- return;
- }
-
- final HwmfFont font = prop.getFont();
- if (font == null || text == null || text.length == 0) {
- return;
- }
-
- final Charset charset = getCharset(font, isUnicode);
- String textString = trimText(charset, text, length);
- if (textString.isEmpty()) {
- return;
- }
-
- final DrawFontManager fontHandler = DrawFactory.getInstance(graphicsCtx).getFontManager(graphicsCtx);
- final FontInfo fontInfo = fontHandler.getMappedFont(graphicsCtx, font);
- textString = fontHandler.mapFontCharset(graphicsCtx, fontInfo, textString);
-
- final AttributedString as = new AttributedString(textString);
- addAttributes(as::addAttribute, font, fontInfo.getTypeface());
- final FontRenderContext frc = graphicsCtx.getFontRenderContext();
-
- calculateDx(textString, dx, font, fontInfo, frc, as);
-
- LineBreakMeasurer lbm = new LineBreakMeasurer(as.getIterator(), frc);
- TextLayout textLayout = lbm.nextLayout(Integer.MAX_VALUE);
-
- final double angle = Math.toRadians(-font.getEscapement()/10.);
-
- // TODO: find out when to use asian align
- boolean useAsianAlign = (opts == null) &&
- textString.codePoints().anyMatch(Character::isIdeographic) &&
- charset.displayName(Locale.ROOT). startsWith("GB");
-
- final Point2D dst = getRotatedOffset(angle, frc, as, useAsianAlign);
- final Shape clipShape = graphicsCtx.getClip();
-
- try {
- updateClipping(graphicsCtx, clip, angle, opts);
-
- // TODO: Check: certain images don't use the reference of the extTextOut, but rely on a moveto issued beforehand
- Point2D moveTo = (reference.distance(0,0) == 0) ? prop.getLocation() : reference;
- graphicsCtx.translate(moveTo.getX(), moveTo.getY());
-
- graphicsCtx.rotate(angle);
- if (scale != null) {
- graphicsCtx.scale(scale.getWidth() < 0 ? -1 : 1, scale.getHeight() < 0 ? -1 : 1);
- }
- graphicsCtx.scale(at.getScaleX() < 0 ? -1 : 1, at.getScaleY() < 0 ? -1 : 1);
- graphicsCtx.translate(dst.getX(), dst.getY());
- graphicsCtx.setColor(prop.getTextColor().getColor());
- graphicsCtx.drawString(as.getIterator(), 0, 0);
-
- // move current location to the end of string
- AffineTransform atRev = new AffineTransform();
- atRev.translate(-dst.getX(), -dst.getY());
- if (scale != null) {
- atRev.scale(scale.getWidth() < 0 ? 1 : -1, scale.getHeight() < 0 ? 1 : -1);
- }
- atRev.rotate(-angle);
-
- Point2D deltaX = new Point2D.Double(textLayout.getBounds().getWidth(), 0);
- Point2D oldLoc = prop.getLocation();
- prop.setLocation(oldLoc.getX() + deltaX.getX(), oldLoc.getY() + deltaX.getY());
-
- } finally {
- graphicsCtx.setTransform(at);
- graphicsCtx.setClip(clipShape);
- }
- }
-
- /**
- * The dx array indicate the distance between origins of adjacent character cells.
- * For example, dx[i] logical units separate the origins of character cell i and character cell i + 1.
- * So dx{i] is the complete width of the current char + space to the next character
- *
- * In AWT we have the {@link TextAttribute#TRACKING} attribute, which works very similar.
- * As we don't know (yet) the calculation based on the font size/height, we interpolate
- * between the default tracking and a tracking value of 1
- */
- private void calculateDx(String textString, List<Integer> dx, HwmfFont font, FontInfo fontInfo, FontRenderContext frc, AttributedString as) {
- if (dx == null || dx.isEmpty()) {
- return;
- }
- final List<DxLayout> dxList = new ArrayList<>();
-
- Map<TextAttribute,Object> fontAtt = new HashMap<>();
- // Font tracking default (= 0)
- addAttributes(fontAtt::put, font, fontInfo.getTypeface());
- final GlyphVector gv0 = new Font(fontAtt).createGlyphVector(frc, textString);
- // Font tracking = 1
- fontAtt.put(TextAttribute.TRACKING, 1);
- final GlyphVector gv1 = new Font(fontAtt).createGlyphVector(frc, textString);
-
- int beginIndex = 0;
- for (int offset = 0; offset < dx.size(); offset++) {
- if (beginIndex >= textString.length()) {
- break;
- }
- DxLayout dxLayout = new DxLayout();
- dxLayout.dx = dx.get(offset);
- dxLayout.pos0 = gv0.getGlyphPosition(offset).getX();
- dxLayout.pos1 = gv1.getGlyphPosition(offset).getX();
- dxLayout.beginIndex = beginIndex;
- dxLayout.endIndex = textString.offsetByCodePoints(beginIndex, 1);
- dxList.add(dxLayout);
-
- beginIndex = dxLayout.endIndex;
- }
-
- // Calculate the linear (y ~= Tracking setting / x ~= character spacing / target value)
- // y = m * x + n
- // y = ((y2-y1)/(x2-x1))x + ((y1x2-y2x1)/(x2-x1))
-
- DxLayout dx0 = null;
- for (DxLayout dx1 : dxList) {
- if (dx0 != null) {
- // Default Tracking = 0 (y1)
- double y1 = 0, x1 = dx1.pos0-dx0.pos0;
- // Second Tracking = 1 (y2)
- double y2 = 1, x2 = dx1.pos1-dx0.pos1;
- double track = ((y2-y1)/(x2-x1))*dx0.dx + ((y1*x2-y2*x1)/(x2-x1));
- as.addAttribute(TextAttribute.TRACKING, (float)track, dx0.beginIndex, dx0.endIndex);
- }
- dx0 = dx1;
- }
- }
-
- private void addAttributes(BiConsumer<TextAttribute,Object> attributes, HwmfFont font, String typeface) {
- Map<TextAttribute,Object> att = new HashMap<>();
- att.put(TextAttribute.FAMILY, typeface);
- att.put(TextAttribute.SIZE, getFontHeight(font));
-
- if (font.isStrikeOut()) {
- att.put(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON);
- }
- if (font.isUnderline()) {
- att.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
- }
- if (font.isItalic()) {
- att.put(TextAttribute.POSTURE, TextAttribute.POSTURE_OBLIQUE);
- }
- // convert font weight to awt font weight - usually a font weight of 400 is regarded as regular
- final int fw = font.getWeight();
- Float awtFW = TextAttribute.WEIGHT_REGULAR;
- for (int i=0; i<WEIGHT_MAP.length; i+=2) {
- if (fw >= WEIGHT_MAP[i]) {
- awtFW = WEIGHT_MAP[i+1];
- break;
- }
- }
- att.put(TextAttribute.WEIGHT, awtFW);
- att.put(TextAttribute.FONT, new Font(att));
-
- att.forEach(attributes);
- }
-
- private double getFontHeight(HwmfFont font) {
- // see HwmfFont#height for details
- double fontHeight = font.getHeight();
- if (fontHeight == 0) {
- return 12;
- } else if (fontHeight < 0) {
- return -fontHeight;
- } else {
- // TODO: fix font height calculation
- // the height is given as font size + ascent + descent
- // as an approximation we reduce the height by a static factor
- return fontHeight*3/4;
- }
- }
-
- private Charset getCharset(HwmfFont font, boolean isUnicode) {
- if (isUnicode) {
- return StandardCharsets.UTF_16LE;
- }
-
- FontCharset fc = font.getCharset();
- if (fc == FontCharset.DEFAULT) {
- return charsetProvider.get();
- }
-
- Charset charset = fc.getCharset();
- return (charset == null) ? charsetProvider.get() : charset;
- }
-
- @Override
- public void setCharsetProvider(Supplier<Charset> provider) {
- charsetProvider = provider;
- }
-
- private String trimText(Charset charset, byte[] text, int length) {
-
- int trimLen;
- for (trimLen=0; trimLen<text.length; trimLen+=2) {
- if (trimLen == text.length-1) {
- if (text[trimLen] != 0) {
- trimLen++;
- }
- break;
- } else if ((text[trimLen] == -1 && text[trimLen+1] == -1) ||
- ((text[trimLen] & 0xE0) == 0 && text[trimLen+1] == 0)) {
- break;
- }
- }
-
- String textString = new String(text, 0, trimLen, charset);
- return textString.substring(0, Math.min(textString.length(), length));
- }
-
- private void updateHorizontalAlign(AffineTransform tx, TextLayout layout, boolean useAsianAlign) {
- // TODO: using prop.getTextAlignAsian doesn't look good ...
- switch (prop.getTextAlignLatin()) {
- default:
- case LEFT:
- break;
- case CENTER:
- tx.translate(-layout.getBounds().getWidth() / 2., 0);
- break;
- case RIGHT:
- tx.translate(-layout.getAdvance(), 0);
- break;
- }
- }
-
- private void updateVerticalAlign(AffineTransform tx, TextLayout layout, boolean useAsianAlign) {
- // TODO: check min/max orientation
- switch (useAsianAlign ? prop.getTextVAlignAsian() : prop.getTextVAlignLatin()) {
- case TOP:
- tx.translate(0, layout.getAscent());
- break;
- default:
- case BASELINE:
- break;
- case BOTTOM:
- tx.translate(0, -(layout.getBounds().getHeight()-layout.getDescent()));
- break;
- }
- }
-
- private void updateClipping(Graphics2D graphicsCtx, Rectangle2D clip, double angle, WmfExtTextOutOptions opts) {
- if (clip == null || clip.getBounds2D().isEmpty()) {
- return;
- }
-
- final AffineTransform at = graphicsCtx.getTransform();
-
- graphicsCtx.translate(-clip.getCenterX(), -clip.getCenterY());
- graphicsCtx.rotate(angle);
- graphicsCtx.translate(clip.getCenterX(), clip.getCenterY());
- if (prop.getBkMode() == HwmfBkMode.OPAQUE && opts.isOpaque()) {
- graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
- graphicsCtx.fill(clip);
- }
- if (opts.isClipped()) {
- graphicsCtx.setClip(clip);
- }
-
- graphicsCtx.setTransform(at);
- }
-
- private Point2D getRotatedOffset(double angle, FontRenderContext frc, AttributedString as, boolean useAsianAlign) {
- final TextLayout layout = new TextLayout(as.getIterator(), frc);
- final AffineTransform tx = new AffineTransform();
- updateHorizontalAlign(tx, layout, useAsianAlign);
- updateVerticalAlign(tx, layout, useAsianAlign);
-
- tx.rotate(angle);
- Point2D src = new Point2D.Double();
- return tx.transform(src, null);
- }
-
- public void drawImage(BufferedImage img, Rectangle2D srcBounds, Rectangle2D dstBounds) {
- drawImage(new BufferedImageRenderer(img), srcBounds, dstBounds);
- }
-
- public void drawImage(ImageRenderer img, Rectangle2D srcBounds, Rectangle2D dstBounds) {
- if (srcBounds.isEmpty()) {
- return;
- }
- HwmfDrawProperties prop = getProperties();
-
- // handle raster op
- // currently the raster op as described in https://docs.microsoft.com/en-us/windows/desktop/gdi/ternary-raster-operations
- // are not supported, as we would need to extract the destination image area from the underlying buffered image
- // and therefore would make it mandatory that the graphics context must be from a buffered image
- // furthermore I doubt the purpose of bitwise image operations on non-black/white images
- switch (prop.getRasterOp3()) {
- case D:
- // keep destination, i.e. do nothing
- break;
- case PATCOPY:
- graphicsCtx.setPaint(getFill());
- graphicsCtx.fill(dstBounds);
- break;
- case BLACKNESS:
- graphicsCtx.setPaint(Color.BLACK);
- graphicsCtx.fill(dstBounds);
- break;
- case WHITENESS:
- graphicsCtx.setPaint(Color.WHITE);
- graphicsCtx.fill(dstBounds);
- break;
- default:
- case SRCAND:
- case SRCCOPY:
- case SRCINVERT:
- if (img == null) {
- return;
- }
-
- final Shape oldClip = graphicsCtx.getClip();
- final AffineTransform oldTrans = graphicsCtx.getTransform();
-
- // add clipping in case of a source subimage, i.e. a clipped source image
- // some dstBounds are horizontal or vertical flipped, so we need to normalize the images
- Rectangle2D normBounds = normalizeRect(dstBounds);
- // graphicsCtx.clip(normBounds);
-
- if (prop.getBkMode() == HwmfBkMode.OPAQUE) {
- Paint oldPaint = graphicsCtx.getPaint();
- graphicsCtx.setPaint(prop.getBackgroundColor().getColor());
- graphicsCtx.fill(dstBounds);
- graphicsCtx.setPaint(oldPaint);
- }
-
- graphicsCtx.translate(normBounds.getCenterX(), normBounds.getCenterY());
- graphicsCtx.scale(Math.signum(dstBounds.getWidth()), Math.signum(dstBounds.getHeight()));
- graphicsCtx.translate(-normBounds.getCenterX(), -normBounds.getCenterY());
-
- // this is similar to drawing bitmaps with a clipping
- // see {@link BitmapImageRenderer#drawImage(Graphics2D,Rectangle2D,Insets)}
- // the difference is, that clippings are 0-based, whereas the srcBounds are absolute in the user-space
- // of the referenced image and can be also negative
- Composite old = graphicsCtx.getComposite();
- int newComp;
- switch (prop.getRasterOp3()) {
- default:
- case SRCCOPY:
- newComp = AlphaComposite.SRC_OVER;
- break;
- case SRCINVERT:
- newComp = AlphaComposite.SRC_IN;
- break;
- case SRCAND:
- newComp = AlphaComposite.SRC;
- break;
- }
- graphicsCtx.setComposite(AlphaComposite.getInstance(newComp));
-
- boolean useDeviceBounds = (img instanceof HwmfImageRenderer);
-
- img.drawImage(graphicsCtx, normBounds,
- getSubImageInsets(srcBounds, useDeviceBounds ? img.getNativeBounds() : img.getBounds()));
- graphicsCtx.setComposite(old);
-
- graphicsCtx.setTransform(oldTrans);
- graphicsCtx.setClip(oldClip);
- break;
- }
-
- }
-
- private static Rectangle2D normalizeRect(Rectangle2D dstBounds) {
- return new Rectangle2D.Double(
- dstBounds.getWidth() >= 0 ? dstBounds.getMinX() : dstBounds.getMaxX(),
- dstBounds.getHeight() >= 0 ? dstBounds.getMinY() : dstBounds.getMaxY(),
- Math.abs(dstBounds.getWidth()),
- Math.abs(dstBounds.getHeight()));
- }
-
- private static Insets getSubImageInsets(Rectangle2D srcBounds, Rectangle2D nativeBounds) {
- // Todo: check if we need to normalize srcBounds x/y, in case of flipped images
- // for now we assume the width/height is positive
- int left = (int)Math.round((srcBounds.getX()-nativeBounds.getX())/nativeBounds.getWidth()*100_000.);
- int top = (int)Math.round((srcBounds.getY()-nativeBounds.getY())/nativeBounds.getHeight()*100_000.);
- int right = (int)Math.round((nativeBounds.getMaxX()-srcBounds.getMaxX())/nativeBounds.getWidth()*100_000.);
- int bottom = (int)Math.round((nativeBounds.getMaxY()-srcBounds.getMaxY())/nativeBounds.getHeight()*100_000.);
-
- return new Insets(top, left, bottom, right);
- }
-
- /**
- * @return the initial AffineTransform, when this graphics context was created
- */
- public AffineTransform getInitTransform() {
- return new AffineTransform(initialAT);
- }
-
- /**
- * @return the current AffineTransform
- */
- public AffineTransform getTransform() {
- return new AffineTransform(graphicsCtx.getTransform());
- }
-
- /**
- * Set the current AffineTransform
- * @param tx the current AffineTransform
- */
- public void setTransform(AffineTransform tx) {
- graphicsCtx.setTransform(tx);
- }
-
- /**
- * Set the new clipping region
- *
- * @param clip the next clipping region to be processed
- * @param regionMode the mode and operation of how to apply the next clipping region
- * @param useInitialAT if true, the clipping is applied on the initial (world) coordinate system
- */
- public void setClip(Shape clip, HwmfRegionMode regionMode, boolean useInitialAT) {
- final AffineTransform at = graphicsCtx.getTransform();
- if (useInitialAT) {
- graphicsCtx.setTransform(getInitTransform());
- }
-
- final Shape oldClip = graphicsCtx.getClip();
- final Shape newClip = regionMode.applyOp(oldClip, clip);
- if (!Objects.equals(oldClip, newClip)) {
- graphicsCtx.setClip(newClip);
- }
-
- if (useInitialAT) {
- graphicsCtx.setTransform(at);
- }
- prop.setClip(graphicsCtx.getClip());
- }
-
- public ImageRenderer getImageRenderer(String contentType) {
- // TODO: refactor DrawPictureShape method to POI Common
- return DrawPictureShape.getImageRenderer(graphicsCtx, contentType);
- }
-
-
- @Internal
- static class BufferedImageRenderer extends BitmapImageRenderer {
- public BufferedImageRenderer(BufferedImage img) {
- this.img = img;
- }
- }
- }
|