Repository: wuwhs/js-image-compressor Branch: master Commit: 8d2a8195183d Files: 26 Total size: 99.4 KB Directory structure: gitextract_663mm2_7/ ├── .gitignore ├── README-CN.md ├── README.md ├── demo/ │ ├── default/ │ │ ├── index.html │ │ └── main.js │ ├── grayscale/ │ │ ├── index.html │ │ └── main.js │ ├── index.css │ ├── simple/ │ │ ├── index.html │ │ └── main.js │ └── watermark/ │ ├── index.html │ └── main.js ├── dist/ │ └── image-compressor.js ├── examples/ │ ├── default/ │ │ ├── index.html │ │ └── main.js │ ├── grayscale/ │ │ ├── index.html │ │ └── main.js │ ├── index.css │ ├── simple/ │ │ ├── index.html │ │ └── main.js │ └── watermark/ │ ├── index.html │ └── main.js ├── index.js ├── package.json ├── src/ │ └── index.js └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .DS_Store node_modules # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: README-CN.md ================================================ ## 简介 `js-image-compressor` 是一个实现轻量级图片压缩的 `javascript` 库,压缩后仅有 `5kb`,在前端页面即可实现对图片的压缩。在提供基本图片压缩功能同时,还暴露出图片处理相关公用方法,以及进行边界情况处理: - 可以对待转化图片大小设置一定的阈值,使得图片转化成 `png` 格式在不理想情况下不至于过大,同时大于这个阈值则可以自动转化成 `jpeg` 格式,实现更优压缩; - 可以限制输出图片宽高大小,从而防止意外情况发生,比如压缩运算过大使得浏览器奔溃; - 默认对 `png` 输出图片添加透明底色,其他格式设为白色,避免“黑屏”发生; - 读取 `jpeg` 格式图片的 `EXIF` 信息,矫正图片方位; - 提供一些图片处理的常用工具函数(`image2Canvas`、`canvas2Blob` 和 `canvas2DataUrl` 等),用户还可以自定义图片输出的样式特征(比如可以灰度处理、加水印)。 文档语言: - [英文](./README.md) - [中文](./README-CN.md) ## 使用 ### 安装引入 你可以通过npm去安装依赖: ```js npm install js-image-compressor --save-dev ``` 也可以在下载后,在 `dist` 目录下找到 `image-compress.min.js` 文件在页面中通过 `script` 引入: ```html ``` ### 简单使用 你可以只传入待压缩图片对象,其他参数都是非必须的,插件按照默认参数自动完成图片压缩处理。不过这样输出的压缩图片符合以下特征: - 默认按照 `0.8` 压缩率配置; - 输出图片宽/高维持源图片宽/高; - 一般的,输出图片格式保持源图片格式; - 当 `png` 图片的 `size` 大于 `2m` 时,默认转化成 `jpeg` 格式图片; - 给 `png` 图片填充透明色; - 当输出图片 `size` 大于源图片时,将源图片当作输出图片返回; - `jpeg` 格式图片,矫正翻转/旋转方向; 如果这些默认配置不能满足你的需求,可能需要其他参数配置。以下是一个简单使用配置: ```js var options = { file: file, // 压缩前回调 beforeCompress: function (result) { console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); }, // 压缩成功回调 success: function (result) { console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); } }; new ImageCompressor(options); ``` 其中,钩子函数 `beforeCompress` 发生在读取图片之后,创建画布之前;钩子函数 `success` 函数发生在压缩完成生成图片之后。它们回调参数 `result` 是整合来尺寸、图片类型和大小等相关信息的 `blob` 对象。 ### 标准使用 在标准使用中,我们可以根据自身需求自定义配置压缩比(`quality`)、输出图片类型(`mimeType`)、宽(`width`)、高(`height`)、最大宽(`maxWidth`)、最大高(`maxHeight`)、最小宽(`minWidth`)、最大高(`minHeight`)、png转jpeg阈值(`convertSize`)、是否矫正jpeg方向(`redressOrientation`)和是否宽松模式(`loose`)。 - 是否矫正jpeg方向(`redressOrientation`),`jpeg` 格式图片在某些iOS浏览器会按其方向呈现图像,这个选项可以控制恢复初始方向,默认为 `true`; - 是否宽松模式(`loose`)、的意思是控制当压缩的图片 `size` 大于源图片,输出源图片,否则输出压缩后图片,默认是 `true`。 以下是标准配置: ```js var options = { file: file, quality: 0.6, mimeType: 'image/jpeg', maxWidth: 2000, maxHeight: 2000, width: 1000, height: 1000, minWidth: 500, minHeight: 500, convertSize: Infinity, loose: true, redressOrientation: true, // 压缩前回调 beforeCompress: function (result) { console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); }, // 压缩成功回调 success: function (result) { console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); }, // 发生错误 error: function (msg) { console.error(msg); } }; new ImageCompressor(options); ``` `error` 钩子函数是图片压缩过程中错误回调,没有这个回调错误则会在插件中 `throw new Error(msg)` 形式抛出。 ### 其他钩子函数 在压缩输出图片之前,我们还可以对画布进行一些自定义处理,融入元素。 以下是对图片进行灰度和加水印处理: ```js var options = { file: file, // 图片绘画前 beforeDraw: function (ctx) { vm.btnText = '准备绘图...'; console.log('准备绘图...'); ctx.filter = 'grayscale(100%)'; }, // 图片绘画后 afterDraw: function (ctx, canvas) { ctx.restore(); vm.btnText = '绘图完成...'; console.log('绘图完成...'); ctx.fillStyle = '#fff'; ctx.font = (canvas.width * 0.1) + 'px microsoft yahei'; ctx.fillText(vm.watermarkText, 10, canvas.height - 20); }, }; new ImageCompressor(options); ``` `beforeDraw` 是在画布创建后,图片绘画前的钩子函数,`afterDraw` 是在图绘画后的钩子函数。 ### 工具函数 下图归纳了 `js-image-compressor` 插件从用户图片通过 `input` 的 `file` 本地上传到对图片压缩的详细过程,同时暴露出这些工具方法供用户使用。 ![js-image-compressor](./relation-chart.jpg) ================================================ FILE: README.md ================================================ ## Introduction `js-image-compressor` is a `javascript` library that implements lightweight image compression. After compression, it is only `5kb`, and the image can be compressed on the front-end page. While providing basic image compression functions, it also exposes related public methods of image processing, as well as border case processing: - A certain threshold can be set for the size of the converted image, so that the image converted to `png` format will not be too large under undesirable conditions, and at the same time larger than this threshold, it can be automatically converted to `jpeg` format for better compression; - You can limit the width and height of the output image to prevent accidents, such as excessive compression operations that cause the browser to crash; - By default, a transparent background color is added to the output image of `png`, and other formats are set to white to avoid "black screen"; - Read the `EXIF` information of `jpeg` format pictures, and correct the picture orientation; - Provide some common tool functions for image processing (`image2Canvas`, `canvas2Blob` and `canvas2DataUrl`, etc.), and users can also customize the style features of image output (for example, grayscale processing, watermarking). Document language: - [English](./README.md) - [Chinese](./README-CN.md) ## Use ### Installation and introduction You can install dependencies through npm: ```js npm install js-image-compressor --save-dev ``` You can also find the file `image-compress.min.js` in the `dist` directory after downloading and import it through `script` on the page: ```html ``` ### Simple to use You can only pass in the image object to be compressed, other parameters are optional, and the plug-in automatically completes the image compression processing according to the default parameters. However, the compressed image output in this way meets the following characteristics: - The default configuration is based on `0.8` compression ratio; - Output picture width/height maintains the source picture width/height; - Generally, the output image format keeps the original image format; - When the `size` of the `png` image is greater than `2m`, it will be converted into a `jpeg` format image by default; - Fill the `png` picture with a transparent color; - When the output picture `size` is larger than the source picture, the source picture will be returned as the output picture; - `jpeg` format picture, correct the flip/rotation direction; If these default configurations cannot meet your needs, other parameter configurations may be required. The following is a simple configuration: ```js var options = { file: file, // Callback before compression beforeCompress: function (result) { console.log('Image size before compression:', result.size); console.log('mime type:', result.type); }, // Compression success callback success: function (result) { console.log('result:', result) console.log('Image size after compression:', result.size); console.log('mime type:', result.type); console.log('Actual compression ratio:', ((file.size-result.size) / file.size * 100).toFixed(2) +'%'); } }; new ImageCompressor(options); ``` Among them, the hook function `beforeCompress` occurs after the image is read and before the canvas is created; the hook function `success` function occurs after the compression is completed to generate the image. Their callback parameter `result` is a `blob` object that integrates relevant information such as size, picture type and size. ### Standard use In standard use, we can customize the compression ratio (`quality`), output image type (`mimeType`), width (`width`), height (`height`), and maximum width (`maxWidth`) according to our own needs. ), maximum height (`maxHeight`), minimum width (`minWidth`), maximum height (`minHeight`), png to jpeg threshold (`convertSize`), whether to correct the jpeg direction (`redressOrientation`) and whether the loose mode ( `loose`). - Whether to correct the jpeg orientation (`redressOrientation`), the `jpeg` format image will be presented according to its orientation in some iOS browsers, this option can control the restoration of the initial orientation, the default is `true`; - Whether it is loose mode (`loose`), which means to control when the compressed image `size` is larger than the source image, output the source image, otherwise output the compressed image, the default is `true`. The following is the standard configuration: ```js var options = { file: file, quality: 0.6, mimeType:'image/jpeg', maxWidth: 2000, maxHeight: 2000, width: 1000, height: 1000, minWidth: 500, minHeight: 500, convertSize: Infinity, loose: true, redressOrientation: true, // Callback before compression beforeCompress: function (result) { console.log('Image size before compression:', result.size); console.log('mime type:', result.type); }, // Compression success callback success: function (result) { console.log('Image size after compression:', result.size); console.log('mime type:', result.type); console.log('Actual compression ratio:', ((file.size-result.size) / file.size * 100).toFixed(2) +'%'); }, // An error occurred error: function (msg) { console.error(msg); } }; new ImageCompressor(options); ``` The `error` hook function is an error callback during the image compression process. Without this callback error, it will be thrown in the form of `throw new Error(msg)` in the plugin. ### Other hook functions Before compressing the output image, we can also customize the canvas to incorporate elements. The following is the grayscale and watermark processing of the picture: ```js var options = { file: file, // Before picture painting beforeDraw: function (ctx) { vm.btnText ='Ready to draw...'; console.log('Ready to draw...'); ctx.filter ='grayscale(100%)'; }, // After the picture is painted afterDraw: function (ctx, canvas) { ctx.restore(); vm.btnText ='Drawing completed...'; console.log('Drawing completed...'); ctx.fillStyle ='#fff'; ctx.font = (canvas.width * 0.1) +'px microsoft yahei'; ctx.fillText(vm.watermarkText, 10, canvas.height-20); }, }; new ImageCompressor(options); ``` `beforeDraw` is the hook function before the picture is drawn after the canvas is created, and `afterDraw` is the hook function after the picture is drawn. ### Tool functions The following figure summarizes the detailed process of the `js-image-compressor` plug-in from uploading user pictures locally through the `file` of `input` to image compression, and at the same time exposes these tools and methods for users to use. ![js-image-compressor](./relation-chart.jpg) ================================================ FILE: demo/default/index.html ================================================ js-image-compressor
拖入文件或

设置参数

mime 类型:

maxWidth:

maxHeight:

width:

height:

minWidth:

minHeigth:

png转jpeg阈值:

宽松模式:

矫正jpeg方向:

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: demo/default/main.js ================================================ var BTN_OK = '确定'; new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', mimeType: 'auto', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, maxWidth: 0, maxHeight: 0, width: 0, height: 0, minWidth: 0, minHeight: 0, convertSize: Infinity, loose: true, redressOrientation: true } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault(); ev.stopPropagation(); var dt = ev.dataTransfer; var file = dt.files[0]; this.file = file; this.compressImage(file); }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0]; this.file = file; this.compressImage(file); }, /** * 确定提交 */ submit: function () { this.compressImage(this.file); }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this; vm.btnText = '读取中...'; var options = { file: file, quality: this.quality, mimeType: this.mimeType, maxWidth: this.maxWidth, maxHeight: this.maxHeight, width: this.width, height: this.height, minWidth: this.minWidth, minHeight: this.minHeight, convertSize: this.convertSize, loose: this.loose, redressOrientation: this.redressOrientation, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...'; vm.originImgWidth = result.width; vm.originImgHeight = result.height; vm.originSize = result.size; vm.originMimeType = result.type; console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url; }) }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK; vm.imgName = result.name; console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); vm.outputImgWidth = result.width; vm.outputImgHeight = result.height; vm.outputSize = result.size; vm.outputMimeType = result.type; vm.compressRatio = ((file.size - result.size) / file.size * 100).toFixed(2) + '%'; // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url; }) // 上传到远程服务器 // util.upload('/upload.png', result); }, // 发生错误 error: function (msg) { vm.btnText = BTN_OK; console.error(msg); } }; new ImageCompressor(options); } } }) ================================================ FILE: demo/grayscale/index.html ================================================ grayscale demo
拖入文件或

设置参数

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: demo/grayscale/main.js ================================================ var BTN_OK = '确定'; new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6 } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault(); ev.stopPropagation(); var dt = ev.dataTransfer; var file = dt.files[0]; this.file = file; this.compressImage(file); }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0]; this.file = file; this.compressImage(file); }, /** * 确定提交 */ submit: function () { this.compressImage(this.file); }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this; vm.btnText = '读取中...'; var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...'; vm.imgName = result.name; vm.originImgWidth = result.width; vm.originImgHeight = result.height; vm.originSize = result.size; vm.originMimeType = result.type; console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url; }) }, // 图片绘画前 beforeDraw: function (ctx) { vm.btnText = '准备绘图...'; console.log('准备绘图...'); ctx.filter = 'grayscale(100%)'; }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK; console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); vm.outputImgWidth = result.width; vm.outputImgHeight = result.height; vm.outputSize = result.size; vm.outputMimeType = result.type; vm.compressRatio = ((file.size - result.size) / file.size * 100).toFixed(2) + '%'; // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url; }) // 上传到远程服务器 // util.upload('/upload.png', result); } }; new ImageCompressor(options); } } }) ================================================ FILE: demo/index.css ================================================ .row { display: flex; border-bottom: 1px dashed #999; } .column { padding: 20px; } .upload, .origin, .output { width: 300px; max-height: 300px; overflow: hidden; text-align: center; } .origin img, .output img { max-width: 100%; max-height: 300px; vertical-align: middle; } .drop-content { display: flex; align-items: center; justify-content: center; border: 1px dotted #999; border-radius: 4px; min-height: 100px; height: 100%; } .drop-content label { color: #3db8fd; } .drop-content label:hover { cursor: pointer; } #file-elem { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); } #file-elem:focus+label { outline: thin dotted; } #file-elem:focus-within+label { outline: thin dotted; } ================================================ FILE: demo/simple/index.html ================================================ simple demo
拖入文件或

设置参数

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: demo/simple/main.js ================================================ var BTN_OK = '确定'; new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6 } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault(); ev.stopPropagation(); var dt = ev.dataTransfer; var file = dt.files[0]; this.file = file; this.compressImage(file); }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0]; this.file = file; this.compressImage(file); }, /** * 确定提交 */ submit: function () { this.compressImage(this.file); }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this; vm.btnText = '读取中...'; var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...'; vm.imgName = result.name; vm.originImgWidth = result.width; vm.originImgHeight = result.height; vm.originSize = result.size; vm.originMimeType = result.type; console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url; }) }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK; console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); vm.outputImgWidth = result.width; vm.outputImgHeight = result.height; vm.outputSize = result.size; vm.outputMimeType = result.type; vm.compressRatio = ((file.size - result.size) / file.size * 100).toFixed(2) + '%'; // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url; }) // 上传到远程服务器 // util.upload('/upload.png', result); } }; new ImageCompressor(options); } } }) ================================================ FILE: demo/watermark/index.html ================================================ watermark demo
拖入文件或

设置参数

压缩率:

水印:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: demo/watermark/main.js ================================================ var BTN_OK = '确定'; new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, watermarkText: 'watermark' } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault(); ev.stopPropagation(); }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault(); ev.stopPropagation(); var dt = ev.dataTransfer; var file = dt.files[0]; this.file = file; this.compressImage(file); }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0]; this.file = file; this.compressImage(file); }, /** * 确定提交 */ submit: function () { this.compressImage(this.file); }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this; vm.btnText = '读取中...'; var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...'; vm.imgName = result.name; vm.originImgWidth = result.width; vm.originImgHeight = result.height; vm.originSize = result.size; vm.originMimeType = result.type; console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url; }) }, // 图片绘画后 afterDraw: function (ctx, canvas) { ctx.restore(); vm.btnText = '绘图完成...'; console.log('绘图完成...'); ctx.fillStyle = '#fff'; ctx.font = (canvas.width * 0.1) + 'px microsoft yahei'; ctx.fillText(vm.watermarkText, 10, canvas.height - 20); }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK; console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('实际压缩率: ', ((file.size - result.size) / file.size * 100).toFixed(2) + '%'); vm.outputImgWidth = result.width; vm.outputImgHeight = result.height; vm.outputSize = result.size; vm.outputMimeType = result.type; vm.compressRatio = ((file.size - result.size) / file.size * 100).toFixed(2) + '%'; // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url; }) // 上传到远程服务器 // util.upload('/upload.png', result); } }; new ImageCompressor(options); } } }) ================================================ FILE: dist/image-compressor.js ================================================ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["ImageCompressor"] = factory(); else root["ImageCompressor"] = factory(); })(window, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); /******/ } /******/ }; /******/ /******/ // define __esModule on exports /******/ __webpack_require__.r = function(exports) { /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); /******/ } /******/ Object.defineProperty(exports, '__esModule', { value: true }); /******/ }; /******/ /******/ // create a fake namespace object /******/ // mode & 1: value is a module id, require it /******/ // mode & 2: merge all properties of value into the ns /******/ // mode & 4: return value when already ns object /******/ // mode & 8|1: behave like require /******/ __webpack_require__.t = function(value, mode) { /******/ if(mode & 1) value = __webpack_require__(value); /******/ if(mode & 8) return value; /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; /******/ var ns = Object.create(null); /******/ __webpack_require__.r(ns); /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); /******/ return ns; /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var WIN = window var REGEXP_IMAGE_TYPE = /^image\// var REGEXP_EXTENSION = /\.\w+$/ var util = {} var defaultOptions = { file: null, quality: 0.8, convertSize: 2048000, loose: true, redressOrientation: true, } /** * 判断是否为函数 * @param {Any} value 任意值 * @returns {Boolean} 判断结果 */ var isFunc = function (value) { return typeof value === 'function' } /** * 判断是否为图片类型 * @param {String} value 类型字符串 * @returns {Boolean} 判断结果 */ var isImageType = function (value) { return REGEXP_IMAGE_TYPE.test(value) } /** * 图片类型转化为文件拓展名 * @param {String} value */ var imageTypeToExtension = function (value) { var extension = isImageType(value) ? value.substr(6) : '' if (extension === 'jpeg') { extension = 'jpg' } return '.' + extension } /** * 图片压缩构造函数 * @param {Object} options 相关参数 */ function ImageCompressor(options) { options = Object.assign({}, defaultOptions, options) this.options = options this.file = options.file this.image = null this.ParsedOrientationInfo = null this.init() } var _proto = ImageCompressor.prototype // WIN.ImageCompressor = ImageCompressor; /* harmony default export */ __webpack_exports__["default"] = (ImageCompressor); /** * 初始化 */ _proto.init = function () { var _this = this var file = this.file var options = this.options if (!file || !isImageType(file.type)) { _this.error('请上传图片文件!') return } if (!isImageType(options.mimeType)) { options.mimeType = file.type } util.file2Image( file, function (img) { if (isFunc(_this.beforeCompress)) { _this.image = img file.width = img.naturalWidth file.height = img.naturalHeight _this.beforeCompress(file) } if (file.type === 'image/jpeg' && options.redressOrientation) { _this.getParsedOrientationInfo(function (info) { _this.parsedOrientationInfo = info _this.rendCanvas() }) } else { _this.parsedOrientationInfo = { rotate: 0, scaleX: 1, scaleY: 1, } _this.rendCanvas() } }, _this.error ) } /** * `Canvas` 渲染模块 */ _proto.rendCanvas = function () { var _this = this var options = this.options var image = this.image var edge = this.getExpectedEdge() var dWidth = edge.dWidth var dHeight = edge.dHeight var width = edge.width var height = edge.height var canvas = util.image2Canvas( image, dWidth, dHeight, _this.beforeDraw.bind(_this), _this.afterDraw.bind(_this), width, height ) util.canvas2Blob( canvas, function (blob) { if (blob) { blob.width = canvas.width blob.height = canvas.height } _this.success(blob) }, options.quality, options.mimeType ) } /** * 压缩之前,读取图片之后钩子函数 */ _proto.beforeCompress = function () { if (isFunc(this.options.beforeCompress)) { this.options.beforeCompress(this.file) } } /** * 获取用户想要输出的边(宽高) */ _proto.getExpectedEdge = function () { var image = this.image var parsedOrientationInfo = this.parsedOrientationInfo var rotate = parsedOrientationInfo.rotate var options = this.options var naturalWidth = image.naturalWidth var naturalHeight = image.naturalHeight var is90DegreesRotated = Math.abs(rotate) % 180 === 90 var temp if (is90DegreesRotated) { temp = naturalHeight naturalHeight = naturalWidth naturalWidth = temp } var aspectRatio = naturalWidth / naturalHeight var maxWidth = Math.max(options.maxWidth, 0) || Infinity var maxHeight = Math.max(options.maxHeight, 0) || Infinity var minWidth = Math.max(options.minWidth, 0) || 0 var minHeight = Math.max(options.minHeight, 0) || 0 var width = Math.max(options.width, 0) || naturalWidth var height = Math.max(options.height, 0) || naturalHeight if (maxWidth < Infinity && maxHeight < Infinity) { if (maxHeight * aspectRatio > maxWidth) { maxHeight = maxWidth / aspectRatio } else { maxWidth = maxHeight * aspectRatio } } else if (maxWidth < Infinity) { maxHeight = maxWidth / aspectRatio } else if (maxHeight < Infinity) { maxWidth = maxHeight * aspectRatio } if (minWidth > 0 && minHeight > 0) { if (minHeight * aspectRatio > minWidth) { minHeight = minWidth / aspectRatio } else { minWidth = minHeight * aspectRatio } } else if (minWidth > 0) { minHeight = minWidth / aspectRatio } else if (minHeight > 0) { minWidth = minHeight * aspectRatio } if (height * aspectRatio > width) { height = width / aspectRatio } else { width = height * aspectRatio } width = Math.floor(Math.min(Math.max(width, minWidth), maxWidth)) height = Math.floor(Math.min(Math.max(height, minHeight), maxHeight)) var dWidth = width var dHeight = height if (is90DegreesRotated) { temp = dHeight dHeight = dWidth dWidth = temp } return { dWidth: dWidth, dHeight: dHeight, width: width, height: height, } } /** * 获取转化后的方向信息 * @param {Function} callback 回调函数 */ _proto.getParsedOrientationInfo = function (callback) { var _this = this this.getOrientation(function (orientation) { if (isFunc(callback)) { callback(_this.parseOrientation(orientation)) } }) } /** * 获取方向 * @param {Function} callback 回调函数 */ _proto.getOrientation = function (callback) { var _this = this util.file2ArrayBuffer(this.file, function (result) { if (isFunc(callback)) { callback(_this.resetAndGetOrientation(result)) } }) } /** * 从 `array buffer` 提取 `orientation` 值 * @param {ArrayBuffer} arrayBuffer - array buffer * @returns {number} `orientation` 值 */ _proto.resetAndGetOrientation = function (arrayBuffer) { var dataView = new DataView(arrayBuffer) var orientation // 当图像没有正确的 Exif 信息时忽略 range error try { var littleEndian var app1Start var ifdStart // JPEG 图片 (以 0xFFD8 开头) if (dataView.getUint8(0) === 0xff && dataView.getUint8(1) === 0xd8) { var length = dataView.byteLength var offset = 2 while (offset + 1 < length) { if ( dataView.getUint8(offset) === 0xff && dataView.getUint8(offset + 1) === 0xe1 ) { app1Start = offset break } offset += 1 } } if (app1Start) { var exifIDCode = app1Start + 4 var tiffOffset = app1Start + 10 if (util.getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { var endianness = dataView.getUint16(tiffOffset) littleEndian = endianness === 0x4949 if (littleEndian || endianness === 0x4d4d) { if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002a) { var firstIFDOffset = dataView.getUint32( tiffOffset + 4, littleEndian ) if (firstIFDOffset >= 0x00000008) { ifdStart = tiffOffset + firstIFDOffset } } } } } if (ifdStart) { var length = dataView.getUint16(ifdStart, littleEndian) var offset var i for (i = 0; i < length; i += 1) { offset = ifdStart + i * 12 + 2 if ( dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */ ) { // 8 是当前标签值的偏移 offset += 8 // 获取原始方向值 orientation = dataView.getUint16(offset, littleEndian) // 以其默认值覆盖方向 dataView.setUint16(offset, 1, littleEndian) break } } } } catch (e) { console.error(e) orientation = 1 } return orientation } /** * 逆向转化Exif获取到图片的方向信息 * @param {Number} orientation 方向标识 * @returns {Object} 转化结果 */ _proto.parseOrientation = function (orientation) { var rotate = 0 var scaleX = 1 var scaleY = 1 switch (orientation) { // 水平翻转 case 2: scaleX = -1 break // 向左旋转180° case 3: rotate = -180 break // 垂直翻转 case 4: scaleY = -1 break // 垂直翻转并且向右旋转90° case 5: rotate = 90 scaleY = -1 break // 向右旋转90° case 6: rotate = 90 break // 水平翻转并且向右旋转90° case 7: rotate = 90 scaleX = -1 break // 向左旋转90° case 8: rotate = -90 break default: break } return { rotate: rotate, scaleX: scaleX, scaleY: scaleY, } } /** * 画布上绘制图片前的一些操作:设置画布一些样式,支持用户自定义 * @param {CanvasRenderingContext2D} ctx Canvas 对象的上下文 * @param {HTMLCanvasElement} canvas Canvas 对象 */ _proto.beforeDraw = function (ctx, canvas) { var parsedOrientationInfo = this.parsedOrientationInfo var rotate = parsedOrientationInfo.rotate var scaleX = parsedOrientationInfo.scaleX var scaleY = parsedOrientationInfo.scaleY var file = this.file var options = this.options var fillStyle = 'transparent' var width = canvas.width var height = canvas.height // `png` 格式图片大小超过 `convertSize`, 转化成 `jpeg` 格式 if (file.size > options.convertSize && options.mimeType === 'image/png') { fillStyle = '#fff' options.mimeType = 'image/jpeg' } // 覆盖默认的黑色填充色 ctx.fillStyle = fillStyle ctx.fillRect(0, 0, width, height) // 用户自定义画布样式 if (isFunc(options.beforeDraw)) { options.beforeDraw.call(this, ctx, canvas) } ctx.save() switch (rotate) { case 90: ctx.translate(width, 0) break case -90: ctx.translate(0, height) break case -180: ctx.translate(width, height) break } ctx.rotate((rotate * Math.PI) / 180) ctx.scale(scaleX, scaleY) } /** * 画布上绘制图片后的一些操作:支持用户自定义 * @param {CanvasRenderingContext2D} ctx Canvas 对象的上下文 * @param {HTMLCanvasElement} canvas Canvas 对象 */ _proto.afterDraw = function (ctx, canvas) { var options = this.options // 用户自定义画布样式 if (isFunc(options.afterDraw)) { options.afterDraw.call(this, ctx, canvas) } } /** * 错误触发函数 * @param {String} msg 错误消息 */ _proto.error = function (msg) { var options = this.options if (isFunc(options.error)) { options.error.call(this, msg) } else { throw new Error(msg) } } /** * 成功触发函数 * @param {File|Blob} result `Blob` 对象 */ _proto.success = function (result) { var options = this.options var file = this.file var image = this.image var edge = this.getExpectedEdge() var naturalHeight = image.naturalHeight var naturalWidth = image.naturalWidth if (result && result.size) { // 在非宽松模式下,用户期待的输出宽高没有大于源图片的宽高情况下,输出文件大小大于源文件,返回源文件 if ( !options.loose && result.size > file.size && !(edge.width > naturalWidth || edge.height > naturalHeight) ) { console.warn('当前设置的是非宽松模式,压缩结果大于源图片,输出源图片') result = file } else { var date = new Date() result.lastModified = date.getTime() result.lastModifiedDate = date result.name = file.name // 文件 `name` 属性中的后缀转化成实际后缀 if (result.name && result.type !== file.type) { result.name = result.name.replace( REGEXP_EXTENSION, imageTypeToExtension(result.type) ) } } } else { // 在某些情况下压缩后文件为 `null`,返回源文件 console.warn('图片压缩出了点意外,输出源图片') result = file } if (isFunc(options.success)) { options.success.call(this, result) } } /** * 文件转化成 `data URL` 字符串 * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 取消回调函数 */ util.file2DataUrl = function (file, callback, error) { var reader = new FileReader() reader.onload = function () { callback(reader.result) } reader.onerror = function () { if (isFunc(error)) { error('读取文件失败!') } } reader.readAsDataURL(file) } /** * 文件转化成 `ArrayBuffer` * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 取消回调函数 */ util.file2ArrayBuffer = function (file, callback, error) { var reader = new FileReader() reader.onload = function (ev) { callback(ev.target.result) } reader.onerror = function () { if (isFunc(error)) { error('读取文件失败!') } } reader.readAsArrayBuffer(file) } /** * 从 data view 中的字符代码获取字符字符串 * @param {DataView} dataView - The data view for read. * @param {number} start - 起始位置 * @param {number} length - 读取长度 * @returns {string} 读取结果 */ util.getStringFromCharCode = function (dataView, start, length) { var str = '' var i length += start for (i = start; i < length; i += 1) { str += String.fromCharCode(dataView.getUint8(i)) } return str } /** * 文件转化成 `Image` 对象 * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 错误回调函数 */ util.file2Image = function (file, callback, error) { var image = new Image() var URL = WIN.URL || WIN.webkitURL if ( WIN.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WIN.navigator.userAgent) ) { // 修复IOS上webkit内核浏览器抛出错误 `The operation is insecure` 问题 image.crossOrigin = 'anonymous' } image.alt = file.name image.onerror = function () { if (isFunc(error)) { error('图片加载错误!') } } if (URL) { var url = URL.createObjectURL(file) image.onload = function () { callback(image) URL.revokeObjectURL(url) } image.src = url } else { this.file2DataUrl( file, function (dataUrl) { image.onload = function () { callback(image) } image.src = dataUrl }, error ) } } /** * `url` 转化成 `Image` 对象 * @param {File} url `url` * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.url2Image = function (url, callback, error) { var image = new Image() image.src = url image.onload = function () { callback(image) } image.onerror = function () { if (isFunc(error)) { error('图片加载错误!') } } } /** * `Image` 转化成 `Canvas` 对象 * @param {File} image `Image` 对象 * @param {Number} dWidth 目标宽度 * @param {Number} dHeight 目标高度 * @param {Function} beforeDraw 在图片绘画之前的回调函数 * @param {Function} afterDraw 在图片绘画之后的回调函数 * @param {Number} width 宽 * @param {Number} height 高 * @return {HTMLCanvasElement} `Canvas` 对象 */ util.image2Canvas = function ( image, dWidth, dHeight, beforeDraw, afterDraw, width, height ) { var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d') canvas.width = width || image.naturalWidth canvas.height = height || image.naturalHeight if (isFunc(beforeDraw)) { beforeDraw(ctx, canvas) } ctx.save() ctx.drawImage(image, 0, 0, dWidth, dHeight) ctx.restore() if (isFunc(afterDraw)) { afterDraw(ctx, canvas) } return canvas } /** * `Canvas` 转化成 `data URL` 对象 * @param {File} file `Canvas` 对象 * @param {Float} quality 输出质量比例 * @return {String} `data URL` 字符串 */ util.canvas2DataUrl = function (canvas, quality, type) { return canvas.toDataURL(type || 'image/jpeg', quality) } /** * `data URL` 转化成 `Image` 对象 * @param {File} dataUrl `data URL` 字符串 * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.dataUrl2Image = function (dataUrl, callback, error) { var image = new Image() image.onload = function () { callback(image) } image.error = function () { if (isFunc(error)) { error('图片加载错误!') } } image.src = dataUrl } /** * `data URL` 转化成 `Blob` 对象 * @param {File} dataUrl `data URL` 字符串 * @param {String} type `mime` * @return {Blob} `Blob` 对象 */ util.dataUrl2Blob = function (dataUrl, type) { var data = dataUrl.split(',')[1] var mimePattern = /^data:(.*?)(;base64)?,/ var mime = dataUrl.match(mimePattern)[1] var binStr = atob(data) var len = data.length var arr = new Uint8Array(len) for (var i = 0; i < len; i++) { arr[i] = binStr.charCodeAt(i) } return new Blob([arr], { type: type || mime }) } /** * `Blob` 对象转化成 `data URL` * @param {Blob} blob `Blob` 对象 * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.blob2DataUrl = function (blob, callback, error) { this.file2DataUrl(blob, callback, error) } /** * `Blob`对象 转化成 `Image` 对象 * @param {Blob} blob `Blob` 对象 * @param {Function} callback 成功回调函数 * @param {Function} callback 失败回调函数 */ util.blob2Image = function (blob, callback, error) { this.file2Image(blob, callback, error) } /** * `Canvas` 对象转化成 `Blob` 对象 * @param {HTMLCanvasElement} canvas `Canvas` 对象 * @param {Function} callback 回调函数 * @param {Float} quality 输出质量比例 * @param {String} type `mime` */ util.canvas2Blob = function (canvas, callback, quality, type) { var _this = this if (!HTMLCanvasElement.prototype.toBlob) { Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { value: function (callback, type, quality) { var dataUrl = this.toDataURL(type, quality) callback(_this.dataUrl2Blob(dataUrl)) }, }) } canvas.toBlob( function (blob) { callback(blob) }, type || 'image/jpeg', quality || 0.8 ) } /** * 文件上传 * @param {String} url 上传路径 * @param {File} file 文件对象 * @param {Function} callback 回调函数 */ util.upload = function (url, file, callback) { var xhr = new XMLHttpRequest() var fd = new FormData() fd.append('file', file) xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { // 上传成功 callback && callback(xhr.responseText) } else { throw new Error(xhr) } } xhr.open('POST', url, true) xhr.send(fd) } for (var key in util) { if (util.hasOwnProperty(key)) { ImageCompressor[key] = util[key] } } /***/ }) /******/ ])["default"]; }); ================================================ FILE: examples/default/index.html ================================================ js-image-compressor
拖入文件或

设置参数

mime 类型:

maxWidth:

maxHeight:

width:

height:

minWidth:

minHeigth:

png转jpeg阈值:

宽松模式:

矫正jpeg方向:

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: examples/default/main.js ================================================ var BTN_OK = '确定' new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', mimeType: 'auto', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, maxWidth: 0, maxHeight: 0, width: 0, height: 0, minWidth: 0, minHeight: 0, convertSize: Infinity, loose: true, redressOrientation: true, } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault() ev.stopPropagation() var dt = ev.dataTransfer var file = dt.files[0] this.file = file this.compressImage(file) }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0] this.file = file this.compressImage(file) }, /** * 确定提交 */ submit: function () { this.compressImage(this.file) }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this vm.btnText = '读取中...' var options = { file: file, quality: this.quality, mimeType: this.mimeType, maxWidth: this.maxWidth, maxHeight: this.maxHeight, width: this.width, height: this.height, minWidth: this.minWidth, minHeight: this.minHeight, convertSize: this.convertSize, loose: this.loose, redressOrientation: this.redressOrientation, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...' vm.originImgWidth = result.width vm.originImgHeight = result.height vm.originSize = result.size vm.originMimeType = result.type console.log('压缩之前图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url }) }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK vm.imgName = result.name console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) console.log( '实际压缩率: ', (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' ) vm.outputImgWidth = result.width vm.outputImgHeight = result.height vm.outputSize = result.size vm.outputMimeType = result.type vm.compressRatio = (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url }) // 上传到远程服务器 // util.upload('/upload.png', result); }, // 发生错误 error: function (msg) { vm.btnText = BTN_OK console.error(msg) }, } new ImageCompressor(options) }, }, }) ================================================ FILE: examples/grayscale/index.html ================================================ grayscale demo
拖入文件或

设置参数

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: examples/grayscale/main.js ================================================ var BTN_OK = '确定' new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault() ev.stopPropagation() var dt = ev.dataTransfer var file = dt.files[0] this.file = file this.compressImage(file) }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0] this.file = file this.compressImage(file) }, /** * 确定提交 */ submit: function () { this.compressImage(this.file) }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this vm.btnText = '读取中...' var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...' vm.imgName = result.name vm.originImgWidth = result.width vm.originImgHeight = result.height vm.originSize = result.size vm.originMimeType = result.type console.log('压缩之前图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url }) }, // 图片绘画前 beforeDraw: function (ctx) { vm.btnText = '准备绘图...' console.log('准备绘图...') ctx.filter = 'grayscale(100%)' }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) console.log( '实际压缩率: ', (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' ) vm.outputImgWidth = result.width vm.outputImgHeight = result.height vm.outputSize = result.size vm.outputMimeType = result.type vm.compressRatio = (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url }) // 上传到远程服务器 // util.upload('/upload.png', result); }, } new ImageCompressor(options) }, }, }) ================================================ FILE: examples/index.css ================================================ .row { display: flex; border-bottom: 1px dashed #999; } .column { padding: 20px; } .upload, .origin, .output { width: 300px; max-height: 300px; overflow: hidden; text-align: center; } .origin img, .output img { max-width: 100%; max-height: 300px; vertical-align: middle; } .drop-content { display: flex; align-items: center; justify-content: center; border: 1px dotted #999; border-radius: 4px; min-height: 100px; height: 100%; } .drop-content label { color: #3db8fd; } .drop-content label:hover { cursor: pointer; } #file-elem { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px, 1px, 1px, 1px); } #file-elem:focus+label { outline: thin dotted; } #file-elem:focus-within+label { outline: thin dotted; } ================================================ FILE: examples/simple/index.html ================================================ simple demo
拖入文件或

设置参数

压缩率:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: examples/simple/main.js ================================================ var BTN_OK = '确定' new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault() ev.stopPropagation() var dt = ev.dataTransfer var file = dt.files[0] this.file = file this.compressImage(file) }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0] this.file = file this.compressImage(file) }, /** * 确定提交 */ submit: function () { this.compressImage(this.file) }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this vm.btnText = '读取中...' var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...' vm.imgName = result.name vm.originImgWidth = result.width vm.originImgHeight = result.height vm.originSize = result.size vm.originMimeType = result.type console.log('压缩之前图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url }) }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) console.log( '实际压缩率: ', (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' ) vm.outputImgWidth = result.width vm.outputImgHeight = result.height vm.outputSize = result.size vm.outputMimeType = result.type vm.compressRatio = (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url }) // 上传到远程服务器 // util.upload('/upload.png', result); }, } new ImageCompressor(options) }, }, }) ================================================ FILE: examples/watermark/index.html ================================================ watermark demo
拖入文件或

设置参数

压缩率:

水印:

源图片信息

mime类型:{{originMimeType}}
尺寸:{{originImgWidth}} x {{originImgHeight}}
大小:{{originSize}}

压缩图片信息

mime类型:{{outputMimeType}}
尺寸:{{outputImgWidth}} x {{outputImgHeight}}
大小:{{outputSize}}
实际压缩率:{{compressRatio}}
下载
================================================ FILE: examples/watermark/main.js ================================================ var BTN_OK = '确定' new Vue({ el: '#app', data: function () { return { file: null, btnText: BTN_OK, imgName: '', originImgUrl: '', originMimeType: 'auto', originSize: 0, originImgWidth: 'auto', originImgHeight: 'auto', outputImgUrl: '', outputMimeType: 'auto', outputImgWidth: 'auto', outputImgHeight: 'auto', outputSize: 0, compressRatio: 0, quality: 0.6, watermarkText: 'watermark', } }, methods: { /** * 拖拽文件进入元素 * @param {Event} ev 事件 */ dragFileEnter: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 拖拽文件在元素上 * @param {Event} ev 事件 */ drageFileOver: function (ev) { ev.preventDefault() ev.stopPropagation() }, /** * 放开文件拖拽 * @param {Event} ev 事件 */ dropFile: function (ev) { ev.preventDefault() ev.stopPropagation() var dt = ev.dataTransfer var file = dt.files[0] this.file = file this.compressImage(file) }, /** * 上传文件改变事件 * @param {Event} ev 事件 */ inputChange: function (ev) { var file = ev.target.files[0] this.file = file this.compressImage(file) }, /** * 确定提交 */ submit: function () { this.compressImage(this.file) }, /** * 压缩图片 * @param {File} file `File` 对象 */ compressImage: function (file) { var vm = this vm.btnText = '读取中...' var options = { file: file, quality: this.quality, // 压缩前回调 beforeCompress: function (result) { vm.btnText = '处理中...' vm.imgName = result.name vm.originImgWidth = result.width vm.originImgHeight = result.height vm.originSize = result.size vm.originMimeType = result.type console.log('压缩之前图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) // 将上传图片在页面预览 ImageCompressor.file2DataUrl(result, function (url) { vm.originImgUrl = url }) }, // 图片绘画后 afterDraw: function (ctx, canvas) { ctx.restore() vm.btnText = '绘图完成...' console.log('绘图完成...') ctx.fillStyle = '#fff' ctx.font = canvas.width * 0.1 + 'px microsoft yahei' ctx.fillText(vm.watermarkText, 10, canvas.height - 20) }, // 压缩成功回调 success: function (result) { vm.btnText = BTN_OK console.log('result: ', result) console.log('压缩之后图片尺寸大小: ', result.size) console.log('mime 类型: ', result.type) console.log( '实际压缩率: ', (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' ) vm.outputImgWidth = result.width vm.outputImgHeight = result.height vm.outputSize = result.size vm.outputMimeType = result.type vm.compressRatio = (((file.size - result.size) / file.size) * 100).toFixed(2) + '%' // 生成压缩后图片在页面展示 ImageCompressor.file2DataUrl(result, function (url) { vm.outputImgUrl = url }) // 上传到远程服务器 // util.upload('/upload.png', result); }, } new ImageCompressor(options) }, }, }) ================================================ FILE: index.js ================================================ if (process.env.NODE_ENV === 'production') { module.exports = require('./dist/image-compressor.min.js') } else { module.exports = require('./dist/image-compressor.js') } ================================================ FILE: package.json ================================================ { "name": "js-image-compressor", "version": "2.0.0", "description": "A simple image compressor of javascript", "main": "index.js", "scripts": { "build": "webpack", "prepublish": "webpack", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [ "jsImageCompressor", "imageToCanvas", "toBlob" ], "author": "wuwhs", "license": "ISC", "devDependencies": { "terser-webpack-plugin": "^3.0.8", "webpack": "^4.44.0", "webpack-cli": "^3.3.12" }, "repository": { "type": "git", "url": "https://github.com/wuwhs/js-image-compressor" } } ================================================ FILE: src/index.js ================================================ var WIN = window var REGEXP_IMAGE_TYPE = /^image\// var REGEXP_EXTENSION = /\.\w+$/ var util = {} var defaultOptions = { file: null, quality: 0.8, convertSize: 2048000, loose: true, redressOrientation: true, } /** * 判断是否为函数 * @param {Any} value 任意值 * @returns {Boolean} 判断结果 */ var isFunc = function (value) { return typeof value === 'function' } /** * 判断是否为图片类型 * @param {String} value 类型字符串 * @returns {Boolean} 判断结果 */ var isImageType = function (value) { return REGEXP_IMAGE_TYPE.test(value) } /** * 图片类型转化为文件拓展名 * @param {String} value */ var imageTypeToExtension = function (value) { var extension = isImageType(value) ? value.substr(6) : '' if (extension === 'jpeg') { extension = 'jpg' } return '.' + extension } /** * 图片压缩构造函数 * @param {Object} options 相关参数 */ function ImageCompressor(options) { options = Object.assign({}, defaultOptions, options) this.options = options this.file = options.file this.image = null this.ParsedOrientationInfo = null this.init() } var _proto = ImageCompressor.prototype // WIN.ImageCompressor = ImageCompressor; export default ImageCompressor /** * 初始化 */ _proto.init = function () { var _this = this var file = this.file var options = this.options if (!file || !isImageType(file.type)) { _this.error('请上传图片文件!') return } if (!isImageType(options.mimeType)) { options.mimeType = file.type } util.file2Image( file, function (img) { if (isFunc(_this.beforeCompress)) { _this.image = img file.width = img.naturalWidth file.height = img.naturalHeight _this.beforeCompress(file) } if (file.type === 'image/jpeg' && options.redressOrientation) { _this.getParsedOrientationInfo(function (info) { _this.parsedOrientationInfo = info _this.rendCanvas() }) } else { _this.parsedOrientationInfo = { rotate: 0, scaleX: 1, scaleY: 1, } _this.rendCanvas() } }, _this.error ) } /** * `Canvas` 渲染模块 */ _proto.rendCanvas = function () { var _this = this var options = this.options var image = this.image var edge = this.getExpectedEdge() var dWidth = edge.dWidth var dHeight = edge.dHeight var width = edge.width var height = edge.height var canvas = util.image2Canvas( image, dWidth, dHeight, _this.beforeDraw.bind(_this), _this.afterDraw.bind(_this), width, height ) util.canvas2Blob( canvas, function (blob) { if (blob) { blob.width = canvas.width blob.height = canvas.height } _this.success(blob) }, options.quality, options.mimeType ) } /** * 压缩之前,读取图片之后钩子函数 */ _proto.beforeCompress = function () { if (isFunc(this.options.beforeCompress)) { this.options.beforeCompress(this.file) } } /** * 获取用户想要输出的边(宽高) */ _proto.getExpectedEdge = function () { var image = this.image var parsedOrientationInfo = this.parsedOrientationInfo var rotate = parsedOrientationInfo.rotate var options = this.options var naturalWidth = image.naturalWidth var naturalHeight = image.naturalHeight var is90DegreesRotated = Math.abs(rotate) % 180 === 90 var temp if (is90DegreesRotated) { temp = naturalHeight naturalHeight = naturalWidth naturalWidth = temp } var aspectRatio = naturalWidth / naturalHeight var maxWidth = Math.max(options.maxWidth, 0) || Infinity var maxHeight = Math.max(options.maxHeight, 0) || Infinity var minWidth = Math.max(options.minWidth, 0) || 0 var minHeight = Math.max(options.minHeight, 0) || 0 var width = Math.max(options.width, 0) || naturalWidth var height = Math.max(options.height, 0) || naturalHeight if (maxWidth < Infinity && maxHeight < Infinity) { if (maxHeight * aspectRatio > maxWidth) { maxHeight = maxWidth / aspectRatio } else { maxWidth = maxHeight * aspectRatio } } else if (maxWidth < Infinity) { maxHeight = maxWidth / aspectRatio } else if (maxHeight < Infinity) { maxWidth = maxHeight * aspectRatio } if (minWidth > 0 && minHeight > 0) { if (minHeight * aspectRatio > minWidth) { minHeight = minWidth / aspectRatio } else { minWidth = minHeight * aspectRatio } } else if (minWidth > 0) { minHeight = minWidth / aspectRatio } else if (minHeight > 0) { minWidth = minHeight * aspectRatio } if (height * aspectRatio > width) { height = width / aspectRatio } else { width = height * aspectRatio } width = Math.floor(Math.min(Math.max(width, minWidth), maxWidth)) height = Math.floor(Math.min(Math.max(height, minHeight), maxHeight)) var dWidth = width var dHeight = height if (is90DegreesRotated) { temp = dHeight dHeight = dWidth dWidth = temp } return { dWidth: dWidth, dHeight: dHeight, width: width, height: height, } } /** * 获取转化后的方向信息 * @param {Function} callback 回调函数 */ _proto.getParsedOrientationInfo = function (callback) { var _this = this this.getOrientation(function (orientation) { if (isFunc(callback)) { callback(_this.parseOrientation(orientation)) } }) } /** * 获取方向 * @param {Function} callback 回调函数 */ _proto.getOrientation = function (callback) { var _this = this util.file2ArrayBuffer(this.file, function (result) { if (isFunc(callback)) { callback(_this.resetAndGetOrientation(result)) } }) } /** * 从 `array buffer` 提取 `orientation` 值 * @param {ArrayBuffer} arrayBuffer - array buffer * @returns {number} `orientation` 值 */ _proto.resetAndGetOrientation = function (arrayBuffer) { var dataView = new DataView(arrayBuffer) var orientation // 当图像没有正确的 Exif 信息时忽略 range error try { var littleEndian var app1Start var ifdStart // JPEG 图片 (以 0xFFD8 开头) if (dataView.getUint8(0) === 0xff && dataView.getUint8(1) === 0xd8) { var length = dataView.byteLength var offset = 2 while (offset + 1 < length) { if ( dataView.getUint8(offset) === 0xff && dataView.getUint8(offset + 1) === 0xe1 ) { app1Start = offset break } offset += 1 } } if (app1Start) { var exifIDCode = app1Start + 4 var tiffOffset = app1Start + 10 if (util.getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { var endianness = dataView.getUint16(tiffOffset) littleEndian = endianness === 0x4949 if (littleEndian || endianness === 0x4d4d) { if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002a) { var firstIFDOffset = dataView.getUint32( tiffOffset + 4, littleEndian ) if (firstIFDOffset >= 0x00000008) { ifdStart = tiffOffset + firstIFDOffset } } } } } if (ifdStart) { var length = dataView.getUint16(ifdStart, littleEndian) var offset var i for (i = 0; i < length; i += 1) { offset = ifdStart + i * 12 + 2 if ( dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */ ) { // 8 是当前标签值的偏移 offset += 8 // 获取原始方向值 orientation = dataView.getUint16(offset, littleEndian) // 以其默认值覆盖方向 dataView.setUint16(offset, 1, littleEndian) break } } } } catch (e) { console.error(e) orientation = 1 } return orientation } /** * 逆向转化Exif获取到图片的方向信息 * @param {Number} orientation 方向标识 * @returns {Object} 转化结果 */ _proto.parseOrientation = function (orientation) { var rotate = 0 var scaleX = 1 var scaleY = 1 switch (orientation) { // 水平翻转 case 2: scaleX = -1 break // 向左旋转180° case 3: rotate = -180 break // 垂直翻转 case 4: scaleY = -1 break // 垂直翻转并且向右旋转90° case 5: rotate = 90 scaleY = -1 break // 向右旋转90° case 6: rotate = 90 break // 水平翻转并且向右旋转90° case 7: rotate = 90 scaleX = -1 break // 向左旋转90° case 8: rotate = -90 break default: break } return { rotate: rotate, scaleX: scaleX, scaleY: scaleY, } } /** * 画布上绘制图片前的一些操作:设置画布一些样式,支持用户自定义 * @param {CanvasRenderingContext2D} ctx Canvas 对象的上下文 * @param {HTMLCanvasElement} canvas Canvas 对象 */ _proto.beforeDraw = function (ctx, canvas) { var parsedOrientationInfo = this.parsedOrientationInfo var rotate = parsedOrientationInfo.rotate var scaleX = parsedOrientationInfo.scaleX var scaleY = parsedOrientationInfo.scaleY var file = this.file var options = this.options var fillStyle = 'transparent' var width = canvas.width var height = canvas.height // `png` 格式图片大小超过 `convertSize`, 转化成 `jpeg` 格式 if (file.size > options.convertSize && options.mimeType === 'image/png') { fillStyle = '#fff' options.mimeType = 'image/jpeg' } // 覆盖默认的黑色填充色 ctx.fillStyle = fillStyle ctx.fillRect(0, 0, width, height) // 用户自定义画布样式 if (isFunc(options.beforeDraw)) { options.beforeDraw.call(this, ctx, canvas) } ctx.save() switch (rotate) { case 90: ctx.translate(width, 0) break case -90: ctx.translate(0, height) break case -180: ctx.translate(width, height) break } ctx.rotate((rotate * Math.PI) / 180) ctx.scale(scaleX, scaleY) } /** * 画布上绘制图片后的一些操作:支持用户自定义 * @param {CanvasRenderingContext2D} ctx Canvas 对象的上下文 * @param {HTMLCanvasElement} canvas Canvas 对象 */ _proto.afterDraw = function (ctx, canvas) { var options = this.options // 用户自定义画布样式 if (isFunc(options.afterDraw)) { options.afterDraw.call(this, ctx, canvas) } } /** * 错误触发函数 * @param {String} msg 错误消息 */ _proto.error = function (msg) { var options = this.options if (isFunc(options.error)) { options.error.call(this, msg) } else { throw new Error(msg) } } /** * 成功触发函数 * @param {File|Blob} result `Blob` 对象 */ _proto.success = function (result) { var options = this.options var file = this.file var image = this.image var edge = this.getExpectedEdge() var naturalHeight = image.naturalHeight var naturalWidth = image.naturalWidth if (result && result.size) { // 在非宽松模式下,用户期待的输出宽高没有大于源图片的宽高情况下,输出文件大小大于源文件,返回源文件 if ( !options.loose && result.size > file.size && !(edge.width > naturalWidth || edge.height > naturalHeight) ) { console.warn('当前设置的是非宽松模式,压缩结果大于源图片,输出源图片') result = file } else { var date = new Date() result.lastModified = date.getTime() result.lastModifiedDate = date result.name = file.name // 文件 `name` 属性中的后缀转化成实际后缀 if (result.name && result.type !== file.type) { result.name = result.name.replace( REGEXP_EXTENSION, imageTypeToExtension(result.type) ) } } } else { // 在某些情况下压缩后文件为 `null`,返回源文件 console.warn('图片压缩出了点意外,输出源图片') result = file } if (isFunc(options.success)) { options.success.call(this, result) } } /** * 文件转化成 `data URL` 字符串 * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 取消回调函数 */ util.file2DataUrl = function (file, callback, error) { var reader = new FileReader() reader.onload = function () { callback(reader.result) } reader.onerror = function () { if (isFunc(error)) { error('读取文件失败!') } } reader.readAsDataURL(file) } /** * 文件转化成 `ArrayBuffer` * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 取消回调函数 */ util.file2ArrayBuffer = function (file, callback, error) { var reader = new FileReader() reader.onload = function (ev) { callback(ev.target.result) } reader.onerror = function () { if (isFunc(error)) { error('读取文件失败!') } } reader.readAsArrayBuffer(file) } /** * 从 data view 中的字符代码获取字符字符串 * @param {DataView} dataView - The data view for read. * @param {number} start - 起始位置 * @param {number} length - 读取长度 * @returns {string} 读取结果 */ util.getStringFromCharCode = function (dataView, start, length) { var str = '' var i length += start for (i = start; i < length; i += 1) { str += String.fromCharCode(dataView.getUint8(i)) } return str } /** * 文件转化成 `Image` 对象 * @param {File} file 文件对象 * @param {Function} callback 成功回调函数 * @param {Function} error 错误回调函数 */ util.file2Image = function (file, callback, error) { var image = new Image() var URL = WIN.URL || WIN.webkitURL if ( WIN.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WIN.navigator.userAgent) ) { // 修复IOS上webkit内核浏览器抛出错误 `The operation is insecure` 问题 image.crossOrigin = 'anonymous' } image.alt = file.name image.onerror = function () { if (isFunc(error)) { error('图片加载错误!') } } if (URL) { var url = URL.createObjectURL(file) image.onload = function () { callback(image) URL.revokeObjectURL(url) } image.src = url } else { this.file2DataUrl( file, function (dataUrl) { image.onload = function () { callback(image) } image.src = dataUrl }, error ) } } /** * `url` 转化成 `Image` 对象 * @param {File} url `url` * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.url2Image = function (url, callback, error) { var image = new Image() image.src = url image.onload = function () { callback(image) } image.onerror = function () { if (isFunc(error)) { error('图片加载错误!') } } } /** * `Image` 转化成 `Canvas` 对象 * @param {File} image `Image` 对象 * @param {Number} dWidth 目标宽度 * @param {Number} dHeight 目标高度 * @param {Function} beforeDraw 在图片绘画之前的回调函数 * @param {Function} afterDraw 在图片绘画之后的回调函数 * @param {Number} width 宽 * @param {Number} height 高 * @return {HTMLCanvasElement} `Canvas` 对象 */ util.image2Canvas = function ( image, dWidth, dHeight, beforeDraw, afterDraw, width, height ) { var canvas = document.createElement('canvas') var ctx = canvas.getContext('2d') canvas.width = width || image.naturalWidth canvas.height = height || image.naturalHeight if (isFunc(beforeDraw)) { beforeDraw(ctx, canvas) } ctx.save() ctx.drawImage(image, 0, 0, dWidth, dHeight) ctx.restore() if (isFunc(afterDraw)) { afterDraw(ctx, canvas) } return canvas } /** * `Canvas` 转化成 `data URL` 对象 * @param {File} file `Canvas` 对象 * @param {Float} quality 输出质量比例 * @return {String} `data URL` 字符串 */ util.canvas2DataUrl = function (canvas, quality, type) { return canvas.toDataURL(type || 'image/jpeg', quality) } /** * `data URL` 转化成 `Image` 对象 * @param {File} dataUrl `data URL` 字符串 * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.dataUrl2Image = function (dataUrl, callback, error) { var image = new Image() image.onload = function () { callback(image) } image.error = function () { if (isFunc(error)) { error('图片加载错误!') } } image.src = dataUrl } /** * `data URL` 转化成 `Blob` 对象 * @param {File} dataUrl `data URL` 字符串 * @param {String} type `mime` * @return {Blob} `Blob` 对象 */ util.dataUrl2Blob = function (dataUrl, type) { var data = dataUrl.split(',')[1] var mimePattern = /^data:(.*?)(;base64)?,/ var mime = dataUrl.match(mimePattern)[1] var binStr = atob(data) var len = data.length var arr = new Uint8Array(len) for (var i = 0; i < len; i++) { arr[i] = binStr.charCodeAt(i) } return new Blob([arr], { type: type || mime }) } /** * `Blob` 对象转化成 `data URL` * @param {Blob} blob `Blob` 对象 * @param {Function} callback 成功回调函数 * @param {Function} error 失败回调函数 */ util.blob2DataUrl = function (blob, callback, error) { this.file2DataUrl(blob, callback, error) } /** * `Blob`对象 转化成 `Image` 对象 * @param {Blob} blob `Blob` 对象 * @param {Function} callback 成功回调函数 * @param {Function} callback 失败回调函数 */ util.blob2Image = function (blob, callback, error) { this.file2Image(blob, callback, error) } /** * `Canvas` 对象转化成 `Blob` 对象 * @param {HTMLCanvasElement} canvas `Canvas` 对象 * @param {Function} callback 回调函数 * @param {Float} quality 输出质量比例 * @param {String} type `mime` */ util.canvas2Blob = function (canvas, callback, quality, type) { var _this = this if (!HTMLCanvasElement.prototype.toBlob) { Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', { value: function (callback, type, quality) { var dataUrl = this.toDataURL(type, quality) callback(_this.dataUrl2Blob(dataUrl)) }, }) } canvas.toBlob( function (blob) { callback(blob) }, type || 'image/jpeg', quality || 0.8 ) } /** * 文件上传 * @param {String} url 上传路径 * @param {File} file 文件对象 * @param {Function} callback 回调函数 */ util.upload = function (url, file, callback) { var xhr = new XMLHttpRequest() var fd = new FormData() fd.append('file', file) xhr.onreadystatechange = function () { if (xhr.readyState === 4 && xhr.status === 200) { // 上传成功 callback && callback(xhr.responseText) } else { throw new Error(xhr) } } xhr.open('POST', url, true) xhr.send(fd) } for (var key in util) { if (util.hasOwnProperty(key)) { ImageCompressor[key] = util[key] } } ================================================ FILE: webpack.config.js ================================================ const TerserWebpackPlugin = require('terser-webpack-plugin') const path = require('path') module.exports = { mode: 'none', entry: { 'image-compressor': './src/index.js', 'image-compressor.min': './src/index.js', }, output: { path: path.resolve(__dirname, './dist'), filename: '[name].js', library: 'ImageCompressor', libraryExport: 'default', libraryTarget: 'umd', }, optimization: { minimize: true, minimizer: [ new TerserWebpackPlugin({ include: /\.min\./, }), ], }, }