All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.ofdrw.layout.element.canvas.DrawContext Maven / Gradle / Ivy

The newest version!
package org.ofdrw.layout.element.canvas;

import org.ofdrw.core.basicStructure.pageObj.layer.block.CT_PageBlock;
import org.ofdrw.core.basicStructure.pageObj.layer.block.ImageObject;
import org.ofdrw.core.basicStructure.pageObj.layer.block.PathObject;
import org.ofdrw.core.basicStructure.pageObj.layer.block.TextObject;
import org.ofdrw.core.basicType.ST_Array;
import org.ofdrw.core.basicType.ST_Box;
import org.ofdrw.core.basicType.ST_ID;
import org.ofdrw.core.graph.pathObj.AbbreviatedData;
import org.ofdrw.core.graph.pathObj.CT_Path;
import org.ofdrw.core.graph.pathObj.OptVal;
import org.ofdrw.core.pageDescription.CT_GraphicUnit;
import org.ofdrw.core.pageDescription.clips.CT_Clip;
import org.ofdrw.core.pageDescription.clips.Clips;
import org.ofdrw.core.pageDescription.color.color.CT_Color;
import org.ofdrw.core.pageDescription.color.color.ColorClusterType;
import org.ofdrw.core.pageDescription.drawParam.LineCapType;
import org.ofdrw.core.pageDescription.drawParam.LineJoinType;
import org.ofdrw.core.text.TextCode;
import org.ofdrw.core.text.font.CT_Font;
import org.ofdrw.core.text.text.CT_Text;
import org.ofdrw.core.text.text.Direction;
import org.ofdrw.core.text.text.Weight;
import org.ofdrw.font.Font;
import org.ofdrw.layout.engine.ExistCTFont;
import org.ofdrw.layout.engine.ResManager;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import java.io.Closeable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 绘制器绘制上下文
 * 

* 上下文中提供系列的绘制方法供绘制 *

* 一个路径对象只允许出现一种描边和填充颜色 * 重复设置,取最后一次设置的颜色。 *

