Excel添加水印功能

实现ExcelWriteHandlerExtendApi接口从而实现对Excel增加复杂功能的操作。如添加水印。具体实现请自行百度


    /**
     * 根据上下文判断是否执行
     *
     * @param context Excel定义上下文
     * @return 是否执行该扩展
     */
    boolean match(ExcelDefinitionContext context);

    /**
     * 构建出一个WriteWorkbook对象,即一个工作簿对象,对应的是一个Excel文件;
     *
     * @param builder 可用于设置inMemory=true实现复杂功能(如添加水印)
     */
    default void extendBuilder(ExcelWriterBuilder builder) {
    }

例:Excel添加水印

本例参考文章:Java使用EasyExcel导出添加水印

实现ExcelWriteHandlerExtendApi接口,并添加@Component注解

添加依赖包

        <!-- eaysexcel -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>3.3.2</version>
        </dependency>
        <!-- poi 添加水印 -->
        <dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>ooxml-schemas</artifactId>
            <version>1.4</version>
        </dependency>
        <!-- 使用了hutool的工具类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.20</version>
        </dependency>
package pro.shushi.pamirs.top.core.temp;

import cn.hutool.core.img.ImgUtil;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder;
import org.apache.poi.openxml4j.opc.PackagePartName;
import org.apache.poi.openxml4j.opc.PackageRelationship;
import org.apache.poi.openxml4j.opc.TargetMode;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFPictureData;
import org.apache.poi.xssf.usermodel.XSSFRelation;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.file.api.context.ExcelDefinitionContext;
import pro.shushi.pamirs.file.api.easyexcel.ExcelWriteHandlerExtendApi;

import java.awt.*;
import java.awt.image.BufferedImage;

@Component
public class CustomWaterMarkHandler implements ExcelWriteHandlerExtendApi {

    private final WaterMark watermark;

    public CustomWaterMarkHandler() {
        this.watermark = new WaterMark().setContent("ABC");
    }

    @Override
    public void extendBuilder(ExcelWriterBuilder builder) {
        builder.inMemory(true);
    }

    @Override
    public boolean match(ExcelDefinitionContext context) {
        return DemoTemplate.TEMPLATE_NAME.equals(context.getName());
    }

    @Override
    public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
        try {
            BufferedImage bufferedImage = createWatermarkImage();
            setWaterMarkToExcel((XSSFWorkbook) writeWorkbookHolder.getWorkbook(), bufferedImage);
        } catch (Exception e) {
            throw new RuntimeException("添加水印出错",e);
        }
    }

    private BufferedImage createWatermarkImage() {
        final Font font = watermark.getFont();
        final int width = watermark.getWidth();
        final int height = watermark.getHeight();

        String[] textArray = watermark.getContent().split(",");
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 背景透明 开始
        Graphics2D g = image.createGraphics();
        image = g.getDeviceConfiguration().createCompatibleImage(width, height, Transparency.TRANSLUCENT);
        g.dispose();
        // 背景透明 结束
        g = image.createGraphics();
        // 设定画笔颜色
        g.setColor(new Color(Integer.parseInt(watermark.getColor().substring(1), 16)));
        // 设置画笔字体
        g.setFont(font);
        // 设定倾斜度
        g.shear(watermark.getShear1(), watermark.getShear2());

        // 设置字体平滑
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        int y = watermark.getYAxis();
        for (String s : textArray) {
            // 从画框的y轴开始画字符串.假设电脑屏幕中心为0,y轴为正数则在下方
            g.drawString(s, 0, y);
            y = y + font.getSize();
        }

        // 释放画笔
        g.dispose();
        return image;
    }

    private void setWaterMarkToExcel(XSSFWorkbook workbook, BufferedImage bfi) {
        //将图片添加到工作簿
        int pictureIdx = workbook.addPicture(ImgUtil.toBytes(bfi, ImgUtil.IMAGE_TYPE_PNG), Workbook.PICTURE_TYPE_PNG);
        //建立 sheet 和 图片 的关联关系
        XSSFPictureData xssfPictureData = workbook.getAllPictures().get(pictureIdx);
        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
            XSSFSheet xssfSheet = workbook.getSheetAt(i);
            PackagePartName packagePartName = xssfPictureData.getPackagePart().getPartName();
            PackageRelationship packageRelationship = xssfSheet.getPackagePart()
                    .addRelationship(packagePartName, TargetMode.INTERNAL, XSSFRelation.IMAGES.getRelation(), null);
            //添加水印到工作表
            xssfSheet.getCTWorksheet().addNewPicture().setId(packageRelationship.getId());
        }
    }

}
水印配置类
/**
 * 水印配置类
 */
