PDF转图片,视频截取图片为封面,处理大文件效率较慢的问题

事件过程介绍

  • 首先我们此次遇到的问题瓶颈;
    • PDF页数较多,转换事件太久,内存溢出
    • 文件较多,不适用于单一线程,过程太耗时
    • 转换的图片较大,再次影响性能

解决方法

异步处理文件数据,等待处理结果,数据存储提升性能

话不多说直接上代码

srevice层 此时的sendMessage 是个异步类

public Boolean addJyKsTrainInformation(JyKsTrainInformation train, ServletWebRequest request) {
        try {
            train.setCreateName(LoginHelper.getNickName());
            save(train);
            if (CollectionUtils.isNotEmpty(train.getList())) {
                List<JyKsFile> list = train.getList();
                list.forEach(v -> {
                    v.setDataId(train.getId());
                });
                jyKsFileMapper.insertBatch(list);
                //文件新增时需要异步处理pdf文件转成图片
                sendMessage.createPdfToImageAsync(list, LoginHelper.getLoginUser(), request);
            }
            //判断资料分类是否不是管理员资料,如果是管理员资料这里暂时不进行通知,否则进行消息通知
            JyKsGroupTitle groupTitle = jyKsGroupTitleMapper.selectById(train.getGroupTitleId());
            if (groupTitle.getIsAdmin().equals(JySystemConstant.DELETE_FLG_FALSE)) {
                List<SysUser> users = sysUserMapper.getSysUserListByRoleKey("XMJL");
                String msg = train.getTitle() + "已发布,请尽快完成培训!";
                for (SysUser user : users) {
                    sendMessage.sendMessage(msg, user.getUserId(), user.getUserName(), train.getGroupTitleName(), train.getGroupTitleId().toString(), msg);
                }
            }

            return true;
        } catch (Exception e) {
            throw new GlobalException("数据新增失败,请联系管理员!");
        }
    }

sendMessage 此时我们在异步中进行了再次异步处理,将每个文件进行单一处理,当然pdfToImageAsync也是异步类

/**
     * 异步将pdf转为图片,便于前端获取图片,缩短时长
     * @param list
     * @param loginUser
     */
    @Async
    public void createPdfToImageAsync(List<JyKsFile> list, LoginUser loginUser, ServletWebRequest request) {
        RequestContextHolder.setRequestAttributes(request);
        List<Future<JyKsFileToImage>> futures = new ArrayList<>();
        List<Future<JyKsFile>> videos = new ArrayList<>();
        boolean flg = false;
        for (JyKsFile ksFile : list) {
            if (ksFile.getFileSuffix().equals(".pdf")) {
                Future<JyKsFileToImage> future = pdfToImageAsync.pdfToImagesAsync(ksFile, loginUser,request);
                futures.add(future);
            } else if (ksFile.getFileSuffix().equals(".mp4")){
                Future<JyKsFile> future = pdfToImageAsync.videoToImage(ksFile, request);
                videos.add(future);
            }
        }
        if (CollectionUtils.isNotEmpty(videos)) {
            List<JyKsFile> files = new ArrayList<>();
            for (Future<JyKsFile> video : videos) {
                try {
                    JyKsFile file = video.get();
                    if ("1".equals(file.getCode())) {
                        files.add(file);
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
            if (CollectionUtils.isNotEmpty(files)) {
                jyKsFileMapper.updateBatchById(files);
            }
        }
        if (CollectionUtils.isNotEmpty(futures)) {
            List<JyKsFileToImage> imageList = new ArrayList<>();
            for (Future<JyKsFileToImage> future : futures) {
                try {
                    JyKsFileToImage image = future.get();
                    if ("1".equals(image.getCode())) {
                        imageList.add(image);
                    }
                } catch (Exception e) {
                    log.error(e.getMessage(), e);
                }
            }
            if (CollectionUtils.isNotEmpty(imageList)) {
                jyKsFileToImageMapper.insertBatch(imageList);
            }
        }
    }

PdfToImageAsync

import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONArray;
import com.ruoyi.common.cache.NotResourceCache;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.VideoUtils;
import com.ruoyi.jinyu.entity.JyKsFile;
import com.ruoyi.jinyu.entity.JyKsFileToImage;
import com.ruoyi.oss.entity.SysOss;
import com.ruoyi.system.service.ISysOssService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

/**
 * @Description
 * @Author GGBond
 * @Date 2023-09-14 16:48
 */
@Slf4j
@Component
public class PdfToImageAsync {

    @Autowired
    private ISysOssService ossService;

    @Async
    public Future<JyKsFileToImage> pdfToImagesAsync(JyKsFile ksFile, LoginUser loginUser, ServletWebRequest request) {
        RequestContextHolder.setRequestAttributes(request);
        JyKsFileToImage toImage = new JyKsFileToImage();
        toImage.setFileId(ksFile.getId());
        toImage.setCreateBy(loginUser.getNickname());
        toImage.setUpdateBy(loginUser.getNickname());
        InputStream inputStream = null;
        try {
            // 当作一个URL来装载文件
            URL url = new URL(ksFile.getPath());
            URLConnection con = url.openConnection();
            con.setConnectTimeout(3 * 1000);
            inputStream = con.getInputStream();
            PDDocument doc = PDDocument.load(inputStream);
            doc.setResourceCache(new NotResourceCache());
            int pageCount = doc.getNumberOfPages();
            PDFRenderer pdfRenderer = new PDFRenderer(doc);
            String imageFilePath;
            List<String> lists = new ArrayList<>();
            Snowflake snowflake = IdUtil.createSnowflake(1,1);
            long id = snowflake.nextId();
            for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
                imageFilePath = id + pageIndex + ".png";
                File file = new File(imageFilePath);
                //需要注意的是dpi是像素,参数越大生成图片分辨率越高,转换时间也就越长
                BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 105, ImageType.RGB);
                ImageIOUtil.writeImage(image, imageFilePath,105);
                MultipartFile multipartFile = new MockMultipartFile(String.valueOf(pageIndex), imageFilePath, null, Files.newInputStream(file.toPath()));
                SysOss sysOss = ossService.upload(multipartFile);
                file.delete();
                lists.add(sysOss.getUrl());
            }
            // 关闭文档
            doc.close();
            toImage.setImages(JSONArray.toJSONString(lists));
            toImage.setCode("1");
            return new AsyncResult<>(toImage);
        } catch (IOException e) {
            log.error("pdf转换jpg图片异常!", e);
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                //do nothing
            }
        }
        toImage.setCode("2");
        return new AsyncResult<>(toImage);
    }

    @Async
    public Future<JyKsFile> videoToImage(JyKsFile ksFile,ServletWebRequest request) {
        RequestContextHolder.setRequestAttributes(request);
        File file = null;
        Snowflake snowflake = IdUtil.createSnowflake(1,1);
        long id = snowflake.nextId();
        try {
            //如果是视频时,需要截图,用来做封面图片
            file = new File(id + "temp.mp4");
            if (!file.exists()) {
                file.createNewFile();
            }
            FileUtils.copyURLToFile(new URL(ksFile.getPath()), file);
            MultipartFile multipartFile = VideoUtils.generateCover(file, 1);
            SysOss sysOss = ossService.upload(multipartFile);
            ksFile.setVideoPath(sysOss.getUrl());
            ksFile.setCode("1");
            return new AsyncResult<>(ksFile);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            if (null != file) {
                file.delete();
            }
        }
        ksFile.setCode("2");
        return new AsyncResult<>(ksFile);
    }
}

