使用itext和freemarker来根据Html模板生成PDF文件,加水印、印章

内容纲要

功能:

实现根据freemarker模板生成对应的PDF文件;
可以指定文字、位置、页数生成指定的印章(图片),可以指定印章大小;
指定字体、字体大小、文字方向、颜色等生成文字水印
maven依赖:
        <dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf-itext5</artifactId>
            <version>9.1.18</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.27-incubating</version>
        </dependency>
将html模板和数据进行匹配,然后用流的方式生成PDF文件
生成PDF工具类源码:
package com.lifengdi.file.pdf;

import com.itextpdf.text.*;
import com.itextpdf.text.pdf.*;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.lifengdi.config.GlobalConfig;
import com.lifengdi.config.PDFFontConfig;
import com.lifengdi.config.SystemConfig;
import com.lifengdi.file.pdf.listener.MyTextLocationListener;
import com.lifengdi.model.pdf.Location;
import com.lifengdi.model.pdf.PDFStamperConfig;
import com.lifengdi.model.pdf.PDFTempFile;
import com.lifengdi.model.pdf.PDFWatermarkConfig;
import com.lifengdi.util.GeneratedKey;
import com.lifengdi.util.MyStringUtil;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.xhtmlrenderer.pdf.ITextFontResolver;
import org.xhtmlrenderer.pdf.ITextRenderer;

import javax.annotation.Resource;
import java.io.*;
import java.util.Objects;

/**
 * 生成PDF工具类
 *
 * @author 李锋镝
 * @date Create at 10:42 2019/5/9
 */
@Component
@Slf4j
public class HtmlToPDF {

    @Resource
    private SystemConfig systemConfig;

    @Resource
    private GeneratedKey generatedKey;

    /**
     * 模板和数据匹配
     *
     * @param data        数据
     * @param pdfTempFile pdfTempFile
     * @return html
     */
    public String matchDataToHtml(Object data, PDFTempFile pdfTempFile) {
        StringWriter writer = new StringWriter();
        String html;
        try {
            // FreeMarker配置
            Configuration config = new Configuration(Configuration.VERSION_2_3_25);
            config.setDefaultEncoding(GlobalConfig.DEFAULT_ENCODING);
            // 注意这里是模板所在文件夹,不是模版文件
            String parentPath, tempFileName = pdfTempFile.getTemplateFileName();
            if (MyStringUtil.isHttpUrl(pdfTempFile.getTemplateFileParentPath())) {
                parentPath = systemConfig.getLocalTempPath();
            } else {
                parentPath = pdfTempFile.getTemplateFileParentPath();
                // 将项目中的文件copy到服务器本地
                if (!parentPath.endsWith(File.separator))
                    parentPath = parentPath + File.separator;
                String localTempPath = systemConfig.getLocalTempPath();
                String target = (localTempPath.endsWith(File.separator) ? localTempPath : localTempPath + File.separator) + tempFileName;
                InputStream tempFileInputStream = new ClassPathResource(parentPath + tempFileName).getInputStream();
                FileUtils.copyInputStreamToFile(tempFileInputStream, new File(target));
            }
            log.info("模板和数据匹配,模板文件parentPath:{}", parentPath);

            config.setDirectoryForTemplateLoading(new File(systemConfig.getLocalTempPath()));
            config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
            config.setLogTemplateExceptions(false);
            // 根据模板名称 获取对应模板
            Template template = config.getTemplate(tempFileName);
            // 模板和数据的匹配
            template.process(data, writer);
            writer.flush();
            html = writer.toString();
            return html;
        } catch (Exception e) {
            log.error("PDF模板和数据匹配异常", e);
        } finally {
            try {
                writer.close();
            } catch (IOException e) {
                log.error("StringWriter close exception.", e);
            }
        }
        return null;
    }


    /**
     * 生成PDF
     *
     * @param html           html字符串
     * @param targetFileName 目标文件名
     * @return 生成文件的名称
     * @throws Exception e
     */
    public String createPDF(String html, String targetFileName) throws Exception {
        if (StringUtils.isBlank(html)) {
            return null;
        }
        targetFileName = getFileName(targetFileName);
        log.info("生成PDF,targetFileName:{}", targetFileName);
        String targetFilePath = getTargetFileTempPath(targetFileName);
        FileOutputStream outFile = new FileOutputStream(targetFilePath);
        ITextRenderer renderer = new ITextRenderer();
        renderer.setDocumentFromString(html);
        // 解决中文支持问题
        log.info("加载字体");
        ITextFontResolver fontResolver = renderer.getFontResolver();
        fontResolver.addFont(SystemConfig.SourceHanSansCN_Regular_TTF, "SourceHanSansCN", BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED, null);
        fontResolver.addFont(SystemConfig.FONT_PATH_SONG, BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
        renderer.layout();
        renderer.createPDF(outFile);
        log.info("生成PDF,targetFileName:{},生成成功", targetFileName);
        return targetFileName;
    }

    /**
     * 渲染文件
     *
     * @param filePath           PDF文件路径
     * @param outFilePath        PDF文件路径
     * @param pdfStamperConfig   pdfStamperConfig
     * @param pdfWatermarkConfig pdfWatermarkConfig
     */
    public void renderLayer(String filePath, String outFilePath, PDFStamperConfig pdfStamperConfig, PDFWatermarkConfig pdfWatermarkConfig) {

        log.info("渲染文件,filePath:{},outFilePath:{}", filePath, outFilePath);
        InputStream inputStream = null;
        try {
            filePath = getTargetFileTempPath(filePath);
            inputStream = new FileInputStream(filePath);
            PdfReader reader = new PdfReader(inputStream);
            PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(getTargetFileTempPath(outFilePath)));
            if (Objects.nonNull(pdfStamperConfig) && pdfStamperConfig.getGenerate()) {
                // 印章
                log.info("添加印章,pdfStamperConfig:{}", pdfStamperConfig);
                stamper(pdfStamperConfig, reader, stamper);
            }
            if (Objects.nonNull(pdfWatermarkConfig) && pdfWatermarkConfig.getGenerate()) {
                // 水印
                log.info("添加水印,pdfWatermarkConfig:{}", pdfWatermarkConfig);
                watermark(reader, stamper, pdfWatermarkConfig);
            }
            stamper.close();
            reader.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != inputStream) {
                    inputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 印章
     *
     * @param pdfStamperConfig pdfStamperConfig
     * @param reader           reader
     * @param stamper          stamper
     * @throws IOException       IOException
     * @throws DocumentException DocumentException
     */
    private void stamper(PDFStamperConfig pdfStamperConfig, PdfReader reader, PdfStamper stamper) throws IOException, DocumentException {
        Image image;
        if (!MyStringUtil.isHttpUrl(pdfStamperConfig.getStamperUrl())) {
            // 将项目中的文件copy到服务器本地
            String imagePath = pdfStamperConfig.getStamperUrl();
            String imageName = imagePath.substring(imagePath.indexOf("/") + 1, imagePath.length());
            String localTempPath = systemConfig.getLocalTempPath();
            String target = (localTempPath.endsWith(File.separator) ? localTempPath : localTempPath + File.separator) + imageName;
            InputStream tempFileInputStream = new ClassPathResource(pdfStamperConfig.getStamperUrl()).getInputStream();
            FileUtils.copyInputStreamToFile(tempFileInputStream, new File(target));
            image = Image.getInstance(target);
        } else {
            image = Image.getInstance(pdfStamperConfig.getStamperUrl());
        }

        PdfReaderContentParser parser = new PdfReaderContentParser(reader);
        Document document = new Document();
        log.info("A4纸width:{},height:{}", document.getPageSize().getWidth(), document.getPageSize().getHeight());
//            // 取所在页和坐标,左下角为起点
//            float x = document.getPageSize().getWidth() - 240;
//            float y = document.getPageSize().getHeight() - 680;
        int pageSize = reader.getNumberOfPages();
        Location location = pdfStamperConfig.getLocation();
        if (Objects.nonNull(location)) {
            Integer page = location.getPage();// -1:全部,0:最后一页,1:首页
            if (Objects.nonNull(page)) {
                switch (page) {
                    case -1:
                        for (int pageNumber = 1; pageNumber <= pageSize; pageNumber++) {
                            insertImage(pdfStamperConfig, stamper, image, parser, pageSize);
                        }
                        break;
                    case 0:
                        insertImage(pdfStamperConfig, stamper, image, parser, pageSize);
                        break;
                    default:
                        if (page > 0) {
                            insertImage(pdfStamperConfig, stamper, image, parser, page);
                        }
                        break;
                }
            }

        }
    }

    /**
     * 向指定位置插入图片
     *
     * @param pdfStamperConfig pdfStamperConfig
     * @param stamper          stamper
     * @param image            image
     * @param parser           parser
     * @param pageNumber       pageNumber
     * @throws IOException       IOException
     * @throws DocumentException DocumentException
     */
    private void insertImage(PDFStamperConfig pdfStamperConfig, PdfStamper stamper, Image image, PdfReaderContentParser parser, int pageNumber)
            throws IOException, DocumentException {
        float x, y;
        Location xy = new Location();
        if (StringUtils.isNotBlank(pdfStamperConfig.getLocation().getWord())) {
            parser.processContent(pageNumber, new MyTextLocationListener(pdfStamperConfig, xy));
        } else {
            xy = pdfStamperConfig.getLocation();
        }
        if (Objects.isNull(xy)) {
            return;
        }

        if (Objects.isNull(xy.getX()) || Objects.isNull(xy.getY())) {
            return;
        }
        x = xy.getX();
        y = xy.getY();
        if (x < 0 || y < 0) {
            return;
        }
        // 读图片
        // 获取操作的页面
        PdfContentByte under = stamper.getOverContent(pageNumber);
//         根据域的大小缩放图片
//        image.scaleToFit(Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth(),
//                Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight());
        image.scaleAbsolute(Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth(),
                Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight());
        // 添加图片
        image.setAbsolutePosition(x, y);
        under.addImage(image);
    }

    /**
     * 水印
     *
     * @param reader             reader
     * @param stamper            stamper
     * @param pdfWatermarkConfig pdfWatermarkConfig
     */
    private void watermark(PdfReader reader, PdfStamper stamper, PDFWatermarkConfig pdfWatermarkConfig) {
        if (Objects.isNull(pdfWatermarkConfig) || !pdfWatermarkConfig.getGenerate()) {
            return;
        }
        PdfContentByte under;
        // 字体
        BaseFont font = PDFFontConfig.FONT_MAP.get(PDFFontConfig.SIM_SUN);
        String fontFamily = pdfWatermarkConfig.getFontFamily();
        if (StringUtils.isNotBlank(fontFamily) && PDFFontConfig.FONT_MAP.containsKey(fontFamily)) {
            font = PDFFontConfig.FONT_MAP.get(fontFamily);
        }
        // 原pdf文件的总页数
        int pageSize = reader.getNumberOfPages();
        PdfGState gs = new PdfGState();
        // 设置填充字体不透明度为0.1f
        gs.setFillOpacity(0.1f);
        Document document = new Document();
        float documentWidth = document.getPageSize().getWidth(), documentHeight = document.getPageSize().getHeight();
        Location location = pdfWatermarkConfig.getLocation();
        if (Objects.isNull(location)) {
            location = new Location();
        }
        final float xStart = 0, yStart = 0,
                xInterval = Objects.nonNull(location.getXInterval()) ? location.getXInterval() : GlobalConfig.DEFAULT_X_INTERVAL,
                yInterval = Objects.nonNull(location.getYInterval()) ? location.getYInterval() : GlobalConfig.DEFAULT_Y_INTERVAL,
                rotation = 45,
                fontSize = Objects.isNull(pdfWatermarkConfig.getFontSize()) ? GlobalConfig.DEFAULT_FONT_SIZE : pdfWatermarkConfig.getFontSize();

        String watermarkWord = pdfWatermarkConfig.getWatermarkWord();
        int red = -1, green = -1, blue = -1;
        String[] colorArray = pdfWatermarkConfig.getWatermarkColor().split(",");
        if (colorArray.length >= 3) {
            red = Integer.parseInt(colorArray[0]);
            green = Integer.parseInt(colorArray[1]);
            blue = Integer.parseInt(colorArray[2]);
        }
        for (int i = 1; i <= pageSize; i++) {
            // 水印在之前文本下
            if (Objects.nonNull(location.getOverContent()) && location.getOverContent()) {
                under = stamper.getOverContent(i);
            } else {
                under = stamper.getUnderContent(i);
            }
            under.beginText();
            // 文字水印 颜色
            if (red >= 0) {
                under.setColorFill(new BaseColor(red, green, blue));
            } else {
                under.setColorFill(BaseColor.GRAY);
            }
            // 文字水印 字体及字号
            under.setFontAndSize(font, fontSize);
            under.setGState(gs);
            // 文字水印 起始位置
            under.setTextMatrix(xStart, yStart);

            if (StringUtils.isNotBlank(watermarkWord)) {
                for (float x = xStart; x <= documentWidth + xInterval; x += xInterval) {
                    for (float y = yStart; y <= documentHeight + yInterval; y += yInterval) {
                        under.showTextAligned(Element.ALIGN_CENTER, watermarkWord, x, y, rotation);
                    }
                }
            }
            under.endText();
        }
    }

    /**
     * 获取生成的PDF文件本地临时路径
     *
     * @param targetFileName 目标文件名
     * @return 本地临时路径
     */
    public String getTargetFileTempPath(String targetFileName) {

        String localTempPath = systemConfig.getLocalTempPath();
        if (!localTempPath.endsWith(File.separator)) {
            localTempPath = localTempPath + File.separator;
        }
        return localTempPath + targetFileName;
    }

    private String getFileName(String targetFileName) {
        if (StringUtils.isBlank(targetFileName)) {
            targetFileName = generatedKey.generatorKey();
        }
        if (!StringUtils.endsWithIgnoreCase(targetFileName, GlobalConfig.PDF_SUFFIX)) {
            targetFileName = targetFileName + GlobalConfig.PDF_SUFFIX;
        }
        return targetFileName;
    }
}
获取指定文字坐标,用来在指定文字上生成印章,主要实现了RenderListener来获取指定文字的坐标。
源码:
package com.lifengdi.file.pdf.listener;

import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.lifengdi.config.GlobalConfig;
import com.lifengdi.model.pdf.Location;
import com.lifengdi.model.pdf.PDFStamperConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.Objects;

/**
 * @author 李锋镝
 * @date Create at 15:32 2019/5/9
 */
@Slf4j
public class MyTextLocationListener implements RenderListener {

    private String text;

    private PDFStamperConfig pdfStamperConfig;

    private Location location;

    public MyTextLocationListener(PDFStamperConfig pdfStamperConfig, Location location) {
        if (StringUtils.isNotBlank(pdfStamperConfig.getLocation().getWord())) {
            this.text = pdfStamperConfig.getLocation().getWord();
        }

        this.pdfStamperConfig = pdfStamperConfig;
        this.location = location;
    }

    @Override
    public void beginTextBlock() {
    }

    @Override
    public void renderText(TextRenderInfo renderInfo) {
        String renderInfoText = renderInfo.getText();
        if (!StringUtils.isEmpty(renderInfoText) && renderInfoText.contains(text)) {
            Rectangle2D.Float base = renderInfo.getBaseline().getBoundingRectange();
            float leftX = (float) base.getMinX();
            float leftY = (float) base.getMinY() - 1;
            float rightX = (float) base.getMaxX();
            float rightY = (float) base.getMaxY() + 1;
            Rectangle2D.Float rect = new Rectangle2D.Float(leftX, leftY, rightX - leftX, rightY - leftY);

            // 当前行长度
            int length = renderInfoText.length();
            // 单个字符的长度
            float wordWidth = rect.width / length;
            // 指定字符串首次出现的索引
            int i = renderInfoText.indexOf(text);

            Float fitWidth = Objects.isNull(pdfStamperConfig.getFitWidth()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_WIDTH : pdfStamperConfig.getFitWidth();
            Float fitHeight = Objects.isNull(pdfStamperConfig.getFitHeight()) ? GlobalConfig.PDF_STAMPER_DEFAULT_FIT_HEIGHT : pdfStamperConfig.getFitHeight();
            float fitWidthRadius = 0f, fitHeightRadius = 0f;
            if (fitWidth > 0) {
                fitWidthRadius = fitWidth / 2;
            }
            if (fitHeight > 0) {
                fitHeightRadius = fitHeight / 2;
            }
            // 偏移量
            Float xOffset = Objects.isNull(pdfStamperConfig.getXOffset()) ? 0F : pdfStamperConfig.getXOffset();
            Float yOffset = Objects.isNull(pdfStamperConfig.getYOffset()) ? 0F : pdfStamperConfig.getYOffset();
            // 设置印章的XY坐标
            float x, y;
            if (rect.x < 60) {
                x = wordWidth * i + fitHeightRadius + xOffset;
            } else {
                x = rect.x + xOffset;
            }
            y = rect.y - fitWidthRadius + yOffset;
            location.setY(y > 0 ? y : 0);
            location.setX(x > 0 ? x : 0);
            log.info("text:{}, location:{}", text, location);
        }
    }

    @Override
    public void endTextBlock() {
    }

    @Override
    public void renderImage(ImageRenderInfo renderInfo) {
    }
}
其他配置类:
package com.lifengdi.model.pdf;

import lombok.Data;

/**
 * PDF模板配置
 * @author 李锋镝
 * @date Create at 11:10 2019/5/9
 */
@Data
public class PDFTempFile {
    /**
     * PDF模板文件类型
     */
    private String pdfType;

    /**
     * 模板文件名称
     */
    private String templateFileName;

    /**
     * 模板文件所在父级路径
     */
    private String templateFileParentPath;

    /**
     * 模板文件本地地址
     */
    private String templateFileLocalPath;
}
package com.lifengdi.config;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @author 李锋镝
 * @date Create at 10:51 2019/5/9
 */
@Getter
@Configuration
public class SystemConfig {
    /**
     * 文件本地缓存目录
     */
    @Value("${file.localTempPath}")
    private String localTempPath;

    /**
     * 项目中自带字体的目录
     */
    public static final String FONT_PATH_SONG = "font/simsun.ttf";

    public static final String SourceHanSansCN_Regular_TTF = "font/SourceHanSansCN-Regular.ttf";

    /**
     * 项目中的缓存目录
     */
    @Value("${file.appTempFolder:file/}")
    private String appTemplateFolder;
}
package com.lifengdi.model.pdf;

import lombok.Data;

/**
 * PDF印章配置
 * @author 李锋镝
 * @date Create at 11:16 2019/5/9
 */
@Data
public class PDFStamperConfig {

    /**
     * 是否生成印章
     */
    private Boolean generate = false;

    /**
     * 印章文件路径(PNG格式)
     */
    private String stamperUrl;

    /**
     * 生成印章的位置
     */
    private Location location;

    /**
     * 印章宽度
     */
    private Float fitWidth;

    /**
     * 印章高度
     */
    private Float fitHeight;

    /**
     * X偏移量
     */
    private Float xOffset;
    /**
     * Y偏移量
     */
    private Float yOffset;
}
package com.lifengdi.model.pdf;

import lombok.Data;

/**
 * PDF文件水印配置
 * @author 李锋镝
 * @date Create at 11:17 2019/5/9
 */
@Data
public class PDFWatermarkConfig {

    /**
     * 是否生成水印
     */
    private Boolean generate;

    /**
     * 水印文字
     */
    private String watermarkWord;

    /**
     * 水印文字颜色 red,green,blue
     */
    private String watermarkColor = "128,128,128";

    /**
     * 水印透明度
     */
    private Float fillOpacity = 0.1F;

    /**
     * 水印文字大小
     */
    private Integer fontSize = 38;

    /**
     * 水印文字字体
     */
    private String fontFamily;

    /**
     * 生成水印的位置,不指定则默认整页
     */
    private Location location;

}
package com.lifengdi.model.pdf;

import lombok.Data;

/**
 * 页面位置坐标
 * @author 李锋镝
 * @date Create at 11:35 2019/5/9
 */
@Data
public class Location {

    /**
     * X坐标
     */
    private Float x;

    /**
     * Y坐标
     */
    private Float y;

    /**
     * 横向间隔
     */
    private Float xInterval;

    /**
     * 纵向间隔
     */
    private Float yInterval;

    /**
     * 指定页 -1:全部,0:最后一页,1:首页
     */
    private Integer page;

    /**
     * 在指定文字上盖章
     */
    private String word;

    /**
     * 水印是否在内容上边
     */
    private Boolean overContent;
}
package com.lifengdi.model.pdf;

import com.lifengdi.model.IType;
import lombok.Data;

/**
 * @author 李锋镝
 * @date Create at 14:07 2019/5/9
 */
@Data
public class PDFType implements IType {

    private PDFTempFile pdfTempFile;

    private PDFStamperConfig pdfStamperConfig;

    private PDFWatermarkConfig pdfWatermarkConfig;
}
package com.lifengdi.config;

import com.lifengdi.model.pdf.PDFType;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * PDF配置
 * @author 李锋镝
 * @date Create at 11:10 2019/5/9
 */
@Data
@Component
@ConfigurationProperties("pdf")
public class PDFConfig {

    private Map<String, PDFType> pdfType = new HashMap<>();
}
在application.yml配置如下:

# 生成PDF文件配置
pdf:
  pdfType:
    test:
      pdfTempFile:
        pdfType: test
        templateFileName: test.ftl
        templateFileParentPath: file/
      pdfStamperConfig:
        generate: true
# 印章所在路径
        stamperUrl: image/666.png
        xOffset: 0
        yOffset: 45
        fitWidth: 120
        fitHeight: 120
        location:
          x: -1
          y: -1
          page: 1
          word: 检测日期
      pdfWatermarkConfig:
        generate: true
        watermarkWord: 水印
        watermarkColor:
        fillOpacity:
        fontSize:
        fontFamily: SourceHanSansCN
        location:
          xInterval: 100
          yInterval: 120
          page:
          overContent: true
生成PDF效果截图:

源码地址:https://github.com/lifengdi/generate-file

 


除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/archives/article/tech/861

打赏 赞(0)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

说点什么

avatar
  订阅  
提醒