跟着大佬学习大文件的切片上传,yyds

简介: 跟着大佬学习大文件的切片上传,yyds

读取文件 fileReader


这个对象上的api可以以不同的方式读取文件内容到result属性中。


  • readAsText(file, encoding): 以纯文本的形式读取文件


  • readAsDataURL(file): 将文件以base64的方式存储在result中。


  • readAsBinaryString(file): 以字符串的形式读取文件,字符串中的每个字符表示一个字节。


  • readAsArrayBuffer(file): 读取文件,将文件以ArrayBuffer格式存储在result中。


具有三个事件来为我们在读取文件时做一些操作


  • error: 读取文件发生错误时触发


  • progress: 继续读取文件时触发,每次默认读取109117440字节文件。


  • load: 全部读取文件时触发


在读取对应的文件时,一定要选择正确的api进行读取,不然result可能会出现乱码或者为空。


如果监听的是progress事件,那么他每次读取109117440字节的文件。并且这里面是不能获取到result的值的。还是需要监听load事件来获取result值。


FileReader 对象允许 Web 应用程序异步读取存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。


网络异常,图片无法展示
|


slice 文件截取


通过slice方法,file是blob的子类型。但是每个浏览器对于file文件对象的截取方法不同。所以一般都是使用blob.slice来截取的。


  • 第一个参数有表示开始截取的字节数。


  • 第二个参数表示截取的长度。


第二个参数 - 第一个·参数才是读取的长度。


如果我们想要分段读取文件。那么我们就需要在onload事件中去再次切割文件,来达到递归调用的效果,直到文件读取完毕。


let reader = new FileReader();
    let blob = null;
    function readerBlob(start) {
      blob = files[0].slice(start, start + 100000000);
      // 将文件读取到ArrayBuffer中然后存入reader.result
      reader.readAsArrayBuffer(blob)
    }
    readerBlob(start)
    reader.onload = function (e) {
      console.log("blob.size", blob.size) // 但是·就我感觉slice的第二个参数他并不是读取的长度,而是每次截止的字节数。2 - 1才是读取的总大小
      // 每次切片的数据
      console.log("this.result", this.result)
      if(blob.size < total)  {
        start = start + 100000000;
      }
      readerBlob(start)
    };


对象URL URL.createObjectURL


传递一个blob类型的数据,然后会生成一个字符串。指向一块内存地址。


可以实现图片预览。而不需要后端返回上传图片的url。


<input type="file" name="a" id="a">
    <img id="img" src="" alt="">
    <script>
        const a = document.getElementById("a")
        const img = document.getElementById("img")
        a.onchange = function(e) {
            // 获取文件对象
            const [file] = e.target.files;
            // 创建一个blob格式的url对象
            const url = URL.createObjectURL(file)
            img.setAttribute("src", url)
        }
    </script>


如果不在使用这个字符串,那么我们需要手动释放内存。


URL.revokeObjectURL(url)


预览图片第二种方式


通过FileReader对象将file对象转换为base64格式,然后放在result中返回,这时就可以监听onload事件拿到该值。


//第二种:使用FileReader
const reader = new FileReader();
reader.onload = (function (aImg) {
    return function (e) { 
        aImg.src = e.target.result; 
    }; 
})(img);
reader.readAsDataURL(file);


表单键值对对象 FormData


FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过XMLHttpRequest.send() 方法发送出去,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。


创建一个formData对象


一个可选参数form, 他是表单的form对象, 如果传入form dom创建的FormData对象会自动将 form 中的表单值也包含进去,包括文件内容也会被编码之后包含进去。


const formData = new FormData(?form)


该对象上有很多操作键值对的方法


  • append。向formData对象中添加新的属性,该属性不存在覆盖操作,只会新增。


  • delete。删除一个键值对。


  • entries。返回所有键值对的iterator对象。


  • keys。返回一个包含所有键的iterator对象。


  • has。返回一个布尔值表明 FormData 对象是否包含某些键。


  • getAll。返回一个包含 FormData 对象中与给定键关联的所有值的数组。


  • get。返回在 FormData 对象中与给定键关联的第一个值。


  • set。给 FormData 设置属性值,如果FormData 对应的属性值存在则覆盖原值,否则新增一项属性值。


他只会覆盖第一个查到的属性,并且删除后面重复的属性的键值对。


  • values。返回包含所有值的iterator对象。


<form action="#" id="form">
    <input type="text" name="name" value="zh">
    <input type="password" name="pwd" value="a">
  </form>
  <script>
    const form = document.getElementById("form")
    const data = new FormData(form);
    data.append("name", "zh")
    // data.set("name", "llm")
    console.log(data.getAll("name")) // ["zh", "zh"]
    console.log([...data.entries()])
  </script>


xml对象中的upload.onprogress事件


如果你使用原生 XMLHttpRequest 发送请求的话,那么xml中有一个upload属性上面有个onprogress事件,可以实时获取我们上传的文件大小。


onprogress事件并不是只执行分块大小的次数,而是根据读取文件大小来确定执行多少次,直到文件全部上传完毕。


事件对象中记录了两个重要的值


  • loaded: 表示当前分片已加载的大小。


  • total:表示当前分片总大小。


网络异常,图片无法展示
|


网络异常,图片无法展示
|


测试大文件上传


前端上传的具体逻辑


  • 点击input监听change事件,获取file对象。


// 点击input上传文件
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    }


  • 将获取的file对象分片。调用slice方法。再加入数组之前,需要添加一些额外的属性,来为以后操作分片对象提供方便。


// 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    }, 
     // 点击上传
    async handleUpload() {
      if (!this.container.file) return;
      // 分割文件
      const fileChunkList = this.createFileChunk(this.container.file);
      // 将切片赋值给data保存。并加入一些其他属性
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        index,
        // 文件名 + 数组下标
        hash: this.container.file.name + "-" + index,
        percentage: 0,
      }));
      // 上传切片
      await this.uploadChunks();
    },
  }


  • 封装原生的xml对象发送请求


request({
      url,
      method = "post",
      data,
      headers = {},
      onProgress = (e) => e,
    }) {
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        // 获取文件的上传进度
        // onprogress事件并不是只执行分块大小的次数,而是根据读取文件大小来确定执行多少次,直到文件全部上传完毕
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach((key) =>
          xhr.setRequestHeader(key, headers[key])
        );
        xhr.send(data);
        xhr.onload = (e) => {
          // 在这里进行总的进度计算。
          let cur = 0;
          this.data.forEach((item) => {
            // 这里也可以测试时并行的,因为cur不是每次增加100
            cur += item.percentage;
            this.totalPercentage = (
              (cur / (this.data.length * 100)) *
              100
            ).toFixed(0);
          });
          resolve({
            data: e.target.response,
          });
        };
      });
    },
    // 上传切片
    async uploadChunks() {
      // 设置上传列表值,增加一些属性
      const requestList = this.data
        .map(({ chunk, hash, index }) => {
          // 创建表单键值对上传对象。
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          return { formData, index };
        })
        .map(({ formData, index }) =>
          this.request({
            url: "http://localhost:3000",
            data: formData,
            // 获取单个切片的值(内部有hash ,chunk)
            onProgress: this.createProgressHandler(this.data[index]),
          })
        );
      // 并发请求
      await Promise.all(requestList);
      // 合并切片
      await this.mergeRequest();
    },
    // 合并请求只需要传递一个文件名即可。
    async mergeRequest() {
      await this.request({
        url: "http://localhost:3000/merge",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          filename: this.container.file.name,
        }),
      });
    },


  • 如果需要统计每个分片上传的进度,我们可以使用xml.upload.onprogress事件来监听每次上传的文件大小,来计算。也就是上文提到的。


// 单个chunk上传的进度。如果是整个文件,我们只需要在xml中的load事件中计算进度即可。
    // 这个事件会被调用很多次,而不是只调用分片多少的次数。
    createProgressHandler(item) {
      return (e) => {
        // e是onprogress事件对象。 loaded表示当前分片已加载的大小,total表示当前分片大小
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    }


  • 文件上传的总进度计算方式


第一种,直接在onload事件中计算,因为每个分片上传完毕,都会触发onload事件。


计算方法就是,每个分片的上传进度都是100,全部分片 * 100,然后遍历累计每个分片的percentage相除即可。


xhr.onload = (e) => {
      // 在这里进行总的进度计算。
      let cur = 0;
      this.data.forEach((item) => {
        // 这里也可以测试时并行的,因为cur不是每次增加100
        cur += item.percentage;
        this.totalPercentage = (
          (cur / (this.data.length * 100)) *
          100
        ).toFixed(0);
      });
    };


第二种,由于我们通过onprogress事件,实时计算每个分片的precentage上传进度,所以可以直接计算。


// 通过上传的进度可知,我们上传文件的时候,他是并行的。
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      // 如果没有发送的他们的percentage还是0
      const loaded = this.data
        .map((item) => item.chunk.size * (item.percentage / 100))
        .reduce((acc, cur) => acc + cur);
      console.log(
        "====================已加载的, 文件总大小, 比例",
        loaded,
        this.container.file.size
      );
      return parseInt(((loaded / this.container.file.size) * 100).toFixed(2));
    },


前端完整代码


<template>
  <div>
    <input type="file" @change="handleFileChange" />
    <el-button @click="handleUpload">上传</el-button>
    <h1>总进度条</h1>
    <el-progress :percentage="uploadPercentage"></el-progress>
    <!-- <el-progress :percentage="totalPercentage"></el-progress> -->
    <h1>每个chunk的进度条</h1>
    <el-progress
      v-for="item in data"
      :key="item.hash"
      :percentage="item.percentage"
    >
    </el-progress>
  </div>
</template>
<script>
// 切片大小
// the chunk size
const SIZE = 10 * 1024 * 1024;
let c = 0;
export default {
  data: () => ({
    container: {
      file: null,
    },
    // 放置若干个切片
    data: [],
    totalPercentage: 0,
  }),
  computed: {
    // 通过上传的进度可知,我们上传文件的时候,他是并行的。
    uploadPercentage() {
      if (!this.container.file || !this.data.length) return 0;
      // 如果没有发送的他们的percentage还是0
      const loaded = this.data
        .map((item) => item.chunk.size * (item.percentage / 100))
        .reduce((acc, cur) => acc + cur);
      console.log(
        "====================已加载的, 文件总大小, 比例",
        loaded,
        this.container.file.size
      );
      return parseInt(((loaded / this.container.file.size) * 100).toFixed(2));
    },
  },
  methods: {
    request({
      url,
      method = "post",
      data,
      headers = {},
      onProgress = (e) => e,
    }) {
      return new Promise((resolve) => {
        const xhr = new XMLHttpRequest();
        // 获取文件的上传进度
        // onprogress事件并不是只执行分块大小的次数,而是根据读取文件大小来确定执行多少次,直到文件全部上传完毕
        xhr.upload.onprogress = onProgress;
        xhr.open(method, url);
        Object.keys(headers).forEach((key) =>
          xhr.setRequestHeader(key, headers[key])
        );
        xhr.send(data);
        xhr.onload = (e) => {
          // console.log("eee", e);
          // 在这里进行总的进度计算。
          let cur = 0;
          this.data.forEach((item) => {
            // 这里也可以测试时并行的,因为cur不是每次增加100
            cur += item.percentage;
            this.totalPercentage = (
              (cur / (this.data.length * 100)) *
              100
            ).toFixed(0);
          });
          resolve({
            data: e.target.response,
          });
        };
      });
    },
    // 点击input上传文件
    handleFileChange(e) {
      const [file] = e.target.files;
      if (!file) return;
      Object.assign(this.$data, this.$options.data());
      this.container.file = file;
    }, 
    // 生成文件切片
    createFileChunk(file, size = SIZE) {
      const fileChunkList = [];
      let cur = 0;
      while (cur < file.size) {
        fileChunkList.push({ file: file.slice(cur, cur + size) });
        cur += size;
      }
      return fileChunkList;
    }, 
    // 上传切片
    async uploadChunks() {
      const requestList = this.data
        .map(({ chunk, hash, index }) => {
          const formData = new FormData();
          formData.append("chunk", chunk);
          formData.append("hash", hash);
          formData.append("filename", this.container.file.name);
          return { formData, index };
        })
        .map(({ formData, index }) =>
          this.request({
            url: "http://localhost:3000",
            data: formData,
            // 获取单个切片的值(内部有hash ,chunk)
            onProgress: this.createProgressHandler(this.data[index]),
          })
        );
      // 并发请求
      await Promise.all(requestList);
      // 合并切片
      await this.mergeRequest();
    },
    // 单个chunk上传的进度。如果是整个文件,我们只需要在xml中的load事件中计算进度即可。
    // 这个事件会被调用很多次,而不是只调用分片多少的次数。
    createProgressHandler(item) {
      return (e) => {
        c++;
        console.log("eeeeeeee", e, this.container.file.size, c);
        item.percentage = parseInt(String((e.loaded / e.total) * 100));
      };
    },
    // 合并请求只需要传递一个文件名即可。
    async mergeRequest() {
      await this.request({
        url: "http://localhost:3000/merge",
        headers: {
          "content-type": "application/json",
        },
        data: JSON.stringify({
          filename: this.container.file.name,
        }),
      });
    },
    // 点击上传
    async handleUpload() {
      if (!this.container.file) return;
      // 分割文件
      const fileChunkList = this.createFileChunk(this.container.file);
      // 将切片赋值给data保存。并加入一些其他属性
      this.data = fileChunkList.map(({ file }, index) => ({
        chunk: file,
        index,
        // 文件名 + 数组下标
        hash: this.container.file.name + "-" + index,
        percentage: 0,
      }));
      // 上传切片
      await this.uploadChunks();
    },
  },
};
</script>


后端完整代码


const http = require("http");
const path = require("path");
const fse = require("fs-extra");
const multiparty = require("multiparty");
const server = http.createServer();
 // 大文件存储目录
const UPLOAD_DIR = path.resolve(__dirname, "./target");
const resolvePost = req =>
   new Promise(resolve => {
     let chunk = "";
     req.on("data", data => {
       chunk += data;
     });
    req.on("end", () => {
       resolve(JSON.parse(chunk));
     });
});
 // 写入文件流
const pipeStream = (path, writeStream) =>
  new Promise(resolve => {
    const readStream = fse.createReadStream(path);
    readStream.on("end", () => {
      fse.unlinkSync(path);
      resolve();
    });
    readStream.pipe(writeStream);
});
// 合并切片
const mergeFileChunk = async (filePath, filename, size = 10 * 1024 * 1024) => {
  const chunkDir = path.resolve(UPLOAD_DIR, 'chunkDir' + filename);
  const chunkPaths = await fse.readdir(chunkDir);
   // 根据切片下标进行排序
   // 否则直接读取目录的获得的顺序会错乱
  chunkPaths.sort((a, b) => a.split("-")[1] - b.split("-")[1]);
   // 并发写入文件
  await Promise.all(
    chunkPaths.map((chunkPath, index) =>
      pipeStream(
        path.resolve(chunkDir, chunkPath),
         // 根据 size 在指定位置创建可写流
        fse.createWriteStream(filePath, {
           start: index * size,
        })
      )
    )
  ).catch(err => {
    console.log("=======err", err)
  });
  // 合并后删除保存切片的目录
  fse.rmdirSync(chunkDir);
};
server.on("request", async (req, res) => {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Headers", "*");
  if (req.method === "OPTIONS") {
     res.status = 200;
     res.end();
     return;
  }
  const multipart = new multiparty.Form();
  multipart.parse(req, async (err, fields, files) => {
     if (err) {
       return;
     }
     const [chunk] = files.chunk;
     const [hash] = fields.hash;
     const [filename] = fields.filename;
     // 创建临时文件夹用于临时存储 chunk
     // 添加 chunkDir 前缀与文件名做区分
     const chunkDir = path.resolve(UPLOAD_DIR, 'chunkDir' + filename);
     if (!fse.existsSync(chunkDir)) {
       await fse.mkdirs(chunkDir);
     }
    // fs-extra 的 rename 方法 windows 平台会有权限问题
     // @see https://github.com/meteor/meteor/issues/7852#issuecomment-255767835
     await fse.move(chunk.path, `${chunkDir}/${hash}`);
     res.end("received file chunk");
  });
  if (req.url === "/merge") {
    const data = await resolvePost(req);
    const { filename,size } = data;
    const filePath = path.resolve(UPLOAD_DIR, `${filename}`);
    await mergeFileChunk(filePath, filename);
    res.end(
      JSON.stringify({
        code: 0,
        message: "file merged success"
      })
    );
  }
});
server.listen(3000, () => console.log("listening port 3000"));


上面部分都是参考这篇文章的内容,具体请看这里


通过vue定义一个切片上传的组件


下面这个组件是公司自己封装的一个组件,但是需要后端配合。他的思路就是一边切片一边上传。


<template>
  <div class="upload-continue-box">
    <!-- 上传按钮 -->
    <el-col v-bind:span="8">
      <input
        id="bag"
        type="file"
        @change="handleFileChange"
        class="upload-continue-btn file-btn"
      />
      <input type="button" :value="btnText" class="upload-continue-btn" />
    </el-col>
    <!-- 文件和进度条 -->
    <el-col v-bind:span="8">
      <div class="filename-progress-box" v-if="fileProgressVisible">
        <div id="bagStop">{{ fileName }}</div>
        <el-progress size="small" :percentage="percent" />
      </div>
    </el-col>
  </div>
</template>
<script>
    var bagFile = document.getElementById('bag')
    var bagReader = null //读取操作对象
    var bagStep = 1024 * 1024 * 3.5 //每次读取文件大小
    var bagCuLoaded = 0 //当前已经读取总数
    var bagSession = null //当前读取的文件对象
    var bagEnableRead = true //标识是否可以读取文件
    var bagNum = 0
    var bagTnum = 0
    var bagFileresult = ''
    export default {
      data() {
        return {
          percent: 0,
          fileName: '',
        }
      },
      computed: {
        action() {
            return ""
        },
      },
      methods: {
        handleClose() {
          this.messageVisible = false
        },
        handleFileChange(e) {
          let _this = this
          const [file] = e.target.files
          if (!file) return
          if (
            file.name.toLowerCase().split('.').splice(-1)[0] != 'apk' &&
            file.name.toLowerCase().split('.').splice(-1)[0] != 'zip' &&
            file.name.toLowerCase().split('.').splice(-1)[0] != 'aab'
          ) {
            _this.$message.error('只能上传apk,zip,aab格式')
            bagFile.value = null
            return
          }
          bagCuLoaded = 0
          //获取文件对象
          bagSession = file
          var total = bagSession.size
          if (total > 0) {
            bagTnum = total
            var startTime = new Date()
            bagReader = new FileReader()
            //读取一段成功
            bagReader.onload = function (e) {
              //处理读取的结果
              var result = bagReader.result
              var loaded = e.loaded
              bagNum = loaded
              if (bagEnableRead == false) return false
              //将分段数据上传到服务器
              _this.uploadFile(result, bagCuLoaded, function () {
                //如果没有读完,继续
                bagCuLoaded += loaded
                if (bagCuLoaded < total) {
                  _this.bagReadBlob(bagCuLoaded)
                } else {
                  if (JSON.parse(bagFileresult).resultInfo != total) {
                    // _this.$message.error('上传文件长度不一致,请重新上传!')
                  }
                  bagCuLoaded = total
                }
                let _percent = (bagCuLoaded / total) * 100
                _this.percent = Math.trunc(_percent)
                _this.fileName = bagSession.name
                _this.fileProgressVisible = true
                if (_this.percent == 100) {
                  if (!_this.manual) {
                    _this.$emit('getGameVersionListById', true)
                  } else {
                    _this.$emit(
                      'uploadFileToNetDisc',
                      _this.toNetDiscFileName,
                      _this.toNetDiscFileUrl
                    )
                  }
                  document.getElementById('bag').value = ''
                }
              })
            }
            //开始读取
            _this.bagReadBlob(0)
          }
        },
        uploadFile(result, startIndex, onSuccess) {
          var _this = this
          var isend = ''
          var blob = new Blob([result])
          //提交到服务器
          var fd = new FormData()
          fd.append('appId', _this.currentGame.appId)
          fd.append('file', blob)
          fd.append('filename', bagSession.name)
          fd.append('manual', _this.manual)
          fd.append('loaded', startIndex > 0 ? 1 : startIndex)
          if (bagCuLoaded + bagNum >= bagTnum) {
            fd.append('isend', 'true')
          } else {
            fd.append('isend', 'false')
          }
          var xhr = new XMLHttpRequest()
          xhr.open('post', _this.action, true)
          xhr.setRequestHeader(
            Object.keys(_this.headers)[0],
            _this.headers[Object.keys(_this.headers)[0]]
          )
          xhr.withCredentials = true
          xhr.onreadystatechange = function () {
            if (xhr.readyState == 4 && xhr.status == 200) {
              var response = JSON.parse(xhr.response)
              if (response.resultCode == 1) {
                bagFileresult = xhr.responseText
                console.log(bagFileresult)
                onSuccess()
              }
            }
          }
          //开始发送
          xhr.send(fd)
        },
        //指定开始位置,分块读取文件
        bagReadBlob(start) {
          var blob = bagSession.slice(start, start + bagStep)
          bagReader.readAsArrayBuffer(blob)
        },
        //中止
        bagStop() {
          if (bagSession != null) {
            bagEnableRead = false
            bagReader.abort()
          }
        },
        //继续
        bagContainue() {
          if (bagSession != null) {
            bagEnableRead = true
            _this.bagReadBlob(bagCuLoaded)
          }
        },
      },
    }
    </script>
    <style lang="scss">
    .upload-continue-box {
      .upload-continue-btn {
        display: block;
        width: 120px;
        background: #539fff;
        height: 40px;
        text-align: center;
        line-height: 40px;
        border-radius: 2px;
        color: #ffffff;
        font-size: 14px;
        cursor: pointer;
        border: none;
        outline: none;
      }
      .file-btn {
        position: absolute;
        z-index: 200;
        opacity: 0;
        display: block;
        // width: 58px;
        margin-left: 0px;
      }
      .filename-progress-box #bagStop {
        width: 300px;
        height: 13px;
        font-size: 12px;
        line-height: 13px;
        color: #606266;
        background: url('../../../assets/images/packCenter/text.png') no-repeat;
        padding-left: 20px;
        overflow: hidden;
        text-overflow: ellipsis;
      }
    }
    </style>


但是需要后端配合才能完成。


相关文章
|
Java
Java实现图片转化为pdf
Java实现图片转化为pdf
490 0
|
Python
pdf文件合并批量合并,转Word--python一招教会你
pdf文件合并批量合并,转Word--python一招教会你
178 0
|
2月前
|
人工智能 计算机视觉 Python
ChatGPT编程省钱、方便小示例——实现PDF转成PNG文件
ChatGPT编程省钱、方便小示例——实现PDF转成PNG文件
37 1
|
2月前
|
IDE 开发工具 iOS开发
Python编程案例:查找指定文件大小的文件并输出路径
Python编程案例:查找指定文件大小的文件并输出路径
28 3
|
7月前
分享:批量多目录图片如何转换PDF,一次性转换多级目录批量的PDF的转换,合并,输出另存等问题,图片转PDF文件,批量图片转PDF文件,多级目录的图片转PDF文件,并且保存到不同的地方,全部搞定
本文介绍了如何高效地将图片转换为PDF,包括单张、多张及多级目录下的图片转换和合并。提供了软件下载链接(百度网盘、腾讯云盘),软件操作简便,支持保存原目录或自定义新目录。转换选项包括单个文件、多个文件夹单独转换以及合并转换。用户可通过双击路径访问源图片和转换结果。该工具特别解决了多级目录图片批量转换的难题,实现保存地址的自由设定,满足不同业务需求。
453 0
|
7月前
|
弹性计算 运维 Shell
|
7月前
|
数据安全/隐私保护 Python Windows
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)
137 0
|
7月前
|
数据安全/隐私保护 Python
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)(下)
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)
84 1
|
7月前
|
数据安全/隐私保护 Python Windows
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)(上)
Python办公自动化【Word转换PDF、PDF读取内容、PDF合并文件、PDF拆分文件、PDF加密文件、PPT基本操作-增加幻灯片、增加内容】(六)-全面详解(学习总结---从入门到深化)
124 0