事件注意点

  • 1、ServletWebRequest 是什么,我们这里采用了satoken + jwt 方式进行用户登录,当我们使用@Async时,后续的方法是无法在异步时获取我们的用户信息的,RequestContextHolder.setRequestAttributes(request);这里我们将每个异步线程进行请求头绑定,后续就可以拿到用户信息了
  • 2、我们这里采用异步线程池,控制线程数量
  • 3、Future 我们这里为什么采用Future,首先根据自己的业务,我们这里将图片存入到oss系统进行保存,所以我们需要等待异步结束后get()获取执行结果的url在更新到我们的业务表中;
  • 4、我们读取PDF时需要注意的是 renderImageWithDPI()方法,第二个参数越大生成图片分辨率越高,转换时间也就越长,如果时间还是很久建议再调整下这里的参数,满足正常看的功能就行

异步线程池配置

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

 /**
 * 原生(Spring)异步任务线程池装配类,实现AsyncConfigurer重写他的两个方法,这样在使用默认的
 * 线程池的时候就会使用自己重写的
 * @Description
 * @Author GGBond
 * @Date 2023-09-14 16:48
 */
@Slf4j
@Configuration
@ComponentScan(basePackages = {"当前文件的包路径"})
@EnableAsync
public class ThreadConfig implements AsyncConfigurer {

    private int core = 20;

    private int max = 200;

    private int capacity = 500;

    private int alive = 60;

    @Override
    @Bean
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        // 核心线程数
        taskExecutor.setCorePoolSize(core);
        // 最大线程数
        taskExecutor.setMaxPoolSize(max);
        // 队列最大长度
        taskExecutor.setQueueCapacity(capacity);
        // 线程池维护线程所允许的空闲时间(单位秒)
        taskExecutor.setKeepAliveSeconds(alive);
        // 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
        taskExecutor.setRejectedExecutionHandler((r, executor) -> {
            // 如果出现了任务数量过多,后续考虑增加队列容量,或者增加线程数量等
            log.error("线程池等待任务数量过多,该任务已被丢弃!");
        });
        taskExecutor.initialize();
        return taskExecutor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        log.warn("Get in AsyncUncaughtExceptionHandler !");
        return null;
    }
}

结语

编写不易,大家多点点赞,支持下,感谢!!!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值