精华内容
下载资源
问答
  • 导出word文档生成docx格式 添加水印

    千次阅读 2019-12-23 17:56:02
    为了导出docx格式看了等多文档,最后做个总结依赖包用到dom4j和freemarker,最为方便。 <!-- https://mvnrepository.com/artifact/freemarker/freemarker --> <dependency> <groupId>...

    为了导出docx格式看了等多文档,最后做个总结依赖包用到dom4j和freemarker,最为方便。

    <!-- https://mvnrepository.com/artifact/freemarker/freemarker -->
    		<dependency>
    			<groupId>freemarker</groupId>
    			<artifactId>freemarker</artifactId>
    			<version>2.3.9</version>
    		</dependency>
    
    		<!-- https://mvnrepository.com/artifact/dom4j/dom4j -->
    		<dependency>
    			<groupId>dom4j</groupId>
    			<artifactId>dom4j</artifactId>
    			<version>1.6.1</version>
    		</dependency>

    0.主要目的:将这样一个页面导出为word文档为doc格式,包含一些文本和循环遍历出来的echarts图表。

     1.新建一个word文档(docx格式)带水印,生成模板内容,例如下面这种。

    水印添加位置(可自己百度)

    整体思路

    -保存后,复制出来一份,

    -修改后缀名为zip。

    -解压到一个文件夹中。

    -打开文件夹看到如下目录

    主要思路:


    -获取word里的document.xml文档以及_rels文件夹下的document.xml.rels文档
    -把内容填充到document.xml里,以及图片配置信息填充至document.xml.rels文档里

    -把水印内容填充到header1.xml,header2.xml,header3.xml里,以及图片配置信息填充至document.xml.rels文档里
    -在输入docx文档的时候把填充过内容的的 document.xml、document.xml.rels用流的方式写入zip(详见下面代码)。
    -把图片写入zip文件下word/media文件夹中
    -输出docx文档(因为word文档本身就是ZIP格式实现的)

    2. 目录结构如下:主要文件由上一步拷贝过来的

    • document.xml里存放主要数据
    • media存放图片信息
    • _rels里存放配置信息

    document.xml中存放图片的模板主要内容

    3.document.xml修改模板内容加上freemarker遍历map集合,填入数据

     4.document.xml.rels修改模板引用内容

    注意:这里图片配置信息是根据 rId来获取的。docx模板总的${mdl.rId}就是rId的具体值。
    为了避免重复,我的图片rId从12开始(在我没有修改之前,里面最大的rId是rId12)。

    5.header1.xml,header2.xml,header3.xml页眉 ,修改红框内容即可

    6.[Content_Types].xml文件模板

    7.还需要在document.xml的最后中加入红框内容,header.xml文件的位置对应的r:id序号 

    8.工具类代码如下

    package com.sl.utils.office.word;
    
    import com.sl.utils.date.DateUtils;
    import com.sl.utils.freemark.FreeMarkUtils;
    import org.dom4j.Document;
    import org.dom4j.DocumentHelper;
    import org.dom4j.Element;
    
    import java.io.*;
    import java.net.URL;
    import java.util.*;
    import java.util.zip.ZipEntry;
    import java.util.zip.ZipFile;
    import java.util.zip.ZipOutputStream;
    
    /**
     * docx、doc文档生成工具类  (改变后缀名即可)
     * 在使用制作模板的过程中如果模板中有图片那就保留图片,注意[Content_Types].xml和document.xml.rels文档
     * 如果模板中没有图片 则不需要设置[Content_Types].xml和document.xml.rels
     * 由于word模板的个性化 所以 每次做模板都要重新覆盖原来的模板
     *
     *
     *
     * gaoxueyong
     */
    public class WordUtils {
    
        private final static String separator = File.separator;
        private final static String suffix_docx = "docx";
        private final static String suffix_doc = "doc";
    
    
        /*
         * @param dataMap               参数数据
         * @param docxTemplateFile      docx模主板名称
         * @param xmlDocument           docx中document.xml模板文件  用来存在word文档的主要数据信息
         * @param xmlDocumentXmlRels    docx中document.xml.rels 模板文件  用来存在word文档的主要数据配置 包括图片的指向
         * @param xmlContentTypes       docx中 [Content_Types].xml 模板文件 用来配置 docx文档中所插入图片的类型 如 png、jpeg、jpg等
         * @param xmlHeader             docx中 header1.xml 模板文件 用来配置docx文档的页眉文件
         * @param templatePath          模板存放路径 如 /templates/
         * @param outputFileTempPath    所生成的docx文件的临时路径文件夹 如果 temp/20180914051811/
         * @param outputFileName        所生成的docx文件名称  如  xxx.docx  或  xxx.doc
         * */
        public static void createDocx(Map dataMap, String docxTemplateFile, String xmlDocument, String xmlDocumentXmlRels,
                                      String xmlContentTypes, String xmlHeader,String xmlHeader2,String xmlHeader3, String templatePath,
                                      String outputFileTempPath, String outputFileName) throws Exception {
    
            URL basePath = WordUtils.class.getClassLoader().getResource("");
    //        System.out.println("basePath.getPath() ==> " + basePath.getPath());
            String realTemplatePath = basePath.getPath() + templatePath;
            //临时文件产出的路径
            String outputPath = basePath.getPath() + outputFileTempPath;
            List<String> delFileList = new ArrayList<>();
            try {
    
    
                //================================获取 document.xml.rels 输入流================================
                String xmlDocumentXmlRelsComment = FreeMarkUtils.getFreemarkerContent(dataMap, xmlDocumentXmlRels, templatePath);
                ByteArrayInputStream documentXmlRelsInput =
                        new ByteArrayInputStream(xmlDocumentXmlRelsComment.getBytes());
                //================================获取 document.xml.rels 输入流================================
    
                //================================获取 header1.xml 输入流================================
                ByteArrayInputStream headerInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlHeader, templatePath);
                ByteArrayInputStream header2Input = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlHeader2, templatePath);
                ByteArrayInputStream header3Input = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlHeader3, templatePath);
    
                //================================获取 header1.xml 输入流================================
    
                //================================获取 [Content_Types].xml 输入流================================
                ByteArrayInputStream contentTypesInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlContentTypes, templatePath);
                //================================获取 [Content_Types].xml 输入流================================
    
    
                //读取 document.xml.rels  文件 并获取rId 与 图片的关系 (如果没有图片 此文件不用编辑直接读取就行了)
                Document document = DocumentHelper.parseText(xmlDocumentXmlRelsComment);
                Element rootElt = document.getRootElement(); // 获取根节点
                Iterator iter = rootElt.elementIterator();// 获取根节点下的子节点head
                List<Map<String, String>> picList = (List<Map<String, String>>) dataMap.get("modelList");
    
                // 遍历Relationships节点
                while (iter.hasNext()) {
                    Element recordEle = (Element) iter.next();
                    String id = recordEle.attribute("Id").getData().toString();
                    String target = recordEle.attribute("Target").getData().toString();
                    if (target.indexOf("media") == 0) {
    //                        System.out.println("id>>>"+id+"   >>>"+target);
    //                        id>>>rId18   >>>media/pic1
    //
                        for (Map<String, String> picMap : picList) {
                            if (target.endsWith(picMap.get("name"))) {
                                picMap.put("rId", id);
                            }
                        }
                    }
                }
                dataMap.put("modelList", picList);//覆盖原来的picList;
    
                //================================获取 document.xml 输入流================================
                ByteArrayInputStream documentInput = FreeMarkUtils.getFreemarkerContentInputStream(dataMap, xmlDocument, templatePath);
                //================================获取 document.xml 输入流================================
    
    
    //            System.out.println("base_path_template+separator+docxTemplate===="+base_path_template+separator+docxTemplate);
                File docxFile = new File(realTemplatePath + separator + docxTemplateFile);
                if (!docxFile.exists()) {
                    docxFile.createNewFile();
                }
    
                ZipFile zipFile = new ZipFile(docxFile);
                Enumeration<? extends ZipEntry> zipEntrys = zipFile.entries();
                File tempPath = new File(outputPath);
                //如果输出目标文件夹不存在,则创建
                if (!tempPath.exists()) {
                    tempPath.mkdirs();
                }
                ZipOutputStream zipout = new ZipOutputStream(new FileOutputStream(outputPath + outputFileName));
    
    
                //------------------覆盖文档------------------
                int len = -1;
                byte[] buffer = new byte[1024];
                while (zipEntrys.hasMoreElements()) {
                    ZipEntry next = zipEntrys.nextElement();
                    InputStream is = zipFile.getInputStream(next);
                    if (next.toString().indexOf("media") < 0) {
                        // 把输入流的文件传到输出流中 如果是word/document.xml由我们输入
                        zipout.putNextEntry(new ZipEntry(next.getName()));
    //                    System.out.println("next.getName()>>>" + next.getName() + "  next.isDirectory()>>>" + next.isDirectory());
                        //写入图片配置类型
                        if (next.getName().equals("[Content_Types].xml")) {
                            if (contentTypesInput != null) {
                                while ((len = contentTypesInput.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                contentTypesInput.close();
                            }
    
                        } else if (next.getName().indexOf("document.xml.rels") > 0) {
                            //写入填充数据后的主数据配置信息
                            if (documentXmlRelsInput != null) {
                                while ((len = documentXmlRelsInput.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                documentXmlRelsInput.close();
                            }
                        } else if ("word/document.xml".equals(next.getName())) {
                            //写入填充数据后的主数据信息
                            if (documentInput != null) {
                                while ((len = documentInput.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                documentInput.close();
                            }
    
                        } else if ("word/header1.xml".equals(next.getName())) {
                            //写入填充数据后的页眉信息
                            if (headerInput != null) {
                                while ((len = headerInput.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                headerInput.close();
                            }
    
                        }else if ("word/header2.xml".equals(next.getName())){
                            //写入填充数据后的页眉信息
                            if (header2Input != null) {
                                while ((len = header2Input.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                header2Input.close();
                            }
                        }else if ("word/header3.xml".equals(next.getName())){
                            //写入填充数据后的页眉信息
                            if (header3Input != null) {
                                while ((len = header3Input.read(buffer)) != -1) {
                                    zipout.write(buffer, 0, len);
                                }
                                header3Input.close();
                            }
                        }
                        else {
                            while ((len = is.read(buffer)) != -1) {
                                zipout.write(buffer, 0, len);
                            }
                            is.close();
                        }
    
                    }
    
                }
                //------------------覆盖文档------------------
    
                //------------------写入新图片------------------
                len = -1;
                if (picList != null && !picList.isEmpty()) {
                    for (Map<String, String> pic : picList) {
                        ZipEntry next = new ZipEntry("word" + separator + "media" + separator + pic.get("name"));
                        zipout.putNextEntry(new ZipEntry(next.toString()));
                        InputStream in = new FileInputStream(pic.get("path"));
                        while ((len = in.read(buffer)) != -1) {
                            zipout.write(buffer, 0, len);
                        }
                        in.close();
                    }
                }
    
    
                //------------------写入新图片------------------
                zipout.close();
            } catch (Exception e) {
                e.printStackTrace();
                throw new Exception("生成docx文件失败!");
            }
    
        }
    
    
        /**
         * 删除文件
         *
         * @param listFiles
         */
        public static void delFiles(List<String> listFiles) {
            try {
                if (listFiles != null && !listFiles.isEmpty()) {
                    for (String file_temp_path : listFiles) {
                        File file_temp = new File(file_temp_path);
                        if (file_temp.exists()) {
                            file_temp.delete();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
    
        public static void main(String[] args) {
            URL basePath = WordUtils.class.getClassLoader().getResource("");
    //        System.out.println("basePath.getPath() ==> " + basePath.getPath());
            String picPath = basePath.getPath() + separator + "templates" + separator;
    
            Map<String, Object> dataMap = new HashMap<>();
    //        页眉
            dataMap.put("ymdhis", DateUtils.getCurrentTime_yyyyMMddHHmmss());
    //        水印
            dataMap.put("waterPic", "水印模板");
    
    //      图片类型
            List<String> picTypes = new ArrayList<>();
            picTypes.add("jpg");
            dataMap.put("mdlTypes", picTypes);
    //        文档标题
            dataMap.put("title", "文档的标题");
            dataMap.put("reportUnit", "文档的单位  ");
            dataMap.put("reportTypeDate", "文档的报告周期");
    
    //        模块内容列表
            List<Map<String, String>> picList = new ArrayList<>();
    
            Map<String, String> picMap = new HashMap<>();
            // 要按顺序
            picMap.put("path", picPath + "pic1.jpg");
            picMap.put("name", "pic1.jpg");
            picMap.put("modelTitle", "模块1标题");
            picMap.put("modelDataSource", "模块1来源:美团");
            picMap.put("modelShowContent", "模块1内容sasasaaaaaaaaaa");
            picList.add(picMap);
    
            picMap = new HashMap<>();
            picMap.put("path", picPath + "pic2.jpg");
            picMap.put("name", "pic2.jpg");
            picMap.put("modelTitle", "模块2标题");
            picMap.put("modelDataSource", "模块2来源:美团");
            picMap.put("modelShowContent", "模块2内容sasasaaaaaaaaaa");
            picList.add(picMap);
    
            picMap = new HashMap<>();
            picMap.put("path", picPath + "pic3.jpg");
            picMap.put("name", "pic3.jpg");
            picMap.put("modelTitle", "模块3标题");
            picMap.put("modelDataSource", "模块3来源:美团");
            picMap.put("modelShowContent", "模块3内容sasasaaaaaaaaaa");
            picList.add(picMap);
            dataMap.put("modelList", picList);
    
            String timeStr = DateUtils.getCurrentTime_yyyyMMddHHmmssSSS();
            String docxTemplateFile = "docxTemplates.docx";
            //带水印的模板
            String docxTemplatesWithWaterPic = "docxTemplatesWithWaterPic.docx";
            String xmlDocument = "document.xml";
            String xmlDocumentXmlRels = "document.xml.rels";
            String xmlContentTypes = "[Content_Types].xml";
            String xmlHeader = "header1.xml";//可以用来修改页眉的一些信息
            String xmlHeader2 = "header2.xml";//可以用来修改页眉的一些信息
            String xmlHeader3 = "header3.xml";//可以用来修改页眉的一些信息
            String templatePath = separator + "templates" + separator;
            String outputFileTempPath = "temp" + separator + timeStr + separator;
            String outputFileName = timeStr + "."+suffix_docx;
    //        String outputFileName = timeStr + "."+suffix_doc;
    
    
            /*
            * @param dataMap               参数数据
            * @param docxTemplateFile      docx模主板名称
            * @param xmlDocument           docx中document.xml模板文件  用来存在word文档的主要数据信息
            * @param xmlDocumentXmlRels    docx中document.xml.rels 模板文件  用来存在word文档的主要数据配置 包括图片的指向
            * @param xmlContentTypes       docx中 [Content_Types].xml 模板文件 用来配置 docx文档中所插入图片的类型 如 png、jpeg、jpg等
            * @param xmlHeader             docx中 header1.xml 模板文件 用来配置docx文档的页眉文件
            * @param templatePath          模板存放路径 如 /templates/
            * @param outputFileTempPath    所生成的docx文件的临时路径文件夹 如果 temp/20180914051811/
            * @param outputFileName        所生成的docx文件名称  如  xxx.docx 或  xxx.doc
            * */
            try {
                //不带水印
    //            createDocx(dataMap, docxTemplateFile, xmlDocument, xmlDocumentXmlRels, xmlContentTypes,
    //                    xmlHeader, templatePath, outputFileTempPath, outputFileName);
                //水印
                createDocx(dataMap, docxTemplatesWithWaterPic, xmlDocument, xmlDocumentXmlRels, xmlContentTypes,
                        xmlHeader,xmlHeader2,xmlHeader3, templatePath, outputFileTempPath, outputFileName);
    
    
    //            String xmlDocumentXmlRelsComment = FreeMarkUtils.getFreemarkerContent(dataMap,xmlDocumentXmlRels,separator + "templates" );
    //            System.out.println(xmlDocumentXmlRelsComment);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
    
    }
    

     目录和我一样,本地运行就可以了

    具体代码

    https://gitee.com/zc0709/JavaUtilsProject

    主要工具类

    https://gitee.com/zc0709/JavaUtilsProject/blob/master/src/main/java/com/sl/utils/office/word/WordUtils.java

     

    展开全文
  • 前端水印生成方案

    千次阅读 2018-07-27 10:43:23
     网页水印生成解决方案  通过canvas生成水印  Canvas兼容性  这里我们用canvas来生成base64图片,通过CanIUse网站查询兼容性,如果在移动端以及一些管理系统使用,兼容性问题可以完全忽略。  ...

      原文出处:QQ音乐前端博客    伯乐在线


            网页水印生成解决方案

      通过canvas生成水印

      Canvas兼容性

      这里我们用canvas来生成base64图片,通过CanIUse网站查询兼容性,如果在移动端以及一些管理系统使用,兼容性问题可以完全忽略。

      HTMLCanvasElement.toDataURL 方法返回一个包含图片展示的 data URI 。可以使用 type 参数其类型,默认为 PNG 格式。图片的分辨率为96dpi。

      如果画布的高度或宽度是0,那么会返回字符串“data:,”。

      如果传入的类型非“image/png”,但是返回的值以“data:image/png”开头,那么该传入的类型是不支持的。

      Chrome支持“image/webp”类型。具体参考HTMLCanvasElement.toDataURL

      具体代码实现如下:

     (function () {
          // canvas 实现 watermark
          function __canvasWM({
            // 使用 ES6 的函数默认值方式设置参数的默认取值
            // 具体参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
            container = document.body,
            width = '200px',
            height = '150px',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px microsoft yahei",
            fillStyle = 'rgba(184, 184, 184, 0.8)',
            content = '请勿外传',
            rotate = '30',
            zIndex = 1000
          } = {}) {
            var args = arguments[0];
            var canvas = document.createElement('canvas');
     
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);
            var ctx = canvas.getContext("2d");
     
            ctx.textAlign = textAlign;
            ctx.textBaseline = textBaseline;
            ctx.font = font;
            ctx.fillStyle = fillStyle;
            ctx.rotate(Math.PI / 180 * rotate);
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
     
            var base64Url = canvas.toDataURL();
            const watermarkDiv = document.createElement("div");
            watermarkDiv.setAttribute('style', `
              position:absolute;
              top:0;
              left:0;
              width:100%;
              height:100%;
              z-index:${zIndex};
              pointer-events:none;
              background-repeat:repeat;
              background-image:url('${base64Url}')`);
     
            container.style.position = 'relative';
            container.insertBefore(watermarkDiv, container.firstChild);
     
            
          });
     
          window.__canvasWM = __canvasWM;
        })();
     
        // 调用
        __canvasWM({
          content: 'QQMusicFE'
        })
    Canvas实现网页水印效果标题

    为了使这个方法更通用,兼容不同的引用方式,我们还可以加上这段代码:

        // 为了兼容不同的环境
          if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __canvasWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __canvasWM;
            });
          } else {
            window.__canvasWM = __canvasWM;
          }

     

      这样似乎能满足我们的需求了,但是还有一个问题,稍微懂一点浏览器的使用或者网页知识的用户,可以用浏览器的开发者工具来动态更改DOM的属性或者结构就可以去掉了。这个时候有两个解决办法:

      1.监测水印div的变化,记录刚生成的div的innerHTML,每隔几秒就取一次新的值,一旦发生变化,则重新生成水印。但是这种方式可能影响性能;

      2.使用MutationObserver

      MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时作出适当反应的能力。

      MutationObserver兼容性

     

     

      通过兼容性表可以看出高级浏览器以及移动浏览器支持非常不错。

      Mutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。

      使用MutationObserver构造函数,新建一个观察器实例,实例的有一个回调函数,该回调函数接受两个参数,第一个是变动数组,第二个是观察器实例。MutationObserver 的实例的observe方法用来启动监听,它接受两个参数。

      第一个参数:所要观察的 DOM 节点,第二个参数:一个配置对象,指定所要观察的特定变动,有以下几种:

            MutationObserver只能监测到诸如属性改变、增删子结点等,对于自己本身被删除,是没有办法的可以通过监测父结点来达到要求。因此最终改造之后代码为:

    (function () {
          // canvas 实现 watermark
          function __canvasWM({
            // 使用 ES6 的函数默认值方式设置参数的默认取值
            // 具体参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
            container = document.body,
            width = '300px',
            height = '200px',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px Microsoft Yahei",
            fillStyle = 'rgba(184, 184, 184, 0.6)',
            content = '请勿外传',
            rotate = '30',
            zIndex = 1000
          } = {}) {
            const args = arguments[0];
            const canvas = document.createElement('canvas');
     
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);
            const ctx = canvas.getContext("2d");
     
            ctx.textAlign = textAlign;
            ctx.textBaseline = textBaseline;
            ctx.font = font;
            ctx.fillStyle = fillStyle;
            ctx.rotate(Math.PI / 180 * rotate);
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
     
            const base64Url = canvas.toDataURL();
            const __wm = document.querySelector('.__wm');
     
            const watermarkDiv = __wm || document.createElement("div");
            const styleStr = `
              position:absolute;
              top:0;
              left:0;
              width:100%;
              height:100%;
              z-index:${zIndex};
              pointer-events:none;
              background-repeat:repeat;
              background-image:url('${base64Url}')`;
     
            watermarkDiv.setAttribute('style', styleStr);
            watermarkDiv.classList.add('__wm');
     
            if (!__wm) {
              container.style.position = 'relative';
              container.insertBefore(watermarkDiv, container.firstChild);
            }
            
            const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
            if (MutationObserver) {
              let mo = new MutationObserver(function () {
                const __wm = document.querySelector('.__wm');
                // 只在__wm元素变动才重新调用 __canvasWM
                if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
                  // 避免一直触发
                  mo.disconnect();
                  mo = null;
                __canvasWM(JSON.parse(JSON.stringify(args)));
                }
              });
     
              mo.observe(container, {
                attributes: true,
                subtree: true,
                childList: true
              })
            }
     
          }
     
          if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __canvasWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __canvasWM;
            });
          } else {
            window.__canvasWM = __canvasWM;
          }
        })();
     
        // 调用
        __canvasWM({
          content: 'QQMusicFE'
        });

      通过SVG生成水印

      SVG:可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。 SVG由W3C制定,是一个开放标准。 — 维基百科

      SVG浏览器兼容性

            相比Canvas,SVG有更好的浏览器兼容性,使用SVG生成水印的方式与Canvas的方式类似,只是base64Url的生成方式换成了SVG。具体如下:

     

    (function () {
          // svg 实现 watermark
          function __svgWM({
            container = document.body,
            content = '请勿外传',
            width = '300px',
            height = '200px',
            opacity = '0.2',
            fontSize = '20px',
            zIndex = 1000
          } = {}) {
            const args = arguments[0];
            const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
      <text x="50%" y="50%" dy="12px"
        text-anchor="middle"
        stroke="#000000"
        stroke-width="1"
        stroke-opacity="${opacity}"
        fill="none"
        transform="rotate(-45, 120 120)"
        style="font-size: ${fontSize};">
        ${content}
      </text>
    </svg>`;
            const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
            const __wm = document.querySelector('.__wm');
     
            const watermarkDiv = __wm || document.createElement("div");
         // ...
         // 与 canvas 的一致
         // ...
        })();
     
        __svgWM({
          content: 'QQMusicFE'
        })

     

      通过NodeJS生成水印

      身为现代前端开发者,Node.JS也是需要掌握的。我们同样可以通过NodeJS来生成网页水印(出于性能考虑更好的方式是利用用户客户端来生成)。前端发一个请求,参数带上水印内容,后台返回图片内容。

      具体实现(Koa2环境):

      1.安装gm以及相关环境,详情看gm文档

      2.ctx.type = 'image/png';设置响应为图片类型

      3.生成图片过程是异步的,所以需要包装一层Promise,这样才能为通过 async/await 方式为 ctx.body 赋值

     

    const fs = require('fs')
    const gm = require('gm');
    const imageMagick = gm.subClass({
      imageMagick: true
    });
     
     
    const router = require('koa-router')();
     
    router.get('/wm', async (ctx, next) => {
      const {
        text
      } = ctx.query;
     
      ctx.type = 'image/png';
      ctx.status = 200;
      ctx.body = await ((() => {
        return new Promise((resolve, reject) => {
          imageMagick(200, 100, "rgba(255,255,255,0)")
            .fontSize(40)
            .drawText(10, 50, text)
            .write(require('path').join(__dirname, `./${text}.png`), function (err) {
              if (err) {
                reject(err);
              } else {
                resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
              }
            });
        })
      })());
    });

     

      如果只是简单的水印展示,建议在浏览器生成,性能更好

      图片水印生成解决方案

      除了给网页加上水印之外,有时候我们需要给图片也加上水印,这样在用户保存图片后,带上了水印来源信息,既可以保护版权,水印的其他信息也可以防止泄密。

      通过canvas给图片加水印

      实现如下:

     

    (function() {
          function __picWM({
            url = '',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px Microsoft Yahei",
            fillStyle = 'rgba(184, 184, 184, 0.8)',
            content = '请勿外传',
            cb = null,
            textX = 100,
            textY = 30
          } = {}) {
            const img = new Image();
            img.src = url;
            img.crossOrigin = 'anonymous';
            img.onload = function() {
              const canvas = document.createElement('canvas');
              canvas.width = img.width;
              canvas.height = img.height;
              const ctx = canvas.getContext('2d');
     
              ctx.drawImage(img, 0, 0);
              ctx.textAlign = textAlign;
              ctx.textBaseline = textBaseline;
              ctx.font = font;
              ctx.fillStyle = fillStyle;
              ctx.fillText(content, img.width - textX, img.height - textY);
     
              const base64Url = canvas.toDataURL();
              cb && cb(base64Url);
            }
          }
     
            if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __picWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __picWM;
            });
          } else {
            window.__picWM = __picWM;
          }
          
        })();
     
        // 调用
        __picWM({
            url: 'http://localhost:3000/imgs/google.png',
            content: 'QQMusicFE',
            cb: (base64Url) => {
              document.querySelector('img').src = base64Url
            },
          });

     效果如下:

    Canvas给图片生成水印

     

     

      通过NodeJS批量为图片加水印

      我们同样可以通过gm这个库来给图片加上水印

     

    function picWM(path, text) {
      imageMagick(path)
        .drawText(10, 50, text)
        .write(require('path').join(__dirname, `./${text}.png`), function (err) {
          if (err) {
            console.log(err);
          }
        });
    }

     

      如果需要批处理图片,只需要遍历相关文件即可。

      如果只是简单的水印展示,建议在浏览器生成,性能更好

      拓展

      隐水印

      前段时间阿里凭截图查到了月饼事件的泄密者,其实就是用了隐水印。这其实很大程度不是前端的范畴了,但是我们也应该了解。AlloyTeam团队写过一篇 不能说的秘密——前端也能玩的图片隐写术 ,通过Canvas给图片加上了“隐水印”,针对用户保存的图片,是可以轻松还原里面隐含的内容,但是对于截图或者处理过的照片却无能为力,不过对于一些机密图片文件展示,是可以偷偷用上该技术的。

      使用加密后的水印内容

      前端生成的水印也可以,别人也可以用同样的方式生成,可能会有“嫁祸于人”(可能这是多虑的),我们还是要有更安全的解决方法。水印内容可以包含多种编码后的信息,包括用户名、用户ID、时间等。比如我们只是想保存用户唯一的用户ID,需要把用户ID传入下面的md5方法,就可以生成唯一标识。编码后的信息是不可逆的,但可以通过全局遍历所有用户的方式进行追溯。这样就可以防止水印造假也可以追溯真正水印的信息。

    // MD5加密库 utility
    const utils = require('utility')
     
    // 加盐MD5
    exports.md5 =  function (content) {
      const salt = 'microzz_asd!@#IdSDAS~~';
      return utils.md5(utils.md5(content + salt));
    }

     

    展开全文
  • 前言在使用 PHP 在 PDF 文件上打水印的过程中,我尝试了如下几个工具:因为想满足对中文水印的支持,并对 PDF 文档进行加密,最终使用 FPDI + TCPDF 的方案完成功能。正文打水印类...

    前言

    在使用 PHP 在 PDF 文件上打水印的过程中,我尝试了如下几个工具:

    因为想满足对中文水印的支持,并对 PDF 文档进行加密,最终使用 FPDI + TCPDF 的方案完成功能。

    正文

    打水印类1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59

    60

    61

    62

    63

    64

    65

    66

    67

    68

    69

    70

    71

    72

    73

    74

    75

    76

    77

    78

    79

    80

    81

    82

    83

    84

    85

    86

    87

    88

    89

    90

    91

    92

    93

    94

    95

    96

    97

    98

    99

    100

    101

    102

    103

    104

    105

    106require_once ('./fpdi/TCPDF/tcpdf.php');

    require_once('./fpdi/fpdi.php');

    class extends FPDI{

    protected $angle = 0;

    protected $extgstates = array();

    public function __construct(){

    parent::__construct();

    }

    /**

    * 旋转角度

    * @param $angle

    * @param int $x

    * @param int $y

    */

    protected function _rotate($angle, $x=-1, $y=-1){

    if ($x == -1)

    $x == $this->x;

    if ($y == -1)

    $y == $this->y;

    if ($this->angle != 0)

    $this->_out('Q');

    $this->angle = $angle;

    if ($angle != 0) {

    $angle *= M_PI / 180;

    $c = cos($angle);

    $s = sin($angle);

    $cx = $x * $this->k;

    $cy = ($this->h - $y) * $this->k;

    $this->_out(sprintf('q %.5F %.5F %.5F %.5F %.2F %.2F cm 1 0 0 1 %.2F %.2F cm',$c,$s,-$s,$c,$cx,$cy,-$cx,-$cy));

    }

    }

    /**

    * 旋转文字角度

    * @param $x

    * @param $y

    * @param $txt

    * @param $angle

    */

    public function RotatedText($x, $y, $txt, $angle)

    {

    $this->_rotate($angle,$x,$y);

    $this->Text($x,$y,$txt);

    $this->_rotate(0);

    }

    /**

    * 旋转图片角度

    * @param $file

    * @param $x

    * @param $y

    * @param $w

    * @param $h

    * @param $angle

    */

    public function RotatedImage($file, $x, $y, $w, $h, $angle)

    {

    //Image rotated around its upper-left corner

    $this->_rotate($angle,$x,$y);

    $this->Image($file,$x,$y,$w,$h);

    $this->_rotate(0);

    }

    /**

    * 设置字体颜色

    * @param $hexStr

    */

    public function SetTextColorByHexStr($hexStr){

    $rgbArray = $this->_hex2rgb($hexStr);

    if (is_array($rgbArray)) {

    $this->SetTextColor($rgbArray['red'], $rgbArray['green'], $rgbArray['blue']);

    }

    }

    /**

    * 16进制颜色代码转RGB值

    * @param string $hexStr (hexadecimal color value)

    * @param boolean $returnAsString (if set true, returns the value separated by the separator character. Otherwise returns associative array)

    * @param string $seperator (to separate RGB values. Applicable only if second parameter is true.)

    * @return array or string (depending on second parameter. Returns False if invalid hex color value)

    */

    protected function _hex2rgb($hexStr, $returnAsString = false, $seperator = ','){

    $hexStr = preg_replace("/[^0-9A-Fa-f]/", '', $hexStr); // Gets a proper hex string

    $rgbArray = array();

    if (strlen($hexStr) == 6) { //If a proper hex code, convert using bitwise operation. No overhead... faster

    $colorVal = hexdec($hexStr);

    $rgbArray['red'] = 0xFF & ($colorVal >> 0x10);

    $rgbArray['green'] = 0xFF & ($colorVal >> 0x8);

    $rgbArray['blue'] = 0xFF & $colorVal;

    } elseif (strlen($hexStr) == 3) { //if shorthand notation, need some string manipulations

    $rgbArray['red'] = hexdec(str_repeat(substr($hexStr, 0, 1), 2));

    $rgbArray['green'] = hexdec(str_repeat(substr($hexStr, 1, 1), 2));

    $rgbArray['blue'] = hexdec(str_repeat(substr($hexStr, 2, 1), 2));

    } else {

    return false; //Invalid hex color code

    }

    return $returnAsString ? implode($seperator, $rgbArray) : $rgbArray; // returns the rgb string or the associative array

    }

    }

    继承顺序

    程序依赖 FPDI 读取已存在的 PDF 文档,依赖 TCPDF 处理 PDF 文档。程序继承顺序如下:Pdf_Watermark extends FPDI

    FPDI extends FPDF_TPL

    FPDF_TPL extends fpdi_bridge

    fpdi_bridge 根据引入的类是 TCPDF 还是 FPDF 进行选择继承

    因此,在使用 FPDI 读取文件后,可以选择使用 TCPDF 或 FPDF 对文档进行处理。

    调用方法1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    19

    20

    21

    22

    23

    24

    25

    26

    27

    28

    29

    30

    31

    32

    33

    34

    35

    36

    37

    38

    39

    40

    41

    42

    43

    44

    45

    46

    47

    48

    49

    50

    51

    52

    53

    54

    55

    56

    57

    58

    59require_once('PdfWatermark.php');

    function pdf_set_watermark_text($file, $newFile, $text, $text_color, $font_style, $font_size, $alpha, $margin_left, $margin_top, $angle){

    try {

    $pdf = new Pdf_Watermark();

    $pageCount = $pdf->setSourceFile($file);

    // 循环读取分页

    for ($i = 1; $i <= $pageCount; $i++) {

    // 加密文档

    $pdf->SetProtection(array('print', 'modify', 'copy', 'annot-forms'));

    $pdf->addPage();

    $tplidx = $pdf->importPage($i);

    // 获取 pdf 长宽

    $whArray = $pdf->getTemplateSize($tplidx);

    $pdfWidth = $whArray['w'];

    $pdfHeight = $whArray['h'];

    // 如果将最后一个参数 $adjustPageSize 设置为TRUE,则当前的pdf页面大小会根据模板自适应。

    $pdf->useTemplate($tplidx, null, null, 0, 0, TRUE);

    for ($j = 0; $j < 9; $j++) {

    // 设置字体

    $pdf->SetFont('helvetica', 'B', $font_size);

    $pdf->SetAlpha($alpha);

    // 设置文字颜色

    $pdf->SetTextColorByHexStr($text_color);

    // 设置文字位置

    $pdf->RotatedText($margin_left+$j*30, $margin_top+$j*30, $text, $angle);

    $pdf->SetAlpha(1);

    }

    }

    // pdf文档在浏览器中输出显示

    $pdf->Output($newFile, 'I');

    } catch (Exception $ex) {

    return false;

    }

    return true;

    }

    $pdfFile = 'test.pdf';

    $newFile = 'word.pdf';

    $watermark_text = 'abc';

    $text_color = '#CFCFCF';

    $font_style = 'B';

    $font_size = 25;

    $alpha = 0.5;

    $margin_left = 10;

    $margin_top = 10;

    $angle = 45;

    pdf_set_watermark_text($pdfFile, $newFile, $watermark_text, $text_color, $font_style, $font_size, $alpha, $margin_left, $margin_top, $angle);

    FPDI的限制

    FPDI 的免费版本只能读取版本 <1.5 的 pdf 文档,为了兼容所有的 pdf 文档需要做一些处理。

    如果不在乎 money,可以直接使用付费工具 FPDI PDF-Parser 来解决这个问题。

    我在这里选择了使用 PDFtk 命令行工具,通过在 PHP 中执行如下代码对 pdf 文档进行处理:1exec("pdftk input_file.pdf output output_file.pdf");

    这样,处理过后的 output_file.pdf 就能使用上诉方法添加水印了。

    关于中文显示

    新版本的 TCPDF 已支持中文,在 TCPDF/fonts 目录下存在 cid0cs.php 与 stsonstdlight.php 均能有效支持中文显示。

    可以使用 setFont 方法进行调用:1

    2// 设置字体

    $pdf->SetFont('cid0cs', '', $font_size);1

    2// 设置字体

    $pdf->SetFont('stsongstdlight', '', $font_size);

    总结

    在研究给 PDF 文档打水印的过程中一波三折。

    因为一开始选用的 FPDF 不支持 utf-8 编码,转战 TFPDF 开始捣鼓中文字体包,最终因为需要加密文档选择了更加方便的 TCPDF。

    总的来说,开发前还是要仔细阅读参考文档啊,泪目。

    参考资料

    展开全文
  • 前端水印生成方案 前段时间做某系统审核后台,出现了审核人员截图把内容外部扭曲的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽略。于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定...


    前端水印生成方案


        前段时间做某系统审核后台,出现了审核人员截图把内容外部扭曲的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽略。于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定的提示作用。

    网页水印生成解决方案

    通过canvas生成水印
    画布兼容性


    这里我们用canvas来生成base64图片,通过CanIUse网站查询兼容性,如果在移动端以及一些管理系统使用,兼容性问题可以完全忽略。

    HTMLCanvasElement.toDataURL 方法返回一个包含图片展示的数据URI。可以使用类型参数其类型,为PNG格式。图片的分辨率为96dpi。

    如果画布的高度或宽度为0,那么会返回字符串“ data :,”。
    如果初始化的类型非“ image / png”,但返回的值以“ data:image / png”开头,那么该必然的类型是不支持的。
    铬支持“图像/ WEBP”类型。具体参考HTMLCanvasElement.toDataURL

    具体代码实现如下:

     (function () {
          // canvas 实现 watermark
          function __canvasWM({
            // 使用 ES6 的函数默认值方式设置参数的默认取值
            // 具体参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
            container = document.body,
            width = '200px',
            height = '150px',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px microsoft yahei",
            fillStyle = 'rgba(184, 184, 184, 0.8)',
            content = '请勿外传',
            rotate = '30',
            zIndex = 1000
          } = {}) {
            var args = arguments[0];
            var canvas = document.createElement('canvas');
    
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);
            var ctx = canvas.getContext("2d");
    
            ctx.textAlign = textAlign;
            ctx.textBaseline = textBaseline;
            ctx.font = font;
            ctx.fillStyle = fillStyle;
            ctx.rotate(Math.PI / 180 * rotate);
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
    
            var base64Url = canvas.toDataURL();
            const watermarkDiv = document.createElement("div");
            watermarkDiv.setAttribute('style', `
              position:absolute;
              top:0;
              left:0;
              width:100%;
              height:100%;
              z-index:${zIndex};
              pointer-events:none;
              background-repeat:repeat;
              background-image:url('${base64Url}')`);
    
            container.style.position = 'relative';
            container.insertBefore(watermarkDiv, container.firstChild);
    
            
          });
    
          window.__canvasWM = __canvasWM;
        })();
    
        // 调用
        __canvasWM({
          content: 'QQMusicFE'
        })


    效果如下:


    ![画布实现网页水印效果]

    图片

    为了使这个方法更通用,兼容不同的引用方式,我们还可以加上这段代码:

       

      // 为了兼容不同的环境
          if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __canvasWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __canvasWM;
            });
          } else {
            window.__canvasWM = __canvasWM;
          }


    这样看起来能满足我们的需求了,但是还有一个问题,稍微懂一点浏览器的使用或网页知识的用户,可以用浏览器的开发者工具来动态更改DOM的属性或者结构就可以去掉了。当时有两个解决办法:

    监测水印div的变化,记录刚生成的div的innerHTML,每隔几秒就取一次新的值,一旦发生变化,则重新生成水印。
    使用MutationObserver
    MutationObserver给开发者们提供了一种能在某个范围内的DOM树发生变化时做出适当反应的能力。

    MutationObserver兼容性

    图片

    通过兼容性表可以看出高级浏览器以及移动浏览器支持非常不错。
    突变观察员API用来监视DOM变动.DOM的任何变动,比如节点的增减,属性的变动,文本内容的变动,这个API都可以得到通知。
    使用MutationObserver的实例的观察函数方法用来启动监听,它接受两个参数。
    第一个参数:所要观察的DOM节点,第二个参数:一个配置对象,指定所要观察的特定变动,有以下几种:

    属性描述
    childList如果需要观察目标目标的子例程(添加了某个子例程,或者可移除了某个子例程),则设置为true。
    属性如果需要观察目标目标的属性变量(添加或删除了某个属性,以及某个属性的属性值发生了变化),则设置为true。
    characterData如果目标目标对象为字符数据例程(一种抽象接口,具体可以为文本索引,注释索引,以及处理指令例程)时,也要观察该例程的文本内容是否发生变化,则设置为true。
    子树除了目标例程,如果还需要观察目标例程的所有后代例程,则设置为true。
    attributeOldValue在属性属性已经被设置为true,则需要设置为true。
    characterDataOldValue在characterData属性已经被设置为true的情况下,如果需要将发生变化的characterData之前的文本内容记录下来(记录到下面的MutationRecord对象的oldValue属性中),则设置为true。
    attributeFilter一个属性名数组(不需要指定命名空间),只有该数组中包含的属性名发生变化时才会被观察到,其他名称的属性发生变化后会被忽略。


    MutationObserver只能监测到某种属性改变,增减子结点等,对于自己本身被删除,是没有办法的可以通过监测父结点来达到要求。因此最终改造之后的代码为: 

     (function () {
          // canvas 实现 watermark
          function __canvasWM({
            // 使用 ES6 的函数默认值方式设置参数的默认取值
            // 具体参见 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Default_parameters
            container = document.body,
            width = '300px',
            height = '200px',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px Microsoft Yahei",
            fillStyle = 'rgba(184, 184, 184, 0.6)',
            content = '请勿外传',
            rotate = '30',
            zIndex = 1000
          } = {}) {
            const args = arguments[0];
            const canvas = document.createElement('canvas');
    
            canvas.setAttribute('width', width);
            canvas.setAttribute('height', height);
            const ctx = canvas.getContext("2d");
    
            ctx.textAlign = textAlign;
            ctx.textBaseline = textBaseline;
            ctx.font = font;
            ctx.fillStyle = fillStyle;
            ctx.rotate(Math.PI / 180 * rotate);
            ctx.fillText(content, parseFloat(width) / 2, parseFloat(height) / 2);
    
            const base64Url = canvas.toDataURL();
            const __wm = document.querySelector('.__wm');
    
            const watermarkDiv = __wm || document.createElement("div");
            const styleStr = `
              position:absolute;
              top:0;
              left:0;
              width:100%;
              height:100%;
              z-index:${zIndex};
              pointer-events:none;
              background-repeat:repeat;
              background-image:url('${base64Url}')`;
    
            watermarkDiv.setAttribute('style', styleStr);
            watermarkDiv.classList.add('__wm');
    
            if (!__wm) {
              container.style.position = 'relative';
              container.insertBefore(watermarkDiv, container.firstChild);
            }
            
            const MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
            if (MutationObserver) {
              let mo = new MutationObserver(function () {
                const __wm = document.querySelector('.__wm');
                // 只在__wm元素变动才重新调用 __canvasWM
                if ((__wm && __wm.getAttribute('style') !== styleStr) || !__wm) {
                  // 避免一直触发
                  mo.disconnect();
                  mo = null;
                __canvasWM(JSON.parse(JSON.stringify(args)));
                }
              });
    
              mo.observe(container, {
                attributes: true,
                subtree: true,
                childList: true
              })
            }
    
          }
    
          if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __canvasWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __canvasWM;
            });
          } else {
            window.__canvasWM = __canvasWM;
          }
        })();
    
        // 调用
        __canvasWM({
          content: 'QQMusicFE'
        });


    通过SVG生成水印


    SVG:可缩放矢量图形(英语:Scalable Vector Graphics,SVG)是一种基于可扩展标记语言(XML),用于描述二维矢量图形的图形格式。维基百科

    SVG浏览器兼容性

    图片

    概述Canvas,SVG有更好的浏览器兼容性,使用SVG生成水印的方式与Canvas的方式类似,只是base64Url的生成方式换成SVG。具体如下:     

    (function () {
          // svg 实现 watermark
          function __svgWM({
            container = document.body,
            content = '请勿外传',
            width = '300px',
            height = '200px',
            opacity = '0.2',
            fontSize = '20px',
            zIndex = 1000
          } = {}) {
            const args = arguments[0];
            const svgStr = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${width}">
      <text x="50%" y="50%" dy="12px"
        text-anchor="middle"
        stroke="#000000"
        stroke-width="1"
        stroke-opacity="${opacity}"
        fill="none"
        transform="rotate(-45, 120 120)"
        style="font-size: ${fontSize};">
        ${content}
      </text>
    </svg>`;
            const base64Url = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(svgStr)))}`;
            const __wm = document.querySelector('.__wm');
    
            const watermarkDiv = __wm || document.createElement("div");
         // ...
         // 与 canvas 的一致
         // ...
        })();
    
        __svgWM({
          content: 'QQMusicFE'
        })


    通过NodeJS生成水印


    我们同样可以通过NodeJS来生成网页水印(出于性能考虑更好的方式是利用用户客户端来生成)。前端发一个请求,参数带上水印内容,后台返回图片内容。
    具体实现(Koa2环境):安装gm以及相关环境,详情看gm文档
    ctx.type = 'image/png';设置响应为图片类型
    生成图片过程是异步的,所以需要包装一层Promise,这样才能为通过async / await方式为ctx.body赋值

    const fs = require('fs')
    const gm = require('gm');
    const imageMagick = gm.subClass({
      imageMagick: true
    });
    
    
    const router = require('koa-router')();
    
    router.get('/wm', async (ctx, next) => {
      const {
        text
      } = ctx.query;
    
      ctx.type = 'image/png';
      ctx.status = 200;
      ctx.body = await ((() => {
        return new Promise((resolve, reject) => {
          imageMagick(200, 100, "rgba(255,255,255,0)")
            .fontSize(40)
            .drawText(10, 50, text)
            .write(require('path').join(__dirname, `./${text}.png`), function (err) {
              if (err) {
                reject(err);
              } else {
                resolve(fs.readFileSync(require('path').join(__dirname, `./${text}.png`)))
              }
            });
        })
      })());
    });


    如果只是简单的水印展示,建议在浏览器生成,性能更好

    图片水印生成解决方案


    除了给网页加上水印之外,有时候我们需要给图片也加上水印,这样在用户保存图片后,带上了水印来源信息,既可以保护版权,水印的其他信息也可以防止泄密。

    通过画布给图片加水印
    实现如下:   

     (function() {
          function __picWM({
            url = '',
            textAlign = 'center',
            textBaseline = 'middle',
            font = "20px Microsoft Yahei",
            fillStyle = 'rgba(184, 184, 184, 0.8)',
            content = '请勿外传',
            cb = null,
            textX = 100,
            textY = 30
          } = {}) {
            const img = new Image();
            img.src = url;
            img.crossOrigin = 'anonymous';
            img.onload = function() {
              const canvas = document.createElement('canvas');
              canvas.width = img.width;
              canvas.height = img.height;
              const ctx = canvas.getContext('2d');
    
              ctx.drawImage(img, 0, 0);
              ctx.textAlign = textAlign;
              ctx.textBaseline = textBaseline;
              ctx.font = font;
              ctx.fillStyle = fillStyle;
              ctx.fillText(content, img.width - textX, img.height - textY);
    
              const base64Url = canvas.toDataURL();
              cb && cb(base64Url);
            }
          }
    
            if (typeof module != 'undefined' && module.exports) {  //CMD
            module.exports = __picWM;
          } else if (typeof define == 'function' && define.amd) { // AMD
            define(function () {
              return __picWM;
            });
          } else {
            window.__picWM = __picWM;
          }
          
        })();
    
        // 调用
        __picWM({
            url: 'http://localhost:3000/imgs/google.png',
            content: 'QQMusicFE',
            cb: (base64Url) => {
              document.querySelector('img').src = base64Url
            },
          });


    效果如下:

    帆布给图片生成水印

    图片

    通过NodeJS批量为图片加水印
    我们同样可以通过gm这个库来给图片加上水印

    function picWM(path, text) {
      imageMagick(path)
        .drawText(10, 50, text)
        .write(require('path').join(__dirname, `./${text}.png`), function (err) {
          if (err) {
            console.log(err);
          }
        });
    }


    如果需要批处理图片,只需要遍历相关文件即可。

    如果只是简单的水印展示,建议在浏览器生成,性能更好

    拓展   隐水印


    前段时间阿里凭截图查到了月饼事件的泄密者,其实就是用了隐水印这其实很大程度不是前端的范畴了,但是我们也应该了解.AlloyTeam团队写过一篇。不能说的秘密-前端也能玩的图片隐写术,通过画布给图片加上了“隐水印”,针对用户保存的图片,是可以轻松还原里面隐含的内容,但是对于截图或处理过的照片却无能为力,不过对于一些机密图片文件展示,是可以偷偷用上该技术的。

    使用加密后的水印内容
    前端生成的水印也可以,别人也可以用同样的方式生成,可能会有“嫁祸于人”(可能这是多虑的),我们还是要有更安全的解决方法。水印内容可以包含多种编码后的信息,包括用户名,用户ID,时间等。例如我们只是想保存用户唯一的用户ID,需要把用户ID放在下面的md5方法,就可以生成唯一标识。 ,但可以通过双向遍历所有用户的方式进行后续。这样就可以防止水印造假也可以进入真正水印的信息。

    // MD5加密库 utility
    const utils = require('utility')
    
    // 加盐MD5
    exports.md5 =  function (content) {
      const salt = 'microzz_asd!@#IdSDAS~~';
      return utils.md5(utils.md5(content + salt));
    }


    总结


    安全问题不能大意,对于一些比较敏感的内容,我们可以通过组合使用上述的水印方案,这样才能最大程度地给浏览者警示的作用,减少泄密的情况,甚至泄密了,也有可能追踪到泄密者。

    展开全文
  • 一般可设置文字水印或者加载图片作为水印,一般可设置...以下内容将分享通过Java编程给Word文档添加水印效果的方法,即文本水印图片水印使用工具:Free Spire.Doc for Java(免费版)Jar导入:方法1:通过官网下载jar...
  • Java实现在线word文档添加铺满效果文字水印,文字可换行使用插件Aspose.words for java生成水印工具类调用生成水印方法 需求:为在线预览的word文档添加自定义水印,还要铺满效果,文字太多还要能换行 使用插件...
  • java操作word生成水印

    2018-05-18 16:35:22
     为了保护版权或辨别文件的真伪,有时需要在生成的Word文件中动态添加水印,PageOffice组件的WaterMark类就封装了给在线编辑的Word文件添加水印这一功能,调用接口非常简单。 WaterMark类所属命名空间  ...
  • 以下内容将分享通过Java编程给Word文档添加水印效果的方法,即文本水印图片水印使用工具:Free Spire.Doc for Java (免费版)Jar导入:方法1:通过官网下载jar文件包。下载后,解压文件,并将lib文件夹下的Spire.Doc....
  • java实现word文档转pdf,并添加水印发布时间:2018-08-01 17:21,浏览次数:1115, 标签:javawordpdf前段时间,项目需要自动生成word文档,用WordFreeMarker生成word文档后,又要求生成文档能在浏览器浏览,...
  • 网上的工具大多都要收费,或者上传到网页(这里有资源泄露的风险),做了个极其简单的加水印工具,选择文件之后,输入要加入的水印内容即可生成。透明度,字体都是固定的,且不支持中文。第一次写,难免垃圾,慢慢...
  • 分享给大家供大家参考,具体如下:前段时间,项目需要自动生成word文档,用WordFreeMarker生成word文档后,又要求生成文档能在浏览器浏览,思来想去,把word文档转成pdf就好了,于是乎研究了一下。将word文档转化...
  • NULL 博文链接:https://llade.iteye.com/blog/2397480
  • 参考文档: 官方文档是最好的参考文档:https://developers.itextpdf.com/content/itext-7-jump-start-tutorial/installing-itext-7 本人是idea maven环境搭建的项目,首先需要在你的项目当中加入一下的依赖: &...
  • 功能: 实现根据freemarker模板生成对应的PDF文件;...指定字体、字体大小、文字方向、颜色等生成文字水印 maven依赖: <dependency> <groupId>org.xhtmlrenderer</groupId> ...
  • 设置水印时必须获取pdf文档的最上层(getOverContent())才能使得你设置的水印显示出来。mark下 /** * pdf添加水印 * * @param filePO po * @param response res * @param waterMarkContent
  • PDFtk 的服务器端 Meteor 包装, ,用于操作 PDF 文档工具。 从: 如果 PDF 是电子纸,那么 pdftk 就是电子订书机、打Kong、活页夹、秘密解码环和 X 射线眼镜。 Pdftk 是一个简单的工具,用于处理 PDF ...
  • 注:操作必须先生成水印图片并将水印图片添加至excel中,然后再将数据往excel中写入,否则在加入水印的时候会删除原excel文件并重新生成新的文件(原因是:因为懒没有去细看为啥会这样)。使用的包是 spire.xls.free...
  • 前端水印生成方案 前段时间做某系统审核后台,出现了审核人员截图把内容外泄露的情况,虽然截图内容不是特别敏感,但是安全问题还是不能忽视。于是便在系统页面上面加上了水印,对于审核人员截图等敏感操作有一定...
  • 利用PDFLib生成PDF文档

    2021-05-23 12:26:18
    一起学习利用PDFLib生成PDF文档作者:浙江省温岭电信局 王骏下载本文示例工程下载PDFLib本文代码生成的PDF文档效果图一、PDF介绍PDF是Portable Document Format的缩写,PDF文件格式是国际通用的电子文档交换事实标准...
  • 豆丁文档下载

    2018-07-04 11:22:30
    文档下载  - 支持基本所有文档网  - 添加了去水印功能  - 自动下载完成后生成一份pdf  - 完全免费,不收取任何费用
  • php根据word模板生成pdf文件并添加水印背景说明工具介绍示例代码总结 背景说明 在一些项目中,经常会出现这样的需求 用户上传了word或者excel文件,需要审核或者查看这些文件内容的时候,总不能一个一个下载吧,...
  • C# 图片处理工具类可以压缩 JPG 图像图片自动生成缩略图为图片添加水印效果 等返回高清缩略图得到最佳的图片比例缩放尺寸并获取图片类型等类代码如下 view sourceprint?001using System; 002using System....
  • C#图片处理工具类可以压缩 JPG图像图片自动生成缩略图为图片添加水印效果 等返回高清缩略图得到最佳的图片比例缩放尺寸并获取图片类型等类代码如下 view sourcepri nt?001us ing System; 002us ing System.Collect ...
  • 项目中基于Spire.Doc封装工具类实现Word文档添加图片水印? 一、Spire.Doc是什么? 1.Spire.Doc是成都冰蓝科技有限公司开发的一款简单易用、功能强大的Word文档编辑工具,有免费版本和收费版本,以下演示基于免费版本...
  • 大家好给大家带来一款图片加水印的微信小程序源码 该小程序拥有多种水印的添加方式 比如: 全屏水印(也就是整张图片都给加上水印透明度可以自行调整) 隐形水印(该水印是肉眼不可见但是通过PS等软件就可以看得到...
  • 利用Java动态生成 PDF 文档,则需要开源的API。首先我们先想象需求,在企业应用中,客户会提出一些复杂的需求,比如会针对具体的业务,构建比较典型的具备文档性质的内容,一般会导出PDF进行存档。那么目前最佳的...
  • Watermark(水印) 有序流中 Watermarks 乱序流中 Watermarks 并行流中的 Watermarks 分配时间戳/水印 (Timestamp/Watermarks) SourceFunction 自定义分配 assignTimestampsAndWatermarks 方法指定分配策略 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 6,500
精华内容 2,600
关键字:

文档水印生成工具