@Data
@Component
public class WaterMark {
    /**
     * 水印内容
     */
    private String content = "";

    /**
     * 画笔颜色. eg:#C5CBCF
     */
    private String color = "#C5CBCF";

    /**
     * 字体颜色
     */
    private Font font = new Font("microsoft-yahei", Font.PLAIN, 20);

    /**
     * 水印宽、高
     */
    private int width = 300;
    private int height = 100;

    /**
     * 倾斜度
     */
    private double shear1 = 0.1;
    private double shear2 = -0.26;

    /**
     * 字体的y轴位置
     */
    private int yAxis = 50;
}

Oinone社区 作者:yexiu原创文章,如若转载,请注明出处:https://doc.oinone.top/dai-ma-shi-jian/17119.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

Like (1)
yexiu's avataryexiu数式员工
Previous 2024年9月6日 pm2:09
Next 2024年9月9日 pm8:06

相关推荐

  • 部分模型不动态修改表结构(由单独DDL处理)

    需求描述 实际项目中, 存在部分模型不动态修改表结构,由单独DDL脚本处理,常见的场景有: 已存在库和表中使用Oinone进行功能开发,此时对于已经存在的表对应的模型不允许改表结构 其他情况不希望动态改变表结构的情况 实现步骤 新建NODDL的基础模型 模型公共字段 公共字段说明:使用Oinone进行开发时,业务模型需继承基础IdModel(或者由IdModel衍生出的子类),这些基础模型有createDate(创建时间)、writeDate(更新时间)、createUid(创建人ID)和writeUid(更新人ID)等公共字段;实际表中公共字段可能与Oinone有所不同。 实现方式 方式1:公共属性字段用平台提供的createDate、writeDate、createUid和writeUid,通过指定column与表中的实际字段对应.【推荐】该方式,公共字段的处理可以继续使用平台的默认赋值处理方式; 方式2:继承平台的时候,把公共字段排除掉(配置unInheritedFields),然后自行加通用字段:排除字段:@Model.Advanced(type= ModelTypeEnum.ABSTRACT, ordering = "createAt DESC, id DESC", unInheritedFields = {"createUid","writeUid","createDate","writeDate"})【不推荐】该方式,公共字段的赋值逻辑需要自行处理,略显复杂; 实现方式举例 下面的示例以方式1举例;假设表的基础字段分别是:createAt、updateAt、createId和updateId 与平台的不同. 不自动DDL的抽象模型示例 import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.IdModel; import pro.shushi.pamirs.meta.enmu.FieldStrategyEnum; import pro.shushi.pamirs.meta.enmu.ModelTypeEnum; import java.util.Date; /** * 假设表的基础字段分别是:createAt、updateAt、createId和updateId 与平台的不同 */ @Model.model(BaseNoDdlModel.MODEL_MODEL) @Model(displayName = "不自动DDL的抽象模型") @Model.Advanced(type= ModelTypeEnum.ABSTRACT, ordering = "createAt DESC, id DESC") public abstract class BaseNoDdlModel extends IdModel { public static final String MODEL_MODEL = "hr.std.BaseNoDdlModel"; // 如果原表的主键的列名不是ID的情况,这里可以定义column指定ID属性对应的列名 /** @Field.PrimaryKey @Field(displayName = "主键ID") @Field.Advanced(column = "XLH") private Long id; **/ // 下面这几个字段按实际项目中的情况来增加,包括字段名 @Field.Advanced(columnDefinition = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP", column = "createAt", insertStrategy = FieldStrategyEnum.NEVER, updateStrategy = FieldStrategyEnum.NEVER, batchStrategy = FieldStrategyEnum.NEVER) @Field(displayName = "创建时间", priority = 200) private Date createDate; @Field.Advanced(columnDefinition = "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP", column = "updateAt", batchStrategy = FieldStrategyEnum.NEVER) @Field(displayName = "更新时间", priority = 210) private Date writeDate; @Field.Advanced(column = "createId") @Field(displayName = "创建人ID", priority = 220, invisible = true) private Long createUid; @Field.Advanced(column = "updateId") @Field(displayName = "更新人ID", priority = 230, invisible = true) private Long writeUid; } 不需动态DDL的业务模型,业务模型继承BaseNoDdlModel。 其他业务模型如果有相同的需求类似的做法 /** * 测试合同表 */ @Model.model(InspectionInfo.MODEL_MODEL) @Model(displayName = "合同", labelFields…

    2025年2月22日
    1.5K00
  • 同一行操作跳转到不同的视图(动态表单)

    背景 实际项目中,存在这样的场景:同一列表中的数据是泛化的数据集合,即数据来源于不同的模型;行操作需要根据来源去向不同的目标页。 如下图,「提报」操作需根据「报表类型」去向不同的表单。 并支持目标弹窗标题和弹框大小的配置。 解决思路 每行记录需要跳转到不同的模型不同视图,增加一个配置页面用于维护源模型和目标模型的调整动作关系; 返回数据的时候,同时返回自定义的动作。 前端自定义实现如上面图例中的「填报」,从返回数据中获取ViewAction并做对应的跳转。 具体步骤 [后端] 建立模型和视图的关系设置的模型 1、创建 模型和视图的关系设置的模型,用于配置列表模型和各记录即目标模型的视图关系 import pro.shushi.oinone.examples.simple.api.proxy.system.SimpleModel; import pro.shushi.oinone.examples.simple.api.proxy.system.SimpleModule; import pro.shushi.pamirs.boot.base.enmu.ActionTargetEnum; import pro.shushi.pamirs.boot.base.model.View; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.base.IdModel; import pro.shushi.pamirs.meta.enmu.ViewTypeEnum; /** * 模型和视图的关系设置 * ModelRelViewSetting */ @Model.model(ModelRelViewSetting.MODEL_MODEL) @Model(displayName = "模型和视图的关系设置", summary = "模型和视图的关系设置") @Model.Advanced(unique = {"oModel,model,target,viewType,viewName"}) public class ModelRelViewSetting extends IdModel { public static final String MODEL_MODEL = "examples.custom.ModelRelViewSetting"; @Field.many2one @Field(displayName = "模块") @Field.Relation(relationFields = {"module"}, referenceFields = {"module"}) private SimpleModule moduleDef; @Field.String @Field(displayName = "模块编码", invisible = true) private String module; @Field.many2one @Field(displayName = "源模型") @Field.Relation(relationFields = {"oModel"}, referenceFields = {"model"}) private SimpleModel originModel; @Field.String @Field(displayName = "源模型编码", invisible = true) private String oModel; @Field.many2one @Field(displayName = "目标模型") @Field.Relation(relationFields = {"model"}, referenceFields = {"model"}) private SimpleModel targetModel; @Field.String @Field(displayName = "目标模型编码", invisible = true) private String model; @Field.Enum @Field(displayName = "视图类型") private ViewTypeEnum viewType; @Field.Enum @Field(displayName = "打开方式", required = true) private ActionTargetEnum target; @Field.String @Field(displayName = "动作名称", invisible = true) private String name; @Field.many2one @Field.Relation(relationFields = {"model", "viewName"}, referenceFields = {"model", "name"}, domain = "systemSource=='UI'") @Field(displayName = "绑定页面", summary = "绑定页面") private View view; @Field.String @Field(displayName = "视图/页面", invisible…

    2025年2月19日
    1.5K00
  • 如何跳过固定path路径下面所有的按钮权限

    场景: 业务上需要跳过弹窗打开里面的所有按钮权限。 实践: 实现AuthFilterService权限接口。 package pro.shushi.pamirs.top.api.spi; import org.apache.commons.lang3.StringUtils; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import pro.shushi.pamirs.auth.api.spi.AuthFilterService; import pro.shushi.pamirs.boot.web.session.AccessResourceInfoSession; import pro.shushi.pamirs.meta.common.spi.SPI; /** * @author Yexiu at 09:04 on 2024/9/27 */ @Order(88) @Component @SPI.Service public class CustomAuthFilterService implements AuthFilterService { public static final String skipPath = "/top_demo/uiMenuc6238c29bca44250a041691565056a63/ACTION#top.Teacher#uiView2b60cc6daa334c7280cb78207d41addc"; @Override public Boolean isAccessAction(String model, String name) { String path = AccessResourceInfoSession.getInfo().getOriginPath(); if (StringUtils.isNotEmpty(path) && path.startsWith(skipPath)) { //返回true就代表通过验证 return true; } return null; } @Override public Boolean isAccessAction(String path) { if (StringUtils.isNotEmpty(path) && path.startsWith(skipPath)) { //返回true就代表通过验证 return true; } return null; } } 可以看到弹窗下面的按钮都不需要权限控制了。

    2025年3月11日
    1.2K00
  • 如何改变调度策略,让Schedule独立执行线程

    schedule里,相同的taskType跑多个业务任务,如果其中一个任务大量重试占满了调度线程,会影响别的业务任务及时被执行,如下面截图中,taskType用的是平台内置的常量,这个常量会被其他任务也使用,如果当前任务出现了异常占用了这个taskType的所有线程,那么这个taskType下面的其他任务就会被阻塞延后执行。应该给需要业务及时性的任务单独建立自定义的taskType,这样每个taskType的线程就是独立的,A任务异常不会影响B任务的执行。 1、后台创建task type相关的类,继承BaseScheduleNoTransactionTask,要加springbean的注解,参考:task type建议使用类名 2、提交任务的时候,设置tasktype为步骤1的TaskType 3、控制台新增策略和任务bean名称为步骤1的spring beanName,任务名称 $xxx,右边的占位符内容为yml里面配置的ownSign字段任务的名称也是步骤1的 spring beanName 4、配置完成后,控制台启动任务,就可以测试了

    2024年2月20日
    1.2K00
  • 查询时自定义排序字段和排序规则

    指定字段排序 平台默认排序字段,参考IdModel,按创建时间和ID倒序(ordering = "createDate DESC, id DESC") 方法1:模型指定排序 模型定义增加排序字段。@Model.Advanced(ordering = "xxxxx DESC, yyyy DESC") @Model.model(PetShop.MODEL_MODEL) @Model(displayName = "宠物店铺",summary="宠物店铺",labelFields ={"shopName"}) @Model.Code(sequence = "DATE_ORDERLY_SEQ",prefix = "P",size=6,step=1,initial = 10000,format = "yyyyMMdd") @Model.Advanced(ordering = "createDate DESC") public class PetShop extends AbstractDemoIdModel { public static final String MODEL_MODEL="demo.PetShop"; // ………… } 方法2:Page查询中可以自定排序规则 API参考 pro.shushi.pamirs.meta.api.dto.condition.Pagination#orderBy public <G, R> Pagination<T> orderBy(SortDirectionEnum direction, Getter<G, R> getter) { if (null == getSort()) { setSort(new Sort()); } getSort().addOrder(direction, getter); return this; } 具体示例 @Function.Advanced(type= FunctionTypeEnum.QUERY) @Function.fun(FunctionConstants.queryPage) @Function(openLevel = {FunctionOpenEnum.API}) public Pagination<PetShop> queryPage(Pagination<PetShop> page, IWrapper<PetShop> queryWrapper){ page.orderBy(SortDirectionEnum.DESC, PetShop::getCreateDate); page = new PetShop().queryPage(page, queryWrapper); return page; } 方法3:查询的wapper中指定 API参考:pro.shushi.pamirs.framework.connectors.data.sql.AbstractWrapper#orderBy @Override public Children orderBy(boolean condition, boolean isAsc, R… columns) { if (ArrayUtils.isEmpty(columns)) { return typedThis; } SqlKeyword mode = isAsc ? ASC : DESC; for (R column : columns) { doIt(condition, ORDER_BY, columnToString(column), mode); } return typedThis; } 具体示例 public List<PetShop> queryList(String name) { List<PetShop> petShops = Models.origin().queryListByWrapper( Pops.<PetShop>lambdaQuery().from(PetShop.MODEL_MODEL) .orderBy(true, true, PetShop::getCreateDate) .orderBy(true, true, PetShop::getId) .like(PetShop::getShopName, name)); return petShops; } 设置查询不排序 方法1:关闭平台默认排序字段,设置模型的ordering,改成:ordering = "1=1" 模型定义增加排序字段。@Model.Advanced(ordering = "1=1") @Model.model(PetShop.MODEL_MODEL) @Model(displayName = "宠物店铺",summary="宠物店铺",labelFields ={"shopName"}) @Model.Code(sequence = "DATE_ORDERLY_SEQ",prefix = "P",size=6,step=1,initial = 10000,format = "yyyyMMdd") @Model.Advanced(ordering =…

    2024年5月25日
    2.8K00

Leave a Reply

Please Login to Comment