* 关于路径: * 1. beginPath 清空路径。 * 2. 所有路径在 fill 和 stroke 是才应用图元效果。 * 3. 路径数据 与 绘图数据分开。 * 4. 除了 beginPath 之外所有数据均认为是 向已经存在的路径追加新的路径。 * * @author 权观宇 * @since 2020-05-01 11:29:20 */ public class DrawContext implements Closeable { static final ST_Array ONE = ST_Array.unitCTM(); /** * 用于容纳所绘制的所有图像的容器 */ private CT_PageBlock container; /** * 对象ID提供器 */ private AtomicInteger maxUnitID; /** * 资源管理器 */ private ResManager resManager; /** * 边框位置,也就是画布大小以及位置 */ private ST_Box boundary; /** * 画布状态 */ private CanvasState state; /** * 绘制参数栈 *

* save() 时将当前绘制参数压栈 *

* restore() 时将当前绘制参数出栈 */ private LinkedList stack; /** * 填充颜色 *

* 支持: *

* {@link String} 16进制颜色值,#000000、rgb(0,0,0)、rgba(0,0,0,1) *

* {@link CT_Color} OFD颜色对象 *

* {@link ColorClusterType} 颜色族 *

* {@link CanvasPattern} 图案 *

* {@link CanvasGradient} 渐变 *

* {@link CanvasRadialGradient} 径向渐变 */ public Object fillStyle; /** * 描边颜色 *

* 支持: *

* {@link String} 16进制颜色值,#000000、rgb(0,0,0)、rgba(0,0,0,1) *

* {@link CT_Color} OFD颜色对象 *

* {@link ColorClusterType} 颜色族 *

* {@link CanvasPattern} 图案 *

* {@link CanvasGradient} 渐变 *

* {@link CanvasRadialGradient} 径向渐变 */ public Object strokeStyle; /** * 字体描述 格式与CSS3格式一致 *

* [font-style] [font-weight] font-size font-family *

* 它必需包含 font-size font-family,[]内容为可选 *

* 详见 {@code https://developer.mozilla.org/en-US/docs/Web/CSS/font} *

* font-style: normal | italic *

* font-weight: normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 *

* font-size: 12px | 3.17mm (默认单位为 mm) *

* font-family: 宋体 | SimSun | Times New Roman | Times | serif | sans-serif | monospace | cursive | fantasy *

* font-family 为必选项,其他为可选项 *

* font-size 和 line-height 可以使用 px 或 mm 作为单位,若不指定单位则默认为 mm *

* 锚点: * - fillText * - measureText */ public String font; /** * 每毫米像素数量 pixel per millimeter */ public double PPM; private DrawContext() { } /** * 创建绘制上下文 * * @param container 绘制内容缩所放置容器 * @param boundary 画布大小以及位置 * @param maxUnitID 自增的对象ID * @param resManager 资源管理器 */ public DrawContext(CT_PageBlock container, ST_Box boundary, AtomicInteger maxUnitID, ResManager resManager) { this.container = container; this.boundary = boundary; this.maxUnitID = maxUnitID; this.resManager = resManager; this.state = new CanvasState(); this.stack = new LinkedList<>(); this.PPM = 3.78; // 初始化颜色默认为黑色 this.fillStyle = "#000000"; this.strokeStyle = "#000000"; } /** * 开启一段新的路径 *

* 如果已经存在路径,那么将会清除已经存在的所有路径。 * * @return this */ public DrawContext beginPath() { this.state.path = new AbbreviatedData(); return this; } /** * 关闭路径 *

* 如果路径存在描边或者填充,那么改路径将会被加入到图形容器中进行渲染 *

* 路径关闭后将会清空上下文中的路径对象 * * @return this */ public DrawContext closePath() { if (this.state.path == null) { return this; } this.state.path.close(); return this; } /** * 从原始画布中剪切任意形状和尺寸 *

* 裁剪路径以当前的路径作为裁剪参数 *

* 裁剪区域受变换矩阵影响 * * @return this */ public DrawContext clip() { if (this.state.path == null) { return this; } this.state.clipArea = this.state.path.clone(); if (this.state.ctm != null && !ONE.equals(this.state.ctm)) { // 受到CTM的影响形变 transform(this.state.clipArea, this.state.ctm); } return this; } /** * 移动绘制点到指定位置 * * @param x X坐标 * @param y Y坐标 * @return this */ public DrawContext moveTo(double x, double y) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } this.state.path.moveTo(x, y); return this; } /** * 从当前点连线到指定点 *

* 请在调用前创建路径 * * @param x X坐标 * @param y Y坐标 * @return this */ public DrawContext lineTo(double x, double y) { if (this.state.path == null) { return this; } this.state.path.lineTo(x, y); return this; } /** * 通过二次贝塞尔曲线的指定控制点,向当前路径添加一个点。 * * @param cpx 贝塞尔控制点的 x 坐标 * @param cpy 贝塞尔控制点的 y 坐标 * @param x 结束点的 x 坐标 * @param y 结束点的 y 坐标 * @return this */ public DrawContext quadraticCurveTo(double cpx, double cpy, double x, double y) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } this.state.path.quadraticBezier(cpx, cpy, x, y); return this; } /** * 方法三次贝塞尔曲线的指定控制点,向当前路径添加一个点。 * * @param cp1x 第一个贝塞尔控制点的 x 坐标 * @param cp1y 第一个贝塞尔控制点的 y 坐标 * @param cp2x 第二个贝塞尔控制点的 x 坐标 * @param cp2y 第二个贝塞尔控制点的 y 坐标 * @param x 结束点的 x 坐标 * @param y 结束点的 y 坐标 * @return this */ public DrawContext bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } this.state.path.cubicBezier(cp1x, cp1y, cp2x, cp2y, x, y); return this; } /** * 从当前点连接到点(x,y)的圆弧,并将当前点移动到点(x,y)。 * rx 表示椭圆的长轴长度,ry 表示椭圆的短轴长度。angle 表示 * 椭圆在当前坐标系下旋转的角度,正值为顺时针,负值为逆时针, * large 为 1 时表示对应度数大于180°的弧,为 0 时表示对应 * 度数小于 180°的弧。sweep 为 1 时表示由圆弧起始点到结束点 * 是顺时针旋转,为 0 时表示由圆弧起始点到结束点是逆时针旋转。 * * @param a 椭圆长轴长度 * @param b 椭圆短轴长度 * @param angle 旋转角度,正值 - 顺时针,负值 - 逆时针 * @param large true表示对应度数大于 180°的弧,false 表示对应度数小于 180°的弧 * @param sweep sweep true 表示由圆弧起始点到结束点是顺时针旋转,false表示由圆弧起始点到结束点是逆时针旋转。 * @param x 目标点 x * @param y 目标点 y * @return this */ public DrawContext arc(double a, double b, double angle, boolean large, boolean sweep, double x, double y) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } this.state.path.arc(a, b, angle % 360, large ? 1 : 0, sweep ? 1 : 0, x, y); return this; } /** * 创建弧/曲线(用于创建圆或部分圆) * * @param x 圆的中心的 x 坐标。 * @param y 圆的中心的 y 坐标。 * @param r 圆的半径。 * @param sAngle 起始角,单位度(弧的圆形的三点钟位置是 0 度)。 * @param eAngle 结束角,单位度 * @param counterclockwise 规定应该逆时针还是顺时针绘图。false = 顺时针,true = 逆时针。 * @return this */ public DrawContext arc(double x, double y, double r, double sAngle, double eAngle, boolean counterclockwise) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } // 首先移动点到起始位置 double x1 = x + r * Math.cos(sAngle * Math.PI / 180); double y1 = y + r * Math.sin(sAngle * Math.PI / 180); this.moveTo(x1, y1); double angle = eAngle - sAngle; if (angle == 360) { // 整个圆的时候需要分为两次路径进行绘制 // 绘制结束位置起始位置 this.state.path.arc(r, r, angle, 1, counterclockwise ? 1 : 0, x - r, y).arc(r, r, angle, 1, counterclockwise ? 1 : 0, x1, y1); } else { // 绘制结束位置起始位置 double x2 = x + r * Math.cos(eAngle * Math.PI / 180); double y2 = y + r * Math.sin(eAngle * Math.PI / 180); this.state.path.arc(r, r, angle, angle > 180 ? 1 : 0, counterclockwise ? 1 : 0, x2, y2); } return this; } /** * 创建弧/曲线(用于创建圆或部分圆) *

* 默认顺时针方向 * * @param x 圆的中心的 x 坐标。 * @param y 圆的中心的 y 坐标。 * @param r 圆的半径。 * @param sAngle 起始角,单位度(弧的圆形的三点钟位置是 0 度)。 * @param eAngle 结束角,单位度 * @return this */ public DrawContext arc(double x, double y, double r, double sAngle, double eAngle) { return arc(x, y, r, sAngle, eAngle, true); } /** * 创建矩形路径 * * @param x 左上角X坐标 * @param y 左上角Y坐标 * @param width 宽度 * @param height 高度 * @return this */ public DrawContext rect(double x, double y, double width, double height) { if (this.state.path == null) { this.state.path = new AbbreviatedData(); } this.state.path.moveTo(x, y).lineTo(x + width, y).lineTo(x + width, y + height).lineTo(x, y + height).close(); return this; } /** * 创建并填充矩形路径 *

* 填充矩形不会导致影响上下文中的路径。 *

* 如果已经存在路径那么改路径将会提前关闭,并创建新的路径。 * * @param x 左上角X坐标 * @param y 左上角Y坐标 * @param width 宽度 * @param height 高度 * @return this */ public DrawContext fillRect(double x, double y, double width, double height) { AbbreviatedData abData = new AbbreviatedData().moveTo(x, y).lineTo(x + width, y).lineTo(x + width, y + height).lineTo(x, y + height).close(); PathObject p = new PathObject(new ST_ID(maxUnitID.incrementAndGet())); p.setAbbreviatedData(abData); p.setFill(true); applyDrawParam(p); container.add(p); return this; } /** * 创建并描边矩形路径 *

* 描边矩形不会导致影响上下文中的路径。 *

* 默认描边颜色为黑色 * * @param x 左上角X坐标 * @param y 左上角Y坐标 * @param width 宽度 * @param height 高度 * @return this */ public DrawContext strokeRect(double x, double y, double width, double height) { AbbreviatedData abData = new AbbreviatedData().moveTo(x, y).lineTo(x + width, y).lineTo(x + width, y + height).lineTo(x, y + height).close(); PathObject p = new PathObject(new ST_ID(maxUnitID.incrementAndGet())); p.setAbbreviatedData(abData); p.setStroke(true); applyDrawParam(p); container.add(p); return this; } /** * 绘制已定义的路径 * * @return this */ public DrawContext stroke() { if (this.state.path == null) { return this; } PathObject p = new PathObject(new ST_ID(maxUnitID.incrementAndGet())); p.setAbbreviatedData(this.state.path.clone()); p.setStroke(true); applyDrawParam(p); container.add(p); return this; } /** * 填充已定义路径 *

* 默认的填充颜色是黑色。 * * @return this */ public DrawContext fill() { if (this.state.path == null) { return this; } PathObject p = new PathObject(new ST_ID(maxUnitID.incrementAndGet())); p.setAbbreviatedData(this.state.path.clone()); p.setFill(true); p.setLineWidth(0d); applyDrawParam(p); container.add(p); return this; } /** * 缩放当前绘图,更大或更小 * * @param scalewidth 缩放当前绘图的宽度 (1=100%, 0.5=50%, 2=200%, 依次类推) * @param scaleheight 缩放当前绘图的高度 (1=100%, 0.5=50%, 2=200%, 依次类推) * @return this */ public DrawContext scale(double scalewidth, double scaleheight) { if (this.state.ctm == null) { this.state.ctm = ST_Array.unitCTM(); } ST_Array scale = new ST_Array(scalewidth, 0, 0, scaleheight, 0, 0); this.state.ctm = scale.mtxMul(this.state.ctm); return this; } /** * 旋转当前的绘图 * * @param angle 旋转角度(0~360) * @return this */ public DrawContext rotate(double angle) { if (this.state.ctm == null) { this.state.ctm = ST_Array.unitCTM(); } double alpha = angle * Math.PI / 180d; ST_Array r = new ST_Array(Math.cos(alpha), Math.sin(alpha), -Math.sin(alpha), Math.cos(alpha), 0, 0); this.state.ctm = r.mtxMul(this.state.ctm); return this; } /** * 重新映射画布上的 (0,0) 位置 * * @param x 添加到水平坐标(x)上的值 * @param y 添加到垂直坐标(y)上的值 * @return this */ public DrawContext translate(double x, double y) { if (this.state.ctm == null) { this.state.ctm = ST_Array.unitCTM(); } ST_Array r = new ST_Array(1, 0, 0, 1, x, y); this.state.ctm = r.mtxMul(this.state.ctm); return this; } /** * 变换矩阵 *

* 每次变换矩阵都会在前一个变换的基础上进行 * * @param a 水平缩放绘图 * @param b 水平倾斜绘图 * @param c 垂直倾斜绘图 * @param d 垂直缩放绘图 * @param e 水平移动绘图 * @param f 垂直移动绘图 * @return this */ public DrawContext transform(double a, double b, double c, double d, double e, double f) { if (this.state.ctm == null) { this.state.ctm = ST_Array.unitCTM(); } ST_Array r = new ST_Array(a, b, c, d, e, f); this.state.ctm = r.mtxMul(this.state.ctm); return this; } /** * 设置变换矩阵 *

* 每当调用 setTransform() 时,它都会重置前一个变换矩阵然后构建新的矩阵 * * @param a 水平缩放绘图 * @param b 水平倾斜绘图 * @param c 垂直倾斜绘图 * @param d 垂直缩放绘图 * @param e 水平移动绘图 * @param f 垂直移动绘图 * @return this */ public DrawContext setTransform(double a, double b, double c, double d, double e, double f) { this.state.ctm = new ST_Array(a, b, c, d, e, f); return this; } /** * 裁剪图片并在OFD上绘制图像 *

* 主要该方法将会裁剪图片的一部分,然后在OFD上绘制 * 该方法所有参数单位都是毫米mm,像素转换毫米需要通过 {@link #PPM} 转换。 * * @param img 图像,路径 * @param sx 图像内部 x 坐标(单位 毫米mm) * @param sy 图像内部 y 坐标(单位 毫米mm) * @param sWidth 图像内部宽度(单位 像素px) * @param sHeight 图像内部高度(单位 像素px) * @param dx 在画布上放置图像的 x 坐标位置(单位 毫米mm) * @param dy 在画布上放置图像的 y 坐标位置(单位 毫米mm) * @param dWidth 在画布上放置图像的宽度(单位 毫米mm) * @param dHeight 在画布上放置图像的高度(单位 毫米mm) * @return this * @throws IOException 图像读取异常 */ public DrawContext drawImage(Path img, double sx, double sy, double sWidth, double sHeight, double dx, double dy, double dWidth, double dHeight) throws IOException { if (img == null || Files.notExists(img)) { throw new IllegalArgumentException("图像不存在"); } // 加载原图片 BufferedImage gImg = ImageIO.read(img.toFile()); int w = pixel(sWidth); int h = pixel(sHeight); // 按照区域裁剪图片 BufferedImage cutOut = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); Graphics2D g2 = cutOut.createGraphics(); g2.drawImage(gImg, 0, 0, w, h, pixel(sx), pixel(sy), pixel(sx) + w, pixel(sy) + h, null); Path tmpImgCutPath = null; try { // 裁剪后的图片存储到临时文件 tmpImgCutPath = Files.createTempFile("", ".png"); ImageIO.write(cutOut, "png", tmpImgCutPath.toFile()); return drawImage(tmpImgCutPath, dx, dy, dWidth, dHeight); } finally { if (tmpImgCutPath != null) { Files.deleteIfExists(tmpImgCutPath); } } } /** * 在OFD上绘制图像 *

* 图像宽度和高度将按照 {@link #PPM} 进行转换 * * @param img 要使用的图像,请避免资源和文档中已经存在的资源重复 * @param dx 在画布上放置图像的 x 坐标位置 * @param dy 在画布上放置图像的 y 坐标位置 * @return this * @throws IOException 图片文件读写异常 */ public DrawContext drawImage(Path img, double dx, double dy) throws IOException { if (img == null || Files.notExists(img)) { throw new IllegalArgumentException("图像不存在"); } // 加载原图片 BufferedImage gImg = ImageIO.read(img.toFile()); int w = gImg.getWidth(); int h = gImg.getHeight(); return drawImage(img, dx, dy, mm(w), mm(h)); } /** * 在OFD上绘制图像 * * @param img 要使用的图像,请避免资源和文档中已经存在的资源重复 * @param dx 在画布上放置图像的 x 坐标位置 * @param dy 在画布上放置图像的 y 坐标位置 * @param dWidth 要使用的图像的宽度(伸展或缩小图像) * @param dHeight 要使用的图像的高度(伸展或缩小图像) * @return this * @throws IOException 图片文件读写异常 */ public DrawContext drawImage(Path img, double dx, double dy, double dWidth, double dHeight) throws IOException { if (img == null || Files.notExists(img)) { throw new IOException("图片(img)不存在"); } ST_ID id = resManager.addImage(img); // 在公共资源中加入图片 ImageObject imgObj = new ImageObject(maxUnitID.incrementAndGet()); imgObj.setResourceID(id.ref()); imgObj.setBoundary(boundary.clone()); // 应用变换矩阵 ST_Array ctm = this.state.ctm == null ? ST_Array.unitCTM() : this.state.ctm; ctm = new ST_Array(dWidth, 0, 0, dHeight, dx, dy).mtxMul(ctm); imgObj.setCTM(ctm); // 应用绘制参数 applyDrawParam(imgObj); container.addPageBlock(imgObj); return this; } /** * 保存当前绘图状态 *

* 与 {@link #restore()} 总是成对出现。 * * @return this */ public DrawContext save() { this.state.strokeStyle = this.strokeStyle; this.state.fillStyle = this.fillStyle; this.state.fontStyle = this.font; stack.push(this.state.clone()); return this; } /** * 还原绘图状态 *

* 与 {@link #save()} 总是成对出现。 * * @return this */ public DrawContext restore() { if (stack.isEmpty()) { return this; } this.state = stack.pop(); this.strokeStyle = this.state.strokeStyle; this.fillStyle = this.state.fillStyle; this.font = this.state.fontStyle; return this; } /** * 填充文字 * * @param text 填充文字 * @param x 阅读方向上的左下角 x坐标 * @param y 阅读方向上的左下角 y坐标 * @return this * @throws IOException 字体获取异常 */ public DrawContext fillText(String text, double x, double y) throws IOException { if (text == null || text.trim().isEmpty()) { return this; } ST_ID fontID = null; // 转换字体样式 为 字体设置 CT_Font existFont = fontStyleToSetting(this.font, this.state.font); if (existFont != null) { fontID = existFont.getID(); } else { fontID = resManager.addFont(state.font.getFont()); } // 新建字体对象 TextObject txtObj = new CT_Text() .setBoundary(this.boundary.clone()) .setFont(fontID.ref()) .setSize(state.font.getFontSize()) .toObj(new ST_ID(maxUnitID.incrementAndGet())); // 设置填充 txtObj.setFill(true); // 设置字体宽度 if (state.font.getFontWeight() != null && state.font.getFontWeight() != 400) { txtObj.setWeight(Weight.getInstance(state.font.getFontWeight())); } // 是否斜体 if (state.font.isItalic()) { txtObj.setItalic(true); } int readDirection = state.font.getReadDirection(); int charDirection = state.font.getCharDirection(); // 设置阅读方向 if (readDirection != 0) { txtObj.setReadDirection(Direction.getInstance(readDirection)); } // 设置文字方向 if (charDirection != 0) { txtObj.setCharDirection(Direction.getInstance(charDirection)); } // 测量字间距 MeasureBody measureBody = TextMeasureTool.measureWithWith(text, state.font); // 第一个字母的偏移量计算 double xx = x + measureBody.firstCharOffsetX; double yy = y + measureBody.firstCharOffsetY; switch (readDirection) { case 0: case 180: xx += textFloatFactor(state.font.getTextAlign(), measureBody.width, readDirection); break; case 90: case 270: yy += textFloatFactor(state.font.getTextAlign(), measureBody.width, readDirection); break; } TextCode tcSTTxt = new TextCode().setContent(text).setX(xx).setY(yy); if (readDirection == 90 || readDirection == 270) { tcSTTxt.setDeltaY(measureBody.offset); } else { tcSTTxt.setDeltaX(measureBody.offset); } txtObj.addTextCode(tcSTTxt); // 应用绘制参数 applyDrawParam(txtObj); // 加入容器 container.addPageBlock(txtObj); return this; } /** * 文本浮动带来的偏移量因子 * * @param align 对齐方向 * @param width 文本宽度 * @param readDirection 阅读方向 * @return 浮动因子 */ private double textFloatFactor(TextAlign align, double width, int readDirection) { double factor = 0; switch (align) { case start: case left: factor = 0; break; case end: case right: factor = -width; break; case center: factor = -width / 2; break; } if (readDirection == 180 || readDirection == 270) { factor = -factor; } return factor; } /** * 获取文本对齐方式 * * @return 文本对齐方式 */ public TextAlign getTextAlign() { return this.state.font.getTextAlign(); } /** * 设置文本对齐方式 * * @param textAlign 文本对齐方式 * @return this */ public DrawContext setTextAlign(TextAlign textAlign) { this.state.font.setTextAlign(textAlign); return this; } /** * 测量文本的宽度或高度 *

* 如果 readDirection为 0或180,测量文本宽度 *

* 如果 readDirection为 0或180,测量文本高度 * * @param text 带测量文本 * @return 测量文本信息 */ public TextMetrics measureText(String text) { // 转换字体样式 为 字体设置 fontStyleToSetting(this.font, this.state.font); TextMetrics tm = new TextMetrics(); tm.readDirection = state.font.getReadDirection(); tm.fontSize = state.font.getFontSize(); // 测量字间距 MeasureBody measureBody = TextMeasureTool.measureWithWith(text, state.font); tm.width = measureBody.width; tm.offset = measureBody.offset; return tm; } /** * 测量文本所占空间大小 * * @param text 带测量文本 * @return 文件所占空间信息 */ public TextMetricsArea measureTextArea(String text) { // 转换字体样式 为 字体设置 fontStyleToSetting(this.font, this.state.font); // 测量字间距 return TextMeasureTool.measureArea(text, state.font); } /** * 读取当前描边颜色(只读) *

* 若描边颜色非颜色值,则返回null * * @return 描边颜色(只读) */ public int[] getStrokeColor() { if (this.strokeStyle instanceof String) { return NamedColor.rgb((String) this.strokeStyle); } return null; } /** * 设置描边颜色 *

* 一条路径只有一种描边颜色,重复设置只取最后一次设置颜色 * * @param strokeColor 描边的RGB颜色 * @return this */ public DrawContext setStrokeColor(int[] strokeColor) { this.strokeStyle = String.format("#%02X%02X%02X", strokeColor[0], strokeColor[1], strokeColor[2]); return this; } /** * 设置描边颜色 *

* 一条路径只有一种描边颜色,重复设置只取最后一次设置颜色 * * @param r 红 * @param g 绿 * @param b 蓝 * @return this */ public DrawContext setStrokeColor(int r, int g, int b) { return setStrokeColor(new int[]{r, g, b}); } /** * 获取填充颜色(只读) *

* 若填充颜色非颜色值,则返回null * * @return 填充颜色(只读) */ public int[] getFillColor() { if (this.fillStyle instanceof String) { return NamedColor.rgb((String) this.fillStyle); } return null; } /** * 设置填充颜色 *

* 一条路径只有一种填充颜色,重复设置只取最后一次设置颜色 * * @param fillColor 填充颜色 * @return this */ public DrawContext setFillColor(int[] fillColor) { this.fillStyle = String.format("#%02X%02X%02X", fillColor[0], fillColor[1], fillColor[2]); return this; } /** * 设置填充颜色 *

* 一条路径只有一种填充颜色,重复设置只取最后一次设置颜色 * * @param r 红 * @param g 绿 * @param b 蓝 * @return this */ public DrawContext setFillColor(int r, int g, int b) { return setFillColor(new int[]{r, g, b}); } /** * 获取当前线宽度 * * @return 线宽度(单位毫米mm) */ public double getLineWidth() { return this.state.drawParam.getLineWidth(); } /** * 获取当前线宽度 * * @param lineWidth 线宽度(单位毫米mm) * @return this */ public DrawContext setLineWidth(double lineWidth) { if (lineWidth < 0) { lineWidth = 0.353; } this.state.drawParam.setLineWidth(lineWidth); return this; } /** * 获取当前使用的绘制文字设置 * * @return 绘制文字设置,可能为null */ public FontSetting getFont() { return state.font; } /** * 设置绘制文字信息 * * @param font 文字配置 * @return this */ public DrawContext setFont(FontSetting font) { this.state.font = font; // 清空字体样式 this.font = ""; return this; } /** * 设置默认字体 * * @param fontSize 字体大小 * @return this */ public DrawContext setDefaultFont(double fontSize) { this.state.font = FontSetting.getInstance(fontSize); return this; } /** * 获取绘图透明度值 * * @return 透明度值 0.0到1.0 */ public Double getGlobalAlpha() { return state.globalAlpha; } /** * 设置 绘图透明度值 * * @param globalAlpha 透明度值 0.0到1.0 * @return this */ public DrawContext setGlobalAlpha(Double globalAlpha) { if (globalAlpha == null || globalAlpha > 1) { globalAlpha = 1.0; } else if (globalAlpha < 0) { globalAlpha = 0d; } this.state.globalAlpha = globalAlpha; return this; } /** * 设置端点样式 *

* 默认值: LineCapType.Butt * * @param cap 端点样式 * @return this */ public DrawContext setLineCap(LineCapType cap) { this.state.drawParam.setCap(cap); return this; } /** * 设置端点样式 *

* 默认值: LineCapType.Butt * * @return 端点样式 */ public LineCapType getLineCap() { return this.state.drawParam.getCap(); } /** * 设置线条连接样式,指定了两个线的端点结合时采用的样式。 *

* 默认值:LineJoinType.Miter * * @param join 线条连接样式 * @return this */ public DrawContext setLineJoin(LineJoinType join) { this.state.drawParam.setJoin(join); return this; } /** * 获取线条连接样式 *

* 默认值:LineJoinType.Miter * * @return 线条连接样式 */ public LineJoinType getLineJoin() { return this.state.drawParam.getJoin(); } /** * 设置最大斜接长度,也就是结合点长度截断值 *

* 默认值:3.528 *

* 当Join不等于Miter时改参数无效 * * @param miterLimit 截断值 * @return this */ public DrawContext setMiterLimit(Double miterLimit) { this.state.drawParam.setMiterLimit(miterLimit); return this; } /** * 获取最大斜接长度,也就是结合点长度截断值 *

* 默认值:3.528 * * @return 截断值 */ public Double getMiterLimit() { return this.state.drawParam.getMiterLimit(); } /** * 设置线段虚线样式 * * @param dashOffset 虚线绘制偏移位置,如果没有则传入null * @param pattern 虚线的线段长度和间隔长度,有两个或多个值,第一个值指定了虚线线段的长度,第二个值制定了线段间隔的长度,依次类推。 * @return this */ public DrawContext setLineDash(Double dashOffset, Double[] pattern) { if (dashOffset == null && pattern == null) { this.state.drawParam.setDashPattern(null); this.state.drawParam.setDashOffset(null); return this; } if (pattern == null || pattern.length < 2) { throw new IllegalArgumentException("虚线的线段长度和间隔长度(pattern),不能为空并且需要大于两个以上的值"); } this.state.drawParam.setDashPattern(new ST_Array(pattern)); this.state.drawParam.setDashOffset(dashOffset); return this; } /** * 设置线段虚线样式 * * @param pattern 虚线的线段长度和间隔长度,有两个或多个值,第一个值指定了虚线线段的长度,第二个值制定了线段间隔的长度,依次类推。 * @return this */ public DrawContext setLineDash(Double... pattern) { return setLineDash(null, pattern); } /** * 获取虚线间隔参数 * * @return 虚线的线段长度和间隔长度, 有两个或多个值,第一个值指定了虚线线段的长度,第二个值制定了线段间隔的长度,依次类推。 */ public ST_Array getDashPattern() { return this.state.drawParam.getDashPattern(); } /** * 获取虚线绘制偏移位置 * * @return 虚线绘制偏移位置 */ public Double getDashOffset() { return this.state.drawParam.getDashOffset(); } /** * 创建一个线性渐变对象(double) * * @param x0 起始点横坐标 * @param y0 起始点纵坐标 * @param x1 结束点横坐标 * @param y1 结束点纵坐标 * @return 线性渐变对象 */ public CanvasGradient createLinearGradient(double x0, double y0, double x1, double y1) { return new CanvasGradient(x0, y0, x1, y1); } /** * 创建一个线性渐变对象(int) * * @param x0 起始点横坐标 * @param y0 起始点纵坐标 * @param x1 结束点横坐标 * @param y1 结束点纵坐标 * @return 线性渐变对象 */ public CanvasGradient createLinearGradient(int x0, int y0, int x1, int y1) { return new CanvasGradient(x0, y0, x1, y1); } /** * 创建一个重复底纹 *

* 注意:默认情况下使用 {@link #mm(int)} 将图片像素转换为OFD的单位毫米。 * 若有自定义重复图片大小设置可以使用 {@link CanvasPattern#setImageSize(double, double)} * * @param img 底纹图片路径 * @param repetition 重复方式,支持 repeat、column、row、row-column * @return 底纹对象 * @throws IOException 图片读取异常 */ public CanvasPattern createPattern(Path img, String repetition) throws IOException { img = img.toAbsolutePath(); if (Files.notExists(img)) { throw new IllegalArgumentException("底纹图片不存在:" + img); } ST_ID id = resManager.addImage(img); // 加载原图片 BufferedImage gImg = ImageIO.read(img.toFile()); double w = mm(gImg.getWidth()); double h = mm(gImg.getHeight()); // 在公共资源中加入图片 ImageObject imgObj = new ImageObject(maxUnitID.incrementAndGet()); imgObj.setResourceID(id.ref()); imgObj.setBoundary(new ST_Box(0, 0, w, h)); imgObj.setCTM(new ST_Array(w, 0, 0, h, 0, 0)); return new CanvasPattern(img, repetition, imgObj); } /** * 创建径向渐变颜色 * * @param x0 渐变的开始圆的 x 坐标 * @param y0 渐变的开始圆的 y 坐标 * @param r0 开始圆的半径 * @param x1 渐变的结束圆的 x 坐标 * @param y1 渐变的结束圆的 y 坐标 * @param r1 结束圆的半径 * @return 径向渐变颜色 */ public CanvasRadialGradient createRadialGradient(double x0, double y0, double r0, double x1, double y1, double r1) { return new CanvasRadialGradient(x0, y0, r0, x1, y1, r1); } /** * 应用当前上下文中的绘制参数到绘制对象 */ private void applyDrawParam(CT_GraphicUnit p) { if (p == null) { return; } // 设置区域 p.setBoundary(this.boundary.clone()); // 设置透明度 if (this.state.globalAlpha != null) { p.setAlpha((int) (255 * this.state.globalAlpha)); } // 设置变换矩阵 忽略已经设置了变换矩阵的图元 if (this.state.ctm != null && p.getCTM() == null) { p.setCTM(this.state.ctm.clone()); } // 设置填充颜色 CT_Color fillColor = detectColor(this.fillStyle); if (fillColor != null) { this.state.drawParam.setFillColor(fillColor); } // 设置描边颜色 CT_Color strokeColor = detectColor(this.strokeStyle); if (strokeColor != null) { this.state.drawParam.setStrokeColor(strokeColor); } // 设置绘制参数 ST_ID paramObjId = this.resManager.addDrawParam(this.state.drawParam); p.setDrawParam(paramObjId.ref()); // 设置裁剪区域 if (this.state.clipArea != null) { Clips clips = new Clips(); org.ofdrw.core.pageDescription.clips.Area area = new org.ofdrw.core.pageDescription.clips.Area(); CT_Path clipObj = new CT_Path().setAbbreviatedData(this.state.clipArea.clone()); clipObj.setFill(true); // 裁剪区域与Canvas等大 clipObj.setBoundary(new ST_Box(0, 0, this.boundary.getWidth(), boundary.getHeight())); if (this.state.ctm != null && !ONE.equals(this.state.ctm)) { // 由于图元内的裁剪区域受到图元的变换矩阵影响, // 而裁剪区域是位于未受到变换的原始画布上的区域, // 因此在图元内部的裁剪区为需要叠加一个图元内变换的逆变换, // 才可以实现向外部空间的映射。 ST_Array inverse = inverse(this.state.ctm); if (inverse == null) { // 获取获取可逆矩阵时放弃裁剪区 return; } clipObj.setCTM(inverse); } area.setClipObj(clipObj); clips.addClip(new CT_Clip().addArea(area)); p.setClips(clips); } } /** * 计算可逆矩阵 *

* 注意:初等变换一定存在可逆矩阵 * * @param ctm 变换矩阵 * @return 可逆矩阵 或 null */ private ST_Array inverse(ST_Array ctm) { if (ctm.size() < 6) { return null; } AffineTransform at = new AffineTransform(ctm.get(0), ctm.get(1), ctm.get(2), ctm.get(3), ctm.get(4), ctm.get(5)); AffineTransform tx = null; try { tx = at.createInverse(); } catch (NoninvertibleTransformException e) { return null; } return new ST_Array(tx.getScaleX(), tx.getShearY(), tx.getShearX(), tx.getScaleY(), tx.getTranslateX(), tx.getTranslateY()); } /** * 对路径应用变换矩阵 * * @param data 图形轮廓数据 * @param ctm 变换矩阵 */ public static void transform(AbbreviatedData data, ST_Array ctm) { AffineTransform at = new AffineTransform(ctm.get(0), ctm.get(1), ctm.get(2), ctm.get(3), ctm.get(4), ctm.get(5)); for (OptVal optVal : data.getRawOptVal()) { switch (optVal.opt) { case "S": case "M": case "L": { double[] arr = optVal.expectValues(); double[] dst = new double[2]; at.transform(arr, 0, dst, 0, 1); optVal.setValues(dst); continue; } case "Q": { double[] arr = optVal.expectValues(); double[] dst = new double[4]; at.transform(arr, 0, dst, 0, 2); optVal.setValues(dst); continue; } case "B": { double[] arr = optVal.expectValues(); double[] dst = new double[6]; at.transform(arr, 0, dst, 0, 3); optVal.setValues(dst); continue; } case "A": { // [0]rx [1]ry [2]angle [3]large [4]sweep [5]x [6]y double[] arr = optVal.expectValues(); double rx = arr[0] * at.getScaleX(); double ry = arr[1] * at.getScaleY(); double[] ptDst = new double[2]; at.transform(arr, 5, ptDst, 0, 1); optVal.setValues(new double[]{rx, ry, arr[2], arr[3], arr[4], ptDst[0], ptDst[1]}); } case "C": default: } } } /** * 根据颜色类型推断颜色对象 *

* 若无法推断或参数错误则返回null * * @param color 颜色类型 * @return OFD颜色对象 */ private CT_Color detectColor(Object color) { if (color == null) { return null; } if (color instanceof String) { int[] rgb = NamedColor.rgb((String) color); if (rgb != null) { CT_Color c = CT_Color.rgb(rgb[0], rgb[1], rgb[2]); if (rgb.length > 3) { // 颜色参数包含透明度,设置透明度 c.setAlpha(rgb[3]); } return c; } return null; } else if (color instanceof ColorClusterType) { CT_Color res = new CT_Color(); res.setColor((ColorClusterType) color); return res; } else if (color instanceof CT_Color) { return (CT_Color) color; } else if (color instanceof CanvasGradient) { // 渐变颜色 CT_Color res = new CT_Color(); res.setColor(((CanvasGradient) color).axialShd); return res; } else if (color instanceof CanvasPattern) { // 图案颜色 CT_Color res = new CT_Color(); res.setColor(((CanvasPattern) color).pattern); return res; } else if (color instanceof CanvasRadialGradient) { // 径向渐变颜色 CT_Color res = new CT_Color(); res.setColor(((CanvasRadialGradient) color).radialShd); return res; } return null; } /** * 解析字体配置字符串为字体配置对象 *

* 解析 font 字符串,格式为:[font-style] [font-weight] font-size font-family [] 表示可选 *

* font-style: normal | italic * font-weight: normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 * font-size: 12px | 3.17mm * font-family: 宋体 | SimSun | Times New Roman | Times | serif | sans-serif | monospace | cursive | fantasy * font-family 为必选项,其他为可选项 *

* font-size 可以使用 px 或 mm 作为单位,若不指定单位则默认为 mm * * @param fontSettingStr 字体配置字符串 * @param fs 字体配置对象 * @return 若文档已经存在字体,那么返回它的字体对象,若不存咋那么只设置 FontSetting 并返还null */ private CT_Font fontStyleToSetting(String fontSettingStr, FontSetting fs) { if (fontSettingStr == null || fontSettingStr.isEmpty() || fs == null) { return null; } String[] arr = fontSettingStr.trim().split(" "); if (arr.length < 2) { return null; } /* * 设置 字体 名称 */ int off = arr.length - 1; String fontFamily = arr[off].trim(); Font nameFont = new Font(fontFamily, fontFamily); CT_Font ctFont = null; ExistCTFont existCTFont = resManager.getFont(fontFamily); if (existCTFont != null) { ctFont = existCTFont.font; if (existCTFont.absPath != null) { nameFont = new Font(ctFont.getFontName(), ctFont.getFamilyName(), existCTFont.absPath); } else { nameFont = new Font(ctFont.getFontName(), ctFont.getFamilyName()); } } fs.setFont(nameFont); /* * 设置 字号 */ off--; // 解析font-size 单位可能是 px 或 mm 或 空 String fontSizeStr = arr[off].trim(); double fontSize = 1; try { if (fontSizeStr.endsWith("px")) { fontSizeStr = fontSizeStr.substring(0, fontSizeStr.length() - 2).trim(); fontSize = Double.parseDouble(fontSizeStr) / PPM; } else if (fontSizeStr.endsWith("mm")) { fontSizeStr = fontSizeStr.substring(0, fontSizeStr.length() - 2).trim(); fontSize = Double.parseDouble(fontSizeStr); } else { fontSize = Double.parseDouble(fontSizeStr); } } catch (NumberFormatException e) { fontSize = 1; } fs.setFontSize(fontSize); off--; if (off < 0) { return ctFont; } /* * 设置 字体粗细 */ int fontWeight = 400; // 解析font-weight // normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900 switch (arr[off].trim().toLowerCase()) { case "lighter": case "100": fontWeight = 100; off--; break; case "200": fontWeight = 200; off--; break; case "300": fontWeight = 300; off--; break; case "normal": case "400": fontWeight = 400; off--; break; case "500": fontWeight = 500; off--; break; case "600": fontWeight = 600; off--; break; case "bold": case "700": fontWeight = 700; off--; break; case "800": fontWeight = 800; off--; break; case "bolder": case "900": fontWeight = 900; off--; break; default: // 非字体宽度配置,忽略 } fs.setFontWeight(fontWeight); if (off < 0) { return ctFont; } /* * 设置 是否斜体 */ // 解析 font-style: normal | italic switch (arr[off].trim()) { case "italic": fs.setItalic(true); break; case "normal": default: fs.setItalic(false); // 非字体样式配置,忽略 } return ctFont; } /** * 像素转换为毫米 * * @param pixel 像素 * @return 毫米 */ public double mm(int pixel) { return (double) pixel / PPM; } /** * 毫米转换为像素 * * @param mm 毫米 * @return 像素 */ public int pixel(double mm) { return (int) (mm * PPM); } /** * 添加字体至文档资源中 * * @param name 字体名称 * @param p 字体文件路径 * @return this * @throws IOException 字体解析异常 */ public DrawContext addFont(String name, Path p) throws IOException { resManager.addFont(new Font(name, p)); return this; } /** * 结束绘制器绘制工作 */ @Override public void close() { } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy