百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

小程序实战:PDF转换为图片工具开发

wptr33 2024-11-14 19:22 34 浏览

今天给大家分享小程序开发系列,PDF转换为图片工具的开发实战,感兴趣的朋友可以一起来学习一下!

一、开发思路

  • 申请微信小程序
  • 编写后端接口
  • 后端接口部署
  • 微信小程序前端页面开发
  • 微信小程序部署上线

1.1 申请微信小程序

关于如何申请微信小程序这里就不过多介绍了,大家可以参考腾讯官方的文档,里面介绍的非常详细。

1.2 编写后端接口

这里使用Java编程语言的SpringBoot框架来快速搭建WebAPI服务。因为涉及到PDF转换为图片,这里使用spire.pdf来实现。首先引入依赖项

      <dependency>
          <groupId>e-iceblue</groupId>
           <artifactId>spire.pdf.free</artifactId>
           <version>2.6.3</version>
           <scope>provided</scope>
       </dependency>

新建PdfUtils.java工具类库用来实现PDF转换为图片的功能

思路:通过微信小程序传递过来的文件转换为InputStream输出流,然后保存到服务器端,因为PDF可能涉及有多页,每一页单独为一个图片文件,然后调用图片拼接的方法实现所有页面图片合并为一张长图。注意:免费的spire.pdf支持10页之内的pdf转换,大家如果更高需求,可以考虑购买收费版。

主要代码如下:转换方法主函数

   /**
    * 根据文件流转换为图片
    *
    * @param stream
    * @return
    */
   public String pdftoimage(InputStream stream, String fileNameOld) {
   Date currentDate = new Date();
   SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd_HHmmss_SSS"); // 指定日期格式,包含毫秒

   String formattedDate = sdf.format(currentDate);
   String pathPath = "/mnt/files/" + formattedDate + "_" + fileNameOld;
   // 4、最终生成的doc所在的目录,默认是和引入的一个地方,开源时对外提供下载的接口。
   saveInputStreamToFile(stream, pathPath);

   String fileName = "result" + formattedDate + ".png";
   String desPath = "/mnt/files/" + fileName; // 构造文件名
   String sux = fileNameOld + "_" + formattedDate;// 临时文件前缀

   boolean result = false;
   try {
       // 0、判断输入的是否是pdf文件
       //第一步:判断输入的是否合法
       //boolean flag = isPDFFile(srcPath);
       //第二步:在输入的路径下新建文件夹
       boolean flag1 = create();

       if (flag1) {
           // 1、加载pdf
           PdfDocument pdf = new PdfDocument();
           //pdf.loadFromStream(stream);
           pdf.loadFromFile(pathPath);
           PdfPageCollection num = pdf.getPages();

           // 2、如果pdf的页数小于11,那么直接进行转化
           if (num.getCount() <= 10) {
               try {
                   for (int i = 0; i < pdf.getPages().getCount(); i++) {
                       BufferedImage image = pdf.saveAsImage(i, PdfImageType.Bitmap, 300, 300);
                       String imgTemp = imgPath + sux + (i + 1) + ".png"; // 构造输出文件路径
                       ImageIO.write(image, "PNG", new File(imgTemp));
                  }
                   pdf.close();
                   System.out.println("PDF转图片完成!");
                   MergeWordDocument.mergeImage(imgPath, desPath, sux);
                   clearFiles(imgPath, formattedDate);
                   clearFiles(pathPath, formattedDate);
              } catch (IOException e) {
                   e.printStackTrace();
                   System.out.println("PDF转图片失败: " + e.getMessage());
              }
          }
           // 3、否则输入的页数比较多,就开始进行切分再转化
           else {
               try {
                   for (int i = 0; i < 10; i++) {
                       BufferedImage image = pdf.saveAsImage(i, PdfImageType.Bitmap, 300, 300);
                       String imgTemp = imgPath + sux + (i + 1) + ".png"; // 构造输出文件路径
                       ImageIO.write(image, "PNG", new File(imgTemp));
                  }
                   pdf.close();
                   System.out.println("PDF转图片完成!");
                   MergeWordDocument.mergeImage(imgPath, desPath, sux);

              } catch (IOException e) {
                   e.printStackTrace();
                   System.out.println("PDF转图片失败: " + e.getMessage());
              } finally {
                   //clearFiles(imgPath);
                   clearFiles(pathPath, formattedDate);
              }
          }
      } else {
           System.out.println("输入的不是pdf文件");
           fileName = "";
           return fileName;
      }
  } catch (Exception e) {
       fileName = "";
       e.printStackTrace();
  } finally {
       //4、把刚刚缓存的split和doc删除
       if (result == true) {
           clearFiles(pathPath, formattedDate);
           clearFiles(splitPath, formattedDate);
           clearFiles(docPath, formattedDate);
      }
  }
   return fileName;
}

保存PDF文件到本地,然后使用后删除

/**
* 保存原始的pdf文件为了方便拆分
*
* @param inputStream
* @param filePath
*/
public static void saveInputStreamToFile(InputStream inputStream, String filePath) {

   // 使用try-with-resources自动关闭流
   try (FileOutputStream outputStream = new FileOutputStream(new File(filePath))) {
       byte[] buffer = new byte[1024];
       int length;

       // 读取输入流并写入到输出流
       while ((length = inputStream.read(buffer)) > 0) {
           outputStream.write(buffer, 0, length);
      }

       System.out.println("文件保存成功!");
  } catch (FileNotFoundException e) {
       e.printStackTrace();
  } catch (IOException e) {
       e.printStackTrace();
  }
}

多张图片合并逻辑

/**
* 多张图片合并之后的逻辑
* @param imagePath
* @param desPath
* @return
*/
public static boolean mergeImage(String imagePath, String desPath,String sux) {
   try {
       File folder = new File(imagePath);
       // 包含文件前缀的文件 简单解决并发的问题
       File[] imageFiles = folder.listFiles((dir, name) ->
              (name.toLowerCase().endsWith(".png") || name.toLowerCase().endsWith(".jpg") && name.contains(sux)));

       if (imageFiles != null && imageFiles.length > 0) {
           int maxWidth = 0;
           int totalHeight = 0;

           // 预先计算最大宽度和总高度
           for (File imageFile : imageFiles) {
               BufferedImage image = ImageIO.read(imageFile);
               maxWidth = Math.max(maxWidth, image.getWidth());
               totalHeight += image.getHeight();
               image.flush(); // 尝试释放资源
          }

           // 创建合并后的图片,仅初始化一次
           BufferedImage mergedImage = new BufferedImage(maxWidth, totalHeight, BufferedImage.TYPE_INT_ARGB);
           Graphics2D g2d = mergedImage.createGraphics();

           int currentY = 0;
           for (File imageFile : imageFiles) {
               BufferedImage image = ImageIO.read(imageFile);
               g2d.drawImage(image, 0, currentY, null);
               currentY += image.getHeight();
               image.flush(); // 处理完后释放当前图片资源
          }

           g2d.dispose();

           // 保存合并后的图片
           ImageIO.write(mergedImage, "PNG", new File(desPath));
           System.out.println("图片合并完成!");
           for (File file : imageFiles){
               if (file.exists()) {
                   if (file.delete()) {
                       System.out.println("文件 " + file.getName() + " 已被删除");
                  } else {
                       System.out.println("无法删除文件 " + file.getName());
                  }
              } else {
                   System.out.println("文件 " + file.getName() + " 不存在");
              }
          }

      } else {
           System.out.println("输入文件夹中没有图片文件!");
      }
  } catch (IOException e) {
       e.printStackTrace();
       System.out.println("图片合并失败: " + e.getMessage());
  }
   return true;
}

新建控制器PdfApi.java

用来接收小程序调用传递过来的参数,需要判断传递过来的文件是否为图片格式,然后调用转换方法即可。

 /**
    * pdf转图片 多页转一张图
    * @param uploadFile
    * @return
    * @throws IOException
    */
   @PostMapping("pdfconvertimage")
   public String upload(@RequestPart("file") MultipartFile uploadFile) throws IOException {
       if (null == uploadFile) {
           return null;
      }
       // BMP、JPG、JPEG、PNG、GIF
       String fileName = uploadFile.getOriginalFilename().toLowerCase();
       if (!fileName.endsWith(".pdf")) {
           return null;
      }
       //String image= PdfUtils.pdf(uploadFile.getInputStream(),Integer.valueOf(type));
       String image= PdfUtils.pdfToPng(uploadFile.getInputStream(),fileName);
       // 返回响应实体
       return image;
  }

1.3 后端接口部署

因为微信小程序调用第三方接口需要https域名形式,所以接口开发完成后,需要部署到云服务器,然后申请域名、申请SSL证书,确保接口可以通过https域名正常访问。并且在微信小程序开发设置配置request合法域名白名单,保证接口可以调通。


1.4 微信小程序前端页面开发

打开微信开发者工具,然后微信小程序管理员扫码登录自己的微信小程序。这里主要给大家贴出主要的代码以及实现思路。具体界面如下:

上传方式:支持微信会话文件上传、直接输入PDF文件的URL,转换成功后可以点击下载按钮进行下载图片。

wxml文件代码如下:

<view style="text-align: center;">
<image style="width: 98%;"  src="推广图片"></image>
</view>
<view class="selectSection">
    <text class="textmag">上传方式:</text>
    <radio-group bindchange="radioChange" class="radio-group">
        <label class="radio" wx:for="{{direction}}" wx:key="i">
            <icon class="radioIcon {{item.checked?'actIcon':''}}"></icon>
            <radio checked="{{item.checked}}" value="{{item.name}}"></radio>{{item.value}}
        </label>
    </radio-group>
</view>
<view class="container">
    <view wx:if="{{directionType==1}}" class="item"> <button style="width: 120px;" class="butss" bindtap="chooseFile">上传pdf文件</button></view>
    <view wx:if="{{directionType==2}}" class="item"> <button style="width: 120px;" class="butss" bindtap="chooseFileNew">生成图片</button></view>
    <view class="item"> <button style="width: 90px;" class="butss" bindtap="saveTap">下载</button></view>
    <view class="item"> <button style="width: 90px;" class="butss" bindtap="clearTap">清空</button></view>
</view>


<view style="padding: 20px;">
    <span style="color: red;font-size: 12px;">温馨提示:目前支持10页以内的pdf文件转换</span>
</view>
<view>
  <textarea  auto-height bindinput="handleInput" class="input-content" value="{{uploadUrl}}"  placeholder="请输入pdf文件url" wx:if="{{directionType==2}}"></textarea>
</view><view class="instruction"> 
    <span style="color: black;padding-left: 10px;">结果文件:{{data}}</span>
</view>


js主要代码:

    // 选择微信会话文件 然后直接调用上传接口
    chooseFile: function () {
        var that = this;
        wx.showLoading({
            title: '图片上传处理中,请稍后...',
        });
        wx.chooseMessageFile({
            count: 1,
            type: 'file',
            extension: ['pdf'], // 限定选择的文件格式为.doc, .docx, .pdf
            success: function (res) {
                const tempFilePath = res.tempFiles[0].path;
                if (res.tempFiles[0].size > 10 * 1024 * 1024) { // 限定文件大小为2MB
                    wx.showToast({
                        title: '文件大小超过限制,请选择小于10MB的文件',
                        icon: 'none'
                    });
                    return;
                }
                that.setData({
                    pdfPath: tempFilePath
                })


                wx.uploadFile({
                    url: '后端接口API',
                    filePath: tempFilePath,
                    formData: {                      
                    },
                    name: 'file',
                    success: function (res) {
                        if (res.statusCode == "200") {                          
                            that.setData({
                                imageUrl: res.data,// 直接可以访问的url
                                data: res.data
                            });                         
                            wx.showToast({
                                title: '转换成功',
                                icon: 'success',
                                duration: 2000
                            });
                        } else {
                            wx.showToast({
                                title: '转换失败,请联系管理员',
                                icon: 'none',
                                duration: 2000
                            });
                        }
                    },
                    fail: function (res) {
                        wx.showToast({
                            title: '上传失败',
                            icon: 'none',
                            duration: 2000
                        });
                    }
                });
            },
            fail: function (res) {
                console.error('选择文件失败', res);
                wx.showToast({
                    title: '选择文件失败',
                    icon: 'none',
                    duration: 2000
                });
            }
        });
    },
    
    // 下载按钮事件
    saveTap: function () {
        if (this.data.imageUrl) {
            wx.downloadFile({
                url: this.data.imageUrl,
                success: function (res) {
                    if (res.statusCode === 200) {
                        var filePath = res.tempFilePath;
                        // 调用保存图片方法
                        wx.saveImageToPhotosAlbum({
                            filePath: filePath,
                            success: function (res) {
                                wx.showToast({
                                    title: '保存成功',
                                    icon: 'success',
                                    duration: 2000
                                });
                            },
                            fail: function (err) {
                                console.error(err);
                                wx.showToast({
                                    title: '保存失败',
                                    icon: 'none',
                                    duration: 2000
                                });
                            }
                        });
                    }
                },
                fail: function (err) {
                    console.error(err);
                    wx.showToast({
                        title: '下载失败',
                        icon: 'none',
                        duration: 2000
                    });
                }
            });
        } else {
            wx.showToast({
                title: '请先上传pdf文件,转换成功后再保存',
                icon: 'none',
                duration: 2000
            });
        }
    },

1.5 运行效果

选择pdf文件上传


转换成功之后的结果文件如下:


然后可以点击下载按钮下载图片文件。整体转还原度还是很高的。大家可以微信搜一搜“小明工作助手”小程序直接体验一下。

1.6 小程序部署上线

该步骤对于小程序开发的朋友来说,还是非常简单的,这里就不过多介绍了,大家有问题的话,欢迎沟通交流!